aboutsummaryrefslogtreecommitdiff
path: root/src/com/android
diff options
context:
space:
mode:
authorNick Chalko <nchalko@google.com>2018-01-17 11:15:16 -0800
committerNick Chalko <nchalko@google.com>2018-01-17 11:20:37 -0800
commit38fef3bf253578f518d1bc727da4afb263194398 (patch)
tree09a06234eda7c54216bca773b6d8407eafe0722d /src/com/android
parentc9889d13513e26649a7708cf2d0562cb592d441a (diff)
downloadTV-38fef3bf253578f518d1bc727da4afb263194398.tar.gz
Fix broken build
This reverts c9889d1 Update aosp build to use a snapshot of exoplyer. by nchalko · 5 hours ago master 8952aa7 Clean format by nchalko · 20 hours ago ba3fb16 Merge "Use a snapshot of exoplayer" by TreeHugger Robot · 18 hours ago ff75e39 Project import generated by Copybara. by Live Channels Team · 22 hours ago 9737fc2 Use a snapshot of exoplayer by Nick Chalko · 20 hours ago 4a5144a Project import generated by Copybara. by Live Channels Team · 6 days ago Bug: 72092981 Bug: 69474774 Change-Id: Ie756857c10bf052c60b6442c3d61252f65b49143
Diffstat (limited to 'src/com/android')
-rw-r--r--src/com/android/exoplayer/MediaFormatUtil.java94
-rw-r--r--src/com/android/exoplayer/MediaSoftwareCodecUtil.java273
-rw-r--r--src/com/android/exoplayer/text/SubtitleView.java321
-rw-r--r--src/com/android/exoplayer2/ext/ffmpeg/FfmpegAudioDecoder.java126
-rw-r--r--src/com/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java75
-rw-r--r--src/com/android/tv/ApplicationSingletons.java (renamed from src/com/android/tv/TvSingletons.java)40
-rw-r--r--src/com/android/tv/AudioManagerHelper.java36
-rw-r--r--src/com/android/tv/Features.java193
-rw-r--r--src/com/android/tv/InputSessionManager.java2
-rw-r--r--src/com/android/tv/LauncherActivity.java27
-rw-r--r--src/com/android/tv/MainActivity.java366
-rw-r--r--src/com/android/tv/MediaSessionWrapper.java18
-rw-r--r--src/com/android/tv/SetupPassthroughActivity.java51
-rw-r--r--src/com/android/tv/Starter.java42
-rw-r--r--src/com/android/tv/TimeShiftManager.java11
-rw-r--r--src/com/android/tv/TvApplication.java243
-rw-r--r--src/com/android/tv/TvFeatures.java103
-rw-r--r--src/com/android/tv/analytics/SendChannelStatusRunnable.java3
-rw-r--r--src/com/android/tv/app/LiveTvApplication.java137
-rw-r--r--src/com/android/tv/config/ConfigKeys.java23
-rw-r--r--src/com/android/tv/config/DefaultConfigManager.java54
-rw-r--r--src/com/android/tv/config/RemoteConfig.java43
-rw-r--r--src/com/android/tv/config/RemoteConfigFeature.java40
-rw-r--r--src/com/android/tv/config/RemoteConfigUtils.java42
-rw-r--r--src/com/android/tv/customization/CustomAction.java68
-rw-r--r--src/com/android/tv/customization/TvCustomizationManager.java271
-rw-r--r--src/com/android/tv/data/Channel.java9
-rw-r--r--src/com/android/tv/data/ChannelDataManager.java5
-rw-r--r--src/com/android/tv/data/ChannelLogoFetcher.java5
-rw-r--r--src/com/android/tv/data/ChannelNumber.java19
-rw-r--r--src/com/android/tv/data/InternalDataUtils.java1
-rw-r--r--src/com/android/tv/data/Lineup.java53
-rw-r--r--src/com/android/tv/data/PreviewDataManager.java15
-rw-r--r--src/com/android/tv/data/PreviewProgramContent.java49
-rw-r--r--src/com/android/tv/data/Program.java8
-rw-r--r--src/com/android/tv/data/ProgramDataManager.java3
-rw-r--r--src/com/android/tv/data/WatchedHistoryManager.java18
-rw-r--r--src/com/android/tv/data/epg/AutoValue_EpgReader_EpgChannel.java86
-rw-r--r--src/com/android/tv/data/epg/EpgFetchHelper.java9
-rw-r--r--src/com/android/tv/data/epg/EpgFetchService.java70
-rw-r--r--src/com/android/tv/data/epg/EpgFetcher.java705
-rw-r--r--src/com/android/tv/data/epg/EpgFetcherImpl.java814
-rw-r--r--src/com/android/tv/data/epg/EpgInputWhiteList.java92
-rw-r--r--src/com/android/tv/data/epg/EpgReader.java24
-rw-r--r--src/com/android/tv/data/epg/StubEpgReader.java11
-rw-r--r--src/com/android/tv/dialog/DvrHistoryDialogFragment.java6
-rw-r--r--src/com/android/tv/dialog/PinDialogFragment.java8
-rw-r--r--src/com/android/tv/dialog/SafeDismissDialogFragment.java4
-rw-r--r--src/com/android/tv/dvr/BaseDvrDataManager.java2
-rw-r--r--src/com/android/tv/dvr/DvrDataManagerImpl.java31
-rw-r--r--src/com/android/tv/dvr/DvrManager.java16
-rw-r--r--src/com/android/tv/dvr/DvrScheduleManager.java14
-rw-r--r--src/com/android/tv/dvr/DvrStorageStatusManager.java286
-rw-r--r--src/com/android/tv/dvr/DvrWatchedPositionManager.java2
-rw-r--r--src/com/android/tv/dvr/data/RecordedProgram.java4
-rw-r--r--src/com/android/tv/dvr/data/ScheduledRecording.java22
-rw-r--r--src/com/android/tv/dvr/data/SeriesRecording.java2
-rw-r--r--src/com/android/tv/dvr/provider/AsyncDvrDbTask.java3
-rw-r--r--src/com/android/tv/dvr/provider/DvrDatabaseHelper.java2
-rw-r--r--src/com/android/tv/dvr/provider/DvrDbSync.java6
-rw-r--r--src/com/android/tv/dvr/provider/EpisodicProgramLoadTask.java6
-rw-r--r--src/com/android/tv/dvr/recorder/ConflictChecker.java34
-rw-r--r--src/com/android/tv/dvr/recorder/DvrRecordingService.java12
-rw-r--r--src/com/android/tv/dvr/recorder/DvrStartRecordingReceiver.java7
-rw-r--r--src/com/android/tv/dvr/recorder/InputTaskScheduler.java3
-rw-r--r--src/com/android/tv/dvr/recorder/RecordingScheduler.java9
-rw-r--r--src/com/android/tv/dvr/recorder/RecordingTask.java13
-rw-r--r--src/com/android/tv/dvr/recorder/ScheduledProgramReaper.java2
-rw-r--r--src/com/android/tv/dvr/recorder/SeriesRecordingScheduler.java34
-rw-r--r--src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java4
-rw-r--r--src/com/android/tv/dvr/ui/DvrAlreadyScheduledFragment.java4
-rw-r--r--src/com/android/tv/dvr/ui/DvrChannelRecordDurationOptionFragment.java6
-rw-r--r--src/com/android/tv/dvr/ui/DvrConflictFragment.java10
-rw-r--r--src/com/android/tv/dvr/ui/DvrGuidedStepFragment.java16
-rw-r--r--src/com/android/tv/dvr/ui/DvrInsufficientSpaceErrorFragment.java4
-rw-r--r--src/com/android/tv/dvr/ui/DvrPrioritySettingsFragment.java10
-rw-r--r--src/com/android/tv/dvr/ui/DvrScheduleFragment.java14
-rw-r--r--src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java4
-rw-r--r--src/com/android/tv/dvr/ui/DvrSeriesDeletionFragment.java8
-rw-r--r--src/com/android/tv/dvr/ui/DvrSeriesScheduledFragment.java8
-rw-r--r--src/com/android/tv/dvr/ui/DvrSeriesSettingsActivity.java4
-rw-r--r--src/com/android/tv/dvr/ui/DvrSeriesSettingsFragment.java14
-rw-r--r--src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java4
-rw-r--r--src/com/android/tv/dvr/ui/DvrStopSeriesRecordingFragment.java5
-rw-r--r--src/com/android/tv/dvr/ui/DvrUiHelper.java23
-rw-r--r--src/com/android/tv/dvr/ui/TrackedGuidedStepFragment.java4
-rw-r--r--src/com/android/tv/dvr/ui/browse/CurrentRecordingDetailsFragment.java6
-rw-r--r--src/com/android/tv/dvr/ui/browse/DetailsContent.java6
-rw-r--r--src/com/android/tv/dvr/ui/browse/DetailsViewBackgroundHelper.java1
-rw-r--r--src/com/android/tv/dvr/ui/browse/DvrBrowseActivity.java4
-rw-r--r--src/com/android/tv/dvr/ui/browse/DvrBrowseFragment.java9
-rw-r--r--src/com/android/tv/dvr/ui/browse/DvrDetailsActivity.java4
-rw-r--r--src/com/android/tv/dvr/ui/browse/DvrDetailsFragment.java10
-rw-r--r--src/com/android/tv/dvr/ui/browse/DvrItemPresenter.java5
-rw-r--r--src/com/android/tv/dvr/ui/browse/FullSchedulesCardPresenter.java4
-rw-r--r--src/com/android/tv/dvr/ui/browse/RecordedProgramDetailsFragment.java8
-rw-r--r--src/com/android/tv/dvr/ui/browse/RecordedProgramPresenter.java4
-rw-r--r--src/com/android/tv/dvr/ui/browse/RecordingCardView.java4
-rw-r--r--src/com/android/tv/dvr/ui/browse/RecordingDetailsFragment.java4
-rw-r--r--src/com/android/tv/dvr/ui/browse/ScheduledRecordingDetailsFragment.java4
-rw-r--r--src/com/android/tv/dvr/ui/browse/ScheduledRecordingPresenter.java4
-rw-r--r--src/com/android/tv/dvr/ui/browse/SeriesRecordingDetailsFragment.java8
-rw-r--r--src/com/android/tv/dvr/ui/browse/SeriesRecordingPresenter.java5
-rw-r--r--src/com/android/tv/dvr/ui/list/BaseDvrSchedulesFragment.java7
-rw-r--r--src/com/android/tv/dvr/ui/list/DvrSchedulesActivity.java4
-rw-r--r--src/com/android/tv/dvr/ui/list/DvrSeriesSchedulesFragment.java5
-rw-r--r--src/com/android/tv/dvr/ui/list/ScheduleRowAdapter.java8
-rw-r--r--src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java10
-rw-r--r--src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java6
-rw-r--r--src/com/android/tv/dvr/ui/list/SeriesScheduleRowAdapter.java5
-rw-r--r--src/com/android/tv/dvr/ui/playback/DvrPlaybackActivity.java4
-rw-r--r--src/com/android/tv/dvr/ui/playback/DvrPlaybackMediaSessionHelper.java6
-rw-r--r--src/com/android/tv/dvr/ui/playback/DvrPlaybackOverlayFragment.java6
-rw-r--r--src/com/android/tv/experiments/ExperimentFlag.java62
-rw-r--r--src/com/android/tv/experiments/Experiments.java42
-rw-r--r--src/com/android/tv/guide/ProgramGuide.java7
-rw-r--r--src/com/android/tv/guide/ProgramItemView.java8
-rw-r--r--src/com/android/tv/guide/ProgramTableAdapter.java21
-rw-r--r--src/com/android/tv/license/LicenseUtils.java1
-rw-r--r--src/com/android/tv/menu/ChannelsRowAdapter.java20
-rw-r--r--src/com/android/tv/menu/CustomizableOptionsRowAdapter.java2
-rw-r--r--src/com/android/tv/menu/Menu.java10
-rw-r--r--src/com/android/tv/menu/MenuLayoutManager.java4
-rw-r--r--src/com/android/tv/menu/MenuRowFactory.java17
-rw-r--r--src/com/android/tv/menu/OptionsRowAdapter.java4
-rw-r--r--src/com/android/tv/menu/PartnerOptionsRowAdapter.java2
-rw-r--r--src/com/android/tv/menu/PlayControlsRowView.java10
-rw-r--r--src/com/android/tv/menu/TvOptionsRowAdapter.java10
-rw-r--r--src/com/android/tv/onboarding/NewSourcesFragment.java11
-rw-r--r--src/com/android/tv/onboarding/OnboardingActivity.java19
-rw-r--r--src/com/android/tv/onboarding/SetupSourcesFragment.java21
-rw-r--r--src/com/android/tv/onboarding/WelcomeFragment.java107
-rw-r--r--src/com/android/tv/parental/ContentRatingsManager.java12
-rw-r--r--src/com/android/tv/parental/ContentRatingsParser.java1
-rw-r--r--src/com/android/tv/parental/ParentalControlSettings.java2
-rw-r--r--src/com/android/tv/perf/EventNames.java1
-rw-r--r--src/com/android/tv/receiver/AudioCapabilitiesReceiver.java11
-rw-r--r--src/com/android/tv/receiver/BootCompletedReceiver.java13
-rw-r--r--src/com/android/tv/receiver/GlobalKeyReceiver.java6
-rw-r--r--src/com/android/tv/receiver/PackageIntentsReceiver.java11
-rw-r--r--src/com/android/tv/recommendation/ChannelPreviewUpdater.java15
-rw-r--r--src/com/android/tv/recommendation/ChannelRecord.java14
-rw-r--r--src/com/android/tv/recommendation/NotificationService.java18
-rw-r--r--src/com/android/tv/recommendation/RecommendationDataManager.java7
-rw-r--r--src/com/android/tv/recommendation/RecordedProgramPreviewUpdater.java13
-rw-r--r--src/com/android/tv/search/DataManagerSearch.java9
-rw-r--r--src/com/android/tv/search/LocalSearchProvider.java10
-rw-r--r--src/com/android/tv/search/ProgramGuideSearchFragment.java7
-rw-r--r--src/com/android/tv/search/TvProviderSearch.java3
-rw-r--r--src/com/android/tv/setup/SystemSetupActivity.java9
-rw-r--r--src/com/android/tv/tuner/ChannelScanFileParser.java104
-rw-r--r--src/com/android/tv/tuner/DvbDeviceAccessor.java222
-rw-r--r--src/com/android/tv/tuner/DvbTunerHal.java177
-rw-r--r--src/com/android/tv/tuner/TunerHal.java358
-rw-r--r--src/com/android/tv/tuner/TunerInputController.java430
-rw-r--r--src/com/android/tv/tuner/TunerPreferenceProvider.java214
-rw-r--r--src/com/android/tv/tuner/TunerPreferences.java428
-rw-r--r--src/com/android/tv/tuner/cc/CaptionLayout.java77
-rw-r--r--src/com/android/tv/tuner/cc/CaptionTrackRenderer.java340
-rw-r--r--src/com/android/tv/tuner/cc/CaptionWindowLayout.java680
-rw-r--r--src/com/android/tv/tuner/cc/Cea708Parser.java922
-rw-r--r--src/com/android/tv/tuner/data/Cea708Data.java329
-rw-r--r--src/com/android/tv/tuner/data/PsiData.java93
-rw-r--r--src/com/android/tv/tuner/data/PsipData.java871
-rw-r--r--src/com/android/tv/tuner/data/TunerChannel.java517
-rw-r--r--src/com/android/tv/tuner/exoplayer/Cea708TextTrackRenderer.java305
-rw-r--r--src/com/android/tv/tuner/exoplayer/ExoPlayerExtractorsFactory.java41
-rw-r--r--src/com/android/tv/tuner/exoplayer/ExoPlayerSampleExtractor.java610
-rw-r--r--src/com/android/tv/tuner/exoplayer/FileSampleExtractor.java139
-rw-r--r--src/com/android/tv/tuner/exoplayer/MpegTsPlayer.java672
-rw-r--r--src/com/android/tv/tuner/exoplayer/MpegTsRendererBuilder.java77
-rw-r--r--src/com/android/tv/tuner/exoplayer/MpegTsSampleExtractor.java345
-rw-r--r--src/com/android/tv/tuner/exoplayer/MpegTsSampleSource.java195
-rw-r--r--src/com/android/tv/tuner/exoplayer/MpegTsVideoTrackRenderer.java111
-rw-r--r--src/com/android/tv/tuner/exoplayer/SampleExtractor.java131
-rw-r--r--src/com/android/tv/tuner/exoplayer/audio/AudioClock.java94
-rw-r--r--src/com/android/tv/tuner/exoplayer/audio/AudioDecoder.java69
-rw-r--r--src/com/android/tv/tuner/exoplayer/audio/AudioTrackMonitor.java140
-rw-r--r--src/com/android/tv/tuner/exoplayer/audio/AudioTrackWrapper.java174
-rw-r--r--src/com/android/tv/tuner/exoplayer/audio/MediaCodecAudioDecoder.java233
-rw-r--r--src/com/android/tv/tuner/exoplayer/audio/MpegTsDefaultAudioTrackRenderer.java743
-rw-r--r--src/com/android/tv/tuner/exoplayer/audio/MpegTsMediaCodecAudioTrackRenderer.java94
-rw-r--r--src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java682
-rw-r--r--src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java391
-rw-r--r--src/com/android/tv/tuner/exoplayer/buffer/RecordingSampleBuffer.java303
-rw-r--r--src/com/android/tv/tuner/exoplayer/buffer/SampleChunk.java428
-rw-r--r--src/com/android/tv/tuner/exoplayer/buffer/SampleChunkIoHelper.java464
-rw-r--r--src/com/android/tv/tuner/exoplayer/buffer/SamplePool.java67
-rw-r--r--src/com/android/tv/tuner/exoplayer/buffer/SampleQueue.java72
-rw-r--r--src/com/android/tv/tuner/exoplayer/buffer/SimpleSampleBuffer.java177
-rw-r--r--src/com/android/tv/tuner/exoplayer/buffer/TrickplayStorageManager.java145
-rw-r--r--src/com/android/tv/tuner/exoplayer/ffmpeg/FfmpegDecoderClient.java247
-rw-r--r--src/com/android/tv/tuner/exoplayer/ffmpeg/FfmpegDecoderService.java202
-rw-r--r--src/com/android/tv/tuner/exoplayer/ffmpeg/IFfmpegDecoder.aidl29
-rw-r--r--src/com/android/tv/tuner/layout/ScaledLayout.java290
-rw-r--r--src/com/android/tv/tuner/setup/ConnectionTypeFragment.java98
-rw-r--r--src/com/android/tv/tuner/setup/PostalCodeFragment.java179
-rw-r--r--src/com/android/tv/tuner/setup/ScanFragment.java542
-rw-r--r--src/com/android/tv/tuner/setup/ScanResultFragment.java133
-rw-r--r--src/com/android/tv/tuner/setup/TunerSetupActivity.java548
-rw-r--r--src/com/android/tv/tuner/setup/WelcomeFragment.java127
-rw-r--r--src/com/android/tv/tuner/source/FileTsStreamer.java487
-rw-r--r--src/com/android/tv/tuner/source/TsDataSource.java49
-rw-r--r--src/com/android/tv/tuner/source/TsDataSourceManager.java136
-rw-r--r--src/com/android/tv/tuner/source/TsStreamWriter.java238
-rw-r--r--src/com/android/tv/tuner/source/TsStreamer.java53
-rw-r--r--src/com/android/tv/tuner/source/TunerTsStreamer.java420
-rw-r--r--src/com/android/tv/tuner/source/TunerTsStreamerManager.java303
-rw-r--r--src/com/android/tv/tuner/ts/SectionParser.java2083
-rw-r--r--src/com/android/tv/tuner/ts/TsParser.java543
-rw-r--r--src/com/android/tv/tuner/tvinput/ChannelDataManager.java801
-rw-r--r--src/com/android/tv/tuner/tvinput/EventDetector.java349
-rw-r--r--src/com/android/tv/tuner/tvinput/FileSourceEventDetector.java259
-rw-r--r--src/com/android/tv/tuner/tvinput/PlaybackBufferListener.java38
-rw-r--r--src/com/android/tv/tuner/tvinput/TunerDebug.java147
-rw-r--r--src/com/android/tv/tuner/tvinput/TunerRecordingSession.java101
-rw-r--r--src/com/android/tv/tuner/tvinput/TunerRecordingSessionWorker.java683
-rw-r--r--src/com/android/tv/tuner/tvinput/TunerSession.java341
-rw-r--r--src/com/android/tv/tuner/tvinput/TunerSessionWorker.java1852
-rw-r--r--src/com/android/tv/tuner/tvinput/TunerStorageCleanUpService.java177
-rw-r--r--src/com/android/tv/tuner/tvinput/TunerTvInputService.java123
-rw-r--r--src/com/android/tv/tuner/util/ByteArrayBuffer.java152
-rw-r--r--src/com/android/tv/tuner/util/ConvertUtils.java33
-rw-r--r--src/com/android/tv/tuner/util/GlobalSettingsUtils.java34
-rw-r--r--src/com/android/tv/tuner/util/Ints.java26
-rw-r--r--src/com/android/tv/tuner/util/PostalCodeUtils.java138
-rw-r--r--src/com/android/tv/tuner/util/StatusTextUtils.java137
-rw-r--r--src/com/android/tv/tuner/util/SystemPropertiesProxy.java79
-rw-r--r--src/com/android/tv/tuner/util/TisConfiguration.java20
-rw-r--r--src/com/android/tv/tuner/util/TunerInputInfoUtils.java111
-rw-r--r--src/com/android/tv/ui/AppLayerTvView.java6
-rw-r--r--src/com/android/tv/ui/ChannelBannerView.java6
-rw-r--r--src/com/android/tv/ui/KeypadChannelSwitchView.java6
-rw-r--r--src/com/android/tv/ui/SelectInputView.java11
-rw-r--r--src/com/android/tv/ui/TunableTvView.java29
-rw-r--r--src/com/android/tv/ui/TvOverlayManager.java13
-rw-r--r--src/com/android/tv/ui/TvViewUiManager.java4
-rw-r--r--src/com/android/tv/ui/sidepanel/CustomizeChannelListFragment.java2
-rw-r--r--src/com/android/tv/ui/sidepanel/DeveloperOptionFragment.java26
-rw-r--r--src/com/android/tv/ui/sidepanel/SettingsFragment.java32
-rw-r--r--src/com/android/tv/ui/sidepanel/SideFragment.java8
-rw-r--r--src/com/android/tv/ui/sidepanel/parentalcontrols/ChannelsBlockedFragment.java11
-rw-r--r--src/com/android/tv/ui/sidepanel/parentalcontrols/RatingsFragment.java2
-rw-r--r--src/com/android/tv/util/AccountHelper.java (renamed from src/com/android/tv/util/account/AccountHelperImpl.java)29
-rw-r--r--src/com/android/tv/util/AsyncDbTask.java4
-rw-r--r--src/com/android/tv/util/BitmapUtils.java1
-rw-r--r--src/com/android/tv/util/Clock.java64
-rw-r--r--src/com/android/tv/util/Debug.java50
-rw-r--r--src/com/android/tv/util/DurationTimer.java80
-rw-r--r--src/com/android/tv/util/ImageLoader.java1
-rw-r--r--src/com/android/tv/util/LocationUtils.java137
-rw-r--r--src/com/android/tv/util/NamedThreadFactory.java45
-rw-r--r--src/com/android/tv/util/NetworkTrafficTags.java63
-rw-r--r--src/com/android/tv/util/OnboardingUtils.java8
-rw-r--r--src/com/android/tv/util/PermissionUtils.java53
-rw-r--r--src/com/android/tv/util/RecurringRunner.java2
-rw-r--r--src/com/android/tv/util/SetupUtils.java53
-rw-r--r--src/com/android/tv/util/SqlParams.java74
-rw-r--r--src/com/android/tv/util/StringUtils.java34
-rw-r--r--src/com/android/tv/util/SystemProperties.java53
-rw-r--r--src/com/android/tv/util/TvInputManagerHelper.java104
-rw-r--r--src/com/android/tv/util/Utils.java69
-rw-r--r--src/com/android/tv/util/ViewCache.java15
-rw-r--r--src/com/android/tv/util/account/AccountHelper.java38
264 files changed, 28995 insertions, 3169 deletions
diff --git a/src/com/android/exoplayer/MediaFormatUtil.java b/src/com/android/exoplayer/MediaFormatUtil.java
new file mode 100644
index 00000000..151c6dd5
--- /dev/null
+++ b/src/com/android/exoplayer/MediaFormatUtil.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer;
+
+import android.support.annotation.Nullable;
+import com.google.android.exoplayer.util.MimeTypes;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+
+/** {@link MediaFormat} creation helper util */
+public class MediaFormatUtil {
+
+ /**
+ * Creates {@link MediaFormat} from {@link android.media.MediaFormat}. Since {@link
+ * com.google.android.exoplayer.TrackRenderer} uses {@link MediaFormat}, {@link
+ * android.media.MediaFormat} should be converted to be used with ExoPlayer.
+ */
+ public static MediaFormat createMediaFormat(android.media.MediaFormat format) {
+ String mimeType = format.getString(android.media.MediaFormat.KEY_MIME);
+ String language = getOptionalStringV16(format, android.media.MediaFormat.KEY_LANGUAGE);
+ int maxInputSize =
+ getOptionalIntegerV16(format, android.media.MediaFormat.KEY_MAX_INPUT_SIZE);
+ int width = getOptionalIntegerV16(format, android.media.MediaFormat.KEY_WIDTH);
+ int height = getOptionalIntegerV16(format, android.media.MediaFormat.KEY_HEIGHT);
+ int rotationDegrees = getOptionalIntegerV16(format, "rotation-degrees");
+ int channelCount =
+ getOptionalIntegerV16(format, android.media.MediaFormat.KEY_CHANNEL_COUNT);
+ int sampleRate = getOptionalIntegerV16(format, android.media.MediaFormat.KEY_SAMPLE_RATE);
+ int encoderDelay = getOptionalIntegerV16(format, "encoder-delay");
+ int encoderPadding = getOptionalIntegerV16(format, "encoder-padding");
+ ArrayList<byte[]> initializationData = new ArrayList<>();
+ for (int i = 0; format.containsKey("csd-" + i); i++) {
+ ByteBuffer buffer = format.getByteBuffer("csd-" + i);
+ byte[] data = new byte[buffer.limit()];
+ buffer.get(data);
+ initializationData.add(data);
+ buffer.flip();
+ }
+ long durationUs =
+ format.containsKey(android.media.MediaFormat.KEY_DURATION)
+ ? format.getLong(android.media.MediaFormat.KEY_DURATION)
+ : C.UNKNOWN_TIME_US;
+ int pcmEncoding =
+ MimeTypes.AUDIO_RAW.equals(mimeType) ? C.ENCODING_PCM_16BIT : MediaFormat.NO_VALUE;
+ MediaFormat mediaFormat =
+ new MediaFormat(
+ null,
+ mimeType,
+ MediaFormat.NO_VALUE,
+ maxInputSize,
+ durationUs,
+ width,
+ height,
+ rotationDegrees,
+ MediaFormat.NO_VALUE,
+ channelCount,
+ sampleRate,
+ language,
+ MediaFormat.OFFSET_SAMPLE_RELATIVE,
+ initializationData,
+ false,
+ MediaFormat.NO_VALUE,
+ MediaFormat.NO_VALUE,
+ pcmEncoding,
+ encoderDelay,
+ encoderPadding,
+ null,
+ MediaFormat.NO_VALUE);
+ mediaFormat.setFrameworkFormatV16(format);
+ return mediaFormat;
+ }
+
+ @Nullable
+ private static String getOptionalStringV16(android.media.MediaFormat format, String key) {
+ return format.containsKey(key) ? format.getString(key) : null;
+ }
+
+ private static int getOptionalIntegerV16(android.media.MediaFormat format, String key) {
+ return format.containsKey(key) ? format.getInteger(key) : MediaFormat.NO_VALUE;
+ }
+}
diff --git a/src/com/android/exoplayer/MediaSoftwareCodecUtil.java b/src/com/android/exoplayer/MediaSoftwareCodecUtil.java
new file mode 100644
index 00000000..cf74f106
--- /dev/null
+++ b/src/com/android/exoplayer/MediaSoftwareCodecUtil.java
@@ -0,0 +1,273 @@
+/*
+ * 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.google.android.exoplayer;
+
+import android.annotation.TargetApi;
+import android.media.MediaCodecInfo;
+import android.media.MediaCodecList;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Pair;
+import com.google.android.exoplayer.util.MimeTypes;
+import java.util.HashMap;
+
+/**
+ * Mostly copied from {@link com.google.android.exoplayer.MediaCodecUtil} in order to choose
+ * software codec over hardware codec.
+ */
+public class MediaSoftwareCodecUtil {
+ private static final String TAG = "MediaSoftwareCodecUtil";
+
+ /**
+ * Thrown when an error occurs querying the device for its underlying media capabilities.
+ *
+ * <p>Such failures are not expected in normal operation and are normally temporary (e.g. if the
+ * mediaserver process has crashed and is yet to restart).
+ */
+ public static class DecoderQueryException extends Exception {
+
+ private DecoderQueryException(Throwable cause) {
+ super("Failed to query underlying media codecs", cause);
+ }
+ }
+
+ private static final HashMap<CodecKey, Pair<String, MediaCodecInfo.CodecCapabilities>>
+ sSwCodecs = new HashMap<>();
+
+ /** Gets information about the software decoder that will be used for a given mime type. */
+ public static DecoderInfo getSoftwareDecoderInfo(String mimeType, boolean secure)
+ throws DecoderQueryException {
+ // TODO: Add a test for this method.
+ Pair<String, MediaCodecInfo.CodecCapabilities> info =
+ getMediaSoftwareCodecInfo(mimeType, secure);
+ if (info == null) {
+ return null;
+ }
+ return new DecoderInfo(info.first, info.second);
+ }
+
+ /** Returns the name of the software decoder and its capabilities for the given mimeType. */
+ private static synchronized Pair<String, MediaCodecInfo.CodecCapabilities>
+ getMediaSoftwareCodecInfo(String mimeType, boolean secure)
+ throws DecoderQueryException {
+ CodecKey key = new CodecKey(mimeType, secure);
+ if (sSwCodecs.containsKey(key)) {
+ return sSwCodecs.get(key);
+ }
+ MediaCodecListCompat mediaCodecList = new MediaCodecListCompatV21(secure);
+ Pair<String, MediaCodecInfo.CodecCapabilities> codecInfo =
+ getMediaSoftwareCodecInfo(key, mediaCodecList);
+ if (secure && codecInfo == null) {
+ // Some devices don't list secure decoders on API level 21. Try the legacy path.
+ mediaCodecList = new MediaCodecListCompatV16();
+ codecInfo = getMediaSoftwareCodecInfo(key, mediaCodecList);
+ if (codecInfo != null) {
+ Log.w(
+ TAG,
+ "MediaCodecList API didn't list secure decoder for: "
+ + mimeType
+ + ". Assuming: "
+ + codecInfo.first);
+ }
+ }
+ return codecInfo;
+ }
+
+ private static Pair<String, MediaCodecInfo.CodecCapabilities> getMediaSoftwareCodecInfo(
+ CodecKey key, MediaCodecListCompat mediaCodecList) throws DecoderQueryException {
+ try {
+ return getMediaSoftwareCodecInfoInternal(key, mediaCodecList);
+ } catch (Exception e) {
+ // If the underlying mediaserver is in a bad state, we may catch an
+ // IllegalStateException or an IllegalArgumentException here.
+ throw new DecoderQueryException(e);
+ }
+ }
+
+ private static Pair<String, MediaCodecInfo.CodecCapabilities> getMediaSoftwareCodecInfoInternal(
+ CodecKey key, MediaCodecListCompat mediaCodecList) {
+ String mimeType = key.mimeType;
+ int numberOfCodecs = mediaCodecList.getCodecCount();
+ boolean secureDecodersExplicit = mediaCodecList.secureDecodersExplicit();
+ // Note: MediaCodecList is sorted by the framework such that the best decoders come first.
+ for (int i = 0; i < numberOfCodecs; i++) {
+ MediaCodecInfo info = mediaCodecList.getCodecInfoAt(i);
+ String codecName = info.getName();
+ if (!info.isEncoder()
+ && codecName.startsWith("OMX.google.")
+ && (secureDecodersExplicit || !codecName.endsWith(".secure"))) {
+ String[] supportedTypes = info.getSupportedTypes();
+ for (String supportedType : supportedTypes) {
+ if (supportedType.equalsIgnoreCase(mimeType)) {
+ MediaCodecInfo.CodecCapabilities capabilities =
+ info.getCapabilitiesForType(supportedType);
+ boolean secure =
+ mediaCodecList.isSecurePlaybackSupported(
+ key.mimeType, capabilities);
+ if (!secureDecodersExplicit) {
+ // Cache variants for both insecure and (if we think it's supported)
+ // secure playback.
+ sSwCodecs.put(
+ key.secure ? new CodecKey(mimeType, false) : key,
+ Pair.create(codecName, capabilities));
+ if (secure) {
+ sSwCodecs.put(
+ key.secure ? key : new CodecKey(mimeType, true),
+ Pair.create(codecName + ".secure", capabilities));
+ }
+ } else {
+ // Only cache this variant. If both insecure and secure decoders are
+ // available, they should both be listed separately.
+ sSwCodecs.put(
+ key.secure == secure ? key : new CodecKey(mimeType, secure),
+ Pair.create(codecName, capabilities));
+ }
+ if (sSwCodecs.containsKey(key)) {
+ return sSwCodecs.get(key);
+ }
+ }
+ }
+ }
+ }
+ sSwCodecs.put(key, null);
+ return null;
+ }
+
+ private interface MediaCodecListCompat {
+
+ /** Returns the number of codecs in the list. */
+ int getCodecCount();
+
+ /**
+ * Returns the info at the specified index in the list.
+ *
+ * @param index The index.
+ */
+ MediaCodecInfo getCodecInfoAt(int index);
+
+ /** Returns whether secure decoders are explicitly listed, if present. */
+ boolean secureDecodersExplicit();
+
+ /**
+ * Returns true if secure playback is supported for the given {@link
+ * android.media.MediaCodecInfo.CodecCapabilities}, which should have been obtained from a
+ * {@link MediaCodecInfo} obtained from this list.
+ */
+ boolean isSecurePlaybackSupported(
+ String mimeType, MediaCodecInfo.CodecCapabilities capabilities);
+ }
+
+ @TargetApi(21)
+ private static final class MediaCodecListCompatV21 implements MediaCodecListCompat {
+
+ private final int codecKind;
+
+ private MediaCodecInfo[] mediaCodecInfos;
+
+ public MediaCodecListCompatV21(boolean includeSecure) {
+ codecKind = includeSecure ? MediaCodecList.ALL_CODECS : MediaCodecList.REGULAR_CODECS;
+ }
+
+ @Override
+ public int getCodecCount() {
+ ensureMediaCodecInfosInitialized();
+ return mediaCodecInfos.length;
+ }
+
+ @Override
+ public MediaCodecInfo getCodecInfoAt(int index) {
+ ensureMediaCodecInfosInitialized();
+ return mediaCodecInfos[index];
+ }
+
+ @Override
+ public boolean secureDecodersExplicit() {
+ return true;
+ }
+
+ @Override
+ public boolean isSecurePlaybackSupported(
+ String mimeType, MediaCodecInfo.CodecCapabilities capabilities) {
+ return capabilities.isFeatureSupported(
+ MediaCodecInfo.CodecCapabilities.FEATURE_SecurePlayback);
+ }
+
+ private void ensureMediaCodecInfosInitialized() {
+ if (mediaCodecInfos == null) {
+ mediaCodecInfos = new MediaCodecList(codecKind).getCodecInfos();
+ }
+ }
+ }
+
+ @SuppressWarnings("deprecation")
+ private static final class MediaCodecListCompatV16 implements MediaCodecListCompat {
+
+ @Override
+ public int getCodecCount() {
+ return MediaCodecList.getCodecCount();
+ }
+
+ @Override
+ public MediaCodecInfo getCodecInfoAt(int index) {
+ return MediaCodecList.getCodecInfoAt(index);
+ }
+
+ @Override
+ public boolean secureDecodersExplicit() {
+ return false;
+ }
+
+ @Override
+ public boolean isSecurePlaybackSupported(
+ String mimeType, MediaCodecInfo.CodecCapabilities capabilities) {
+ // Secure decoders weren't explicitly listed prior to API level 21. We assume that
+ // a secure H264 decoder exists.
+ return MimeTypes.VIDEO_H264.equals(mimeType);
+ }
+ }
+
+ private static final class CodecKey {
+
+ public final String mimeType;
+ public final boolean secure;
+
+ public CodecKey(String mimeType, boolean secure) {
+ this.mimeType = mimeType;
+ this.secure = secure;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((mimeType == null) ? 0 : mimeType.hashCode());
+ result = 2 * result + (secure ? 0 : 1);
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof CodecKey)) {
+ return false;
+ }
+ CodecKey other = (CodecKey) obj;
+ return TextUtils.equals(mimeType, other.mimeType) && secure == other.secure;
+ }
+ }
+}
diff --git a/src/com/android/exoplayer/text/SubtitleView.java b/src/com/android/exoplayer/text/SubtitleView.java
new file mode 100644
index 00000000..e930ef2d
--- /dev/null
+++ b/src/com/android/exoplayer/text/SubtitleView.java
@@ -0,0 +1,321 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.exoplayer.text;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Paint.Join;
+import android.graphics.Paint.Style;
+import android.graphics.RectF;
+import android.graphics.Typeface;
+import android.text.Layout.Alignment;
+import android.text.StaticLayout;
+import android.text.TextPaint;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.view.View;
+import com.google.android.exoplayer.util.Util;
+import java.util.ArrayList;
+
+/**
+ * Since this class does not exist in recent version of ExoPlayer and used by {@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 {
+ /** Ratio of inner padding to font size. */
+ private static final float INNER_PADDING_RATIO = 0.125f;
+
+ /** Temporary rectangle used for computing line bounds. */
+ private final RectF mLineBounds = new RectF();
+
+ // Styled dimensions.
+ private final float mCornerRadius;
+ private final float mOutlineWidth;
+ private final float mShadowRadius;
+ private final float mShadowOffset;
+
+ private final TextPaint mTextPaint;
+ private final Paint mPaint;
+
+ private CharSequence mText;
+
+ private int mForegroundColor;
+ private int mBackgroundColor;
+ private int mEdgeColor;
+ private int mEdgeType;
+
+ private boolean mHasMeasurements;
+ private int mLastMeasuredWidth;
+ private StaticLayout mLayout;
+
+ private Alignment mAlignment;
+ private final float mSpacingMult;
+ private final float mSpacingAdd;
+ private int mInnerPaddingX;
+ private float mWhiteSpaceWidth;
+ private ArrayList<Integer> mPrefixSpaces = new ArrayList<>();
+
+ public SubtitleView(Context context) {
+ this(context, null);
+ }
+
+ public SubtitleView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public SubtitleView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+
+ int[] viewAttr = {
+ android.R.attr.text,
+ android.R.attr.textSize,
+ android.R.attr.lineSpacingExtra,
+ android.R.attr.lineSpacingMultiplier
+ };
+ TypedArray a = context.obtainStyledAttributes(attrs, viewAttr, defStyleAttr, 0);
+ CharSequence text = a.getText(0);
+ int textSize = a.getDimensionPixelSize(1, 15);
+ mSpacingAdd = a.getDimensionPixelSize(2, 0);
+ mSpacingMult = a.getFloat(3, 1);
+ a.recycle();
+
+ Resources resources = getContext().getResources();
+ DisplayMetrics displayMetrics = resources.getDisplayMetrics();
+ int twoDpInPx =
+ Math.round((2f * displayMetrics.densityDpi) / DisplayMetrics.DENSITY_DEFAULT);
+ mCornerRadius = twoDpInPx;
+ mOutlineWidth = twoDpInPx;
+ mShadowRadius = twoDpInPx;
+ mShadowOffset = twoDpInPx;
+
+ mTextPaint = new TextPaint();
+ mTextPaint.setAntiAlias(true);
+ mTextPaint.setSubpixelText(true);
+
+ mAlignment = Alignment.ALIGN_CENTER;
+
+ mPaint = new Paint();
+ mPaint.setAntiAlias(true);
+
+ mInnerPaddingX = 0;
+ setText(text);
+ setTextSize(textSize);
+ setStyle(CaptionStyleCompat.DEFAULT);
+ }
+
+ @Override
+ public void setBackgroundColor(int color) {
+ mBackgroundColor = color;
+ forceUpdate(false);
+ }
+
+ /**
+ * Sets the text to be displayed by the view.
+ *
+ * @param text The text to display.
+ */
+ public void setText(CharSequence text) {
+ this.mText = text;
+ forceUpdate(true);
+ }
+
+ /**
+ * Sets the text size in pixels.
+ *
+ * @param size The text size in pixels.
+ */
+ public void setTextSize(float size) {
+ if (mTextPaint.getTextSize() != size) {
+ mTextPaint.setTextSize(size);
+ mInnerPaddingX = (int) (size * INNER_PADDING_RATIO + 0.5f);
+ mWhiteSpaceWidth -= mInnerPaddingX * 2;
+ forceUpdate(true);
+ }
+ }
+
+ /**
+ * Sets the text alignment.
+ *
+ * @param textAlignment The text alignment.
+ */
+ public void setTextAlignment(Alignment textAlignment) {
+ mAlignment = textAlignment;
+ }
+
+ /**
+ * Configures the view according to the given style.
+ *
+ * @param style A style for the view.
+ */
+ public void setStyle(CaptionStyleCompat style) {
+ mForegroundColor = style.foregroundColor;
+ mBackgroundColor = style.backgroundColor;
+ mEdgeType = style.edgeType;
+ mEdgeColor = style.edgeColor;
+ setTypeface(style.typeface);
+ super.setBackgroundColor(style.windowColor);
+ forceUpdate(true);
+ }
+
+ public void setPrefixSpaces(ArrayList<Integer> prefixSpaces) {
+ mPrefixSpaces = prefixSpaces;
+ }
+
+ public void setWhiteSpaceWidth(float whiteSpaceWidth) {
+ mWhiteSpaceWidth = whiteSpaceWidth;
+ }
+
+ private void setTypeface(Typeface typeface) {
+ if (mTextPaint.getTypeface() != typeface) {
+ mTextPaint.setTypeface(typeface);
+ forceUpdate(true);
+ }
+ }
+
+ private void forceUpdate(boolean needsLayout) {
+ if (needsLayout) {
+ mHasMeasurements = false;
+ requestLayout();
+ }
+ invalidate();
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ final int widthSpec = MeasureSpec.getSize(widthMeasureSpec);
+
+ if (computeMeasurements(widthSpec)) {
+ final StaticLayout layout = this.mLayout;
+ final int paddingX = getPaddingLeft() + getPaddingRight() + mInnerPaddingX * 2;
+ final int height = layout.getHeight() + getPaddingTop() + getPaddingBottom();
+ int width = 0;
+ int lineCount = layout.getLineCount();
+ for (int i = 0; i < lineCount; i++) {
+ width = Math.max((int) Math.ceil(layout.getLineWidth(i)), width);
+ }
+ width += paddingX;
+ setMeasuredDimension(width, height);
+ } else if (Util.SDK_INT >= 11) {
+ setTooSmallMeasureDimensionV11();
+ } else {
+ setMeasuredDimension(0, 0);
+ }
+ }
+
+ @TargetApi(11)
+ private void setTooSmallMeasureDimensionV11() {
+ setMeasuredDimension(MEASURED_STATE_TOO_SMALL, MEASURED_STATE_TOO_SMALL);
+ }
+
+ @Override
+ public void onLayout(boolean changed, int l, int t, int r, int b) {
+ final int width = r - l;
+ computeMeasurements(width);
+ }
+
+ private boolean computeMeasurements(int maxWidth) {
+ if (mHasMeasurements && maxWidth == mLastMeasuredWidth) {
+ return true;
+ }
+
+ // Account for padding.
+ final int paddingX = getPaddingLeft() + getPaddingRight() + mInnerPaddingX * 2;
+ maxWidth -= paddingX;
+ if (maxWidth <= 0) {
+ return false;
+ }
+
+ mHasMeasurements = true;
+ mLastMeasuredWidth = maxWidth;
+ mLayout =
+ new StaticLayout(
+ mText, mTextPaint, maxWidth, mAlignment, mSpacingMult, mSpacingAdd, true);
+ return true;
+ }
+
+ @Override
+ protected void onDraw(Canvas c) {
+ final StaticLayout layout = this.mLayout;
+ if (layout == null) {
+ return;
+ }
+
+ final int saveCount = c.save();
+ final int innerPaddingX = this.mInnerPaddingX;
+ c.translate(getPaddingLeft() + innerPaddingX, getPaddingTop());
+
+ final int lineCount = layout.getLineCount();
+ final Paint textPaint = this.mTextPaint;
+ final Paint paint = this.mPaint;
+ final RectF bounds = mLineBounds;
+
+ if (Color.alpha(mBackgroundColor) > 0) {
+ final float cornerRadius = this.mCornerRadius;
+ float previousBottom = layout.getLineTop(0);
+
+ paint.setColor(mBackgroundColor);
+ paint.setStyle(Style.FILL);
+
+ for (int i = 0; i < lineCount; i++) {
+ float spacesPadding = 0.0f;
+ if (i < mPrefixSpaces.size()) {
+ spacesPadding += mPrefixSpaces.get(i) * mWhiteSpaceWidth;
+ }
+ bounds.left = layout.getLineLeft(i) - innerPaddingX + spacesPadding;
+ bounds.right = layout.getLineRight(i) + innerPaddingX;
+ bounds.top = previousBottom;
+ bounds.bottom = layout.getLineBottom(i);
+ previousBottom = bounds.bottom;
+
+ c.drawRoundRect(bounds, cornerRadius, cornerRadius, paint);
+ }
+ }
+
+ if (mEdgeType == CaptionStyleCompat.EDGE_TYPE_OUTLINE) {
+ textPaint.setStrokeJoin(Join.ROUND);
+ textPaint.setStrokeWidth(mOutlineWidth);
+ textPaint.setColor(mEdgeColor);
+ textPaint.setStyle(Style.FILL_AND_STROKE);
+ layout.draw(c);
+ } else if (mEdgeType == CaptionStyleCompat.EDGE_TYPE_DROP_SHADOW) {
+ textPaint.setShadowLayer(mShadowRadius, mShadowOffset, mShadowOffset, mEdgeColor);
+ } else if (mEdgeType == CaptionStyleCompat.EDGE_TYPE_RAISED
+ || mEdgeType == CaptionStyleCompat.EDGE_TYPE_DEPRESSED) {
+ boolean raised = mEdgeType == CaptionStyleCompat.EDGE_TYPE_RAISED;
+ int colorUp = raised ? Color.WHITE : mEdgeColor;
+ int colorDown = raised ? mEdgeColor : Color.WHITE;
+ float offset = mShadowRadius / 2f;
+ textPaint.setColor(mForegroundColor);
+ textPaint.setStyle(Style.FILL);
+ textPaint.setShadowLayer(mShadowRadius, -offset, -offset, colorUp);
+ layout.draw(c);
+ textPaint.setShadowLayer(mShadowRadius, offset, offset, colorDown);
+ }
+
+ textPaint.setColor(mForegroundColor);
+ textPaint.setStyle(Style.FILL);
+ layout.draw(c);
+ textPaint.setShadowLayer(0, 0, 0, 0);
+ c.restoreToCount(saveCount);
+ }
+}
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..321e19da
--- /dev/null
+++ b/src/com/android/exoplayer2/ext/ffmpeg/FfmpegAudioDecoder.java
@@ -0,0 +1,126 @@
+/*
+ * 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.android.tv.common.SoftPreconditions;
+import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
+import com.google.android.exoplayer2.decoder.SimpleOutputBuffer;
+import com.google.android.exoplayer2.util.MimeTypes;
+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..a33d4020
--- /dev/null
+++ b/src/com/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java
@@ -0,0 +1,75 @@
+/*
+ * 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/TvSingletons.java b/src/com/android/tv/ApplicationSingletons.java
index 80c74576..f9eaf58e 100644
--- a/src/com/android/tv/TvSingletons.java
+++ b/src/com/android/tv/ApplicationSingletons.java
@@ -16,41 +16,27 @@
package com.android.tv;
-import android.content.Context;
import com.android.tv.analytics.Analytics;
import com.android.tv.analytics.Tracker;
-import com.android.tv.common.BaseApplication;
-import com.android.tv.common.BaseSingletons;
-import com.android.tv.common.experiments.ExperimentLoader;
+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.data.epg.EpgReader;
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.tuner.TunerInputController;
-import com.android.tv.util.SetupUtils;
+import com.android.tv.util.AccountHelper;
import com.android.tv.util.TvInputManagerHelper;
-import com.android.tv.util.account.AccountHelper;
-import javax.inject.Provider;
/** Interface with getters for application scoped singletons. */
-public interface TvSingletons extends BaseSingletons {
-
- /** Returns the @{@link TvSingletons} using the application context. */
- static TvSingletons getSingletons(Context context) {
- return (TvSingletons) BaseApplication.getSingletons(context);
- }
+public interface ApplicationSingletons {
Analytics getAnalytics();
- void handleInputCountChanged();
-
ChannelDataManager getChannelDataManager();
/**
@@ -71,6 +57,8 @@ public interface TvSingletons extends BaseSingletons {
DvrDataManager getDvrDataManager();
+ DvrStorageStatusManager getDvrStorageStatusManager();
+
DvrScheduleManager getDvrScheduleManager();
DvrManager getDvrManager();
@@ -83,23 +71,15 @@ public interface TvSingletons extends BaseSingletons {
Tracker getTracker();
+ TvInputManagerHelper getTvInputManagerHelper();
+
MainActivityWrapper getMainActivityWrapper();
AccountHelper getAccountHelper();
+ RemoteConfig getRemoteConfig();
+
boolean isRunningInMainProcess();
PerformanceMonitor getPerformanceMonitor();
-
- TvInputManagerHelper getTvInputManagerHelper();
-
- Provider<EpgReader> providesEpgReader();
-
- EpgFetcher getEpgFetcher();
-
- SetupUtils getSetupUtils();
-
- TunerInputController getTunerInputController();
-
- ExperimentLoader getExperimentLoader();
}
diff --git a/src/com/android/tv/AudioManagerHelper.java b/src/com/android/tv/AudioManagerHelper.java
index b0187617..f4bfdb9a 100644
--- a/src/com/android/tv/AudioManagerHelper.java
+++ b/src/com/android/tv/AudioManagerHelper.java
@@ -1,18 +1,3 @@
-/*
- * 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.app.Activity;
@@ -61,33 +46,20 @@ class AudioManagerHelper implements AudioManager.OnAudioFocusChangeListener {
if (mTvView.isPlaying()) {
switch (mAudioFocusStatus) {
case AudioManager.AUDIOFOCUS_GAIN:
- if (mTvView.isTimeShiftAvailable()) {
- mTvView.timeshiftPlay();
- } else {
- mTvView.setStreamVolume(AUDIO_MAX_VOLUME);
- }
+ mTvView.setStreamVolume(AUDIO_MAX_VOLUME);
break;
case AudioManager.AUDIOFOCUS_LOSS:
- if (TvFeatures.PICTURE_IN_PICTURE.isEnabled(mActivity)
+ if (Features.PICTURE_IN_PICTURE.isEnabled(mActivity)
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
&& mActivity.isInPictureInPictureMode()) {
mActivity.finish();
break;
}
- // fall through
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
- if (mTvView.isTimeShiftAvailable()) {
- mTvView.timeshiftPause();
- } else {
- mTvView.setStreamVolume(AUDIO_MIN_VOLUME);
- }
+ mTvView.setStreamVolume(AUDIO_MIN_VOLUME);
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
- if (mTvView.isTimeShiftAvailable()) {
- mTvView.timeshiftPause();
- } else {
- mTvView.setStreamVolume(AUDIO_DUCKING_VOLUME);
- }
+ mTvView.setStreamVolume(AUDIO_DUCKING_VOLUME);
break;
}
}
diff --git a/src/com/android/tv/Features.java b/src/com/android/tv/Features.java
new file mode 100644
index 00000000..3f8e56a8
--- /dev/null
+++ b/src/com/android/tv/Features.java
@@ -0,0 +1,193 @@
+/*
+ * 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;
+
+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.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.experiments.Experiments;
+import com.android.tv.util.LocationUtils;
+import com.android.tv.util.PermissionUtils;
+import com.android.tv.util.Utils;
+import java.util.Locale;
+
+/**
+ * List of {@link Feature} for the Live TV App.
+ *
+ * <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.
+ *
+ * <p>Do not turn this on until the splash screen asking existing users to opt-in is launched.
+ * See <a href="http://b/20228119">b/20228119</a>
+ */
+ public static final Feature ANALYTICS_OPT_IN = ENG_ONLY_FEATURE;
+
+ /**
+ * Analytics that include sensitive information such as channel or program identifiers.
+ *
+ * <p>See <a href="http://b/22062676">b/22062676</a>
+ */
+ public static final Feature ANALYTICS_V2 = AND(ON, ANALYTICS_OPT_IN);
+
+ 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) {
+
+ 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";
+ /** A flag which indicates that LC app is unhidden even when there is no input. */
+ public static final Feature UNHIDE =
+ OR(
+ new GServiceFeature(GSERVICE_KEY_UNHIDE, false),
+ new Feature() {
+ @Override
+ public boolean isEnabled(Context context) {
+ // If LC app runs as non-system app, we unhide the app.
+ return !PermissionUtils.hasAccessAllEpg(context);
+ }
+ });
+
+ public static final Feature PICTURE_IN_PICTURE =
+ new Feature() {
+ private Boolean 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;
+ }
+ };
+
+ /** 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;
+
+ /** Use input blacklist to disable partner's tuner input. */
+ 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);
+
+ private Features() {}
+}
diff --git a/src/com/android/tv/InputSessionManager.java b/src/com/android/tv/InputSessionManager.java
index 416dbb68..709ed4a4 100644
--- a/src/com/android/tv/InputSessionManager.java
+++ b/src/com/android/tv/InputSessionManager.java
@@ -76,7 +76,7 @@ public class InputSessionManager {
public InputSessionManager(Context context) {
mContext = context.getApplicationContext();
- mInputManager = TvSingletons.getSingletons(context).getTvInputManagerHelper();
+ mInputManager = TvApplication.getSingletons(context).getTvInputManagerHelper();
}
/**
diff --git a/src/com/android/tv/LauncherActivity.java b/src/com/android/tv/LauncherActivity.java
index 3aca35a4..545d49b1 100644
--- a/src/com/android/tv/LauncherActivity.java
+++ b/src/com/android/tv/LauncherActivity.java
@@ -27,16 +27,15 @@ import android.util.Log;
* An activity to launch a new activity.
*
* <p>In the case when {@link MainActivity} starts a new activity using {@link
- * Activity#startActivity} or {@link Activity#startActivityForResult}, Live TV app is
- * terminated if the new activity crashes. That's because the {@link android.app.ActivityManager}
- * terminates the activity which is just below the crashed activity in the activity stack. To avoid
- * this, we need to locate an additional activity between these activities in the activity stack.
+ * Activity#startActivity} or {@link Activity#startActivityForResult}, Live TV app is terminated if
+ * the new activity crashes. That's because the {@link android.app.ActivityManager} terminates the
+ * activity which is just below the crashed activity in the activity stack. To avoid this, we need
+ * to locate an additional activity between these activities in the activity stack.
*/
public class LauncherActivity extends Activity {
private static final String TAG = "LauncherActivity";
- public static final String ERROR_MESSAGE =
- "com.android.tv.LauncherActivity.ErrorMessage";
+ public static final String ERROR_MESSAGE = "com.android.tv.LauncherActivity.ErrorMessage";
private static final int REQUEST_CODE_DEFAULT = 0;
private static final int REQUEST_START_ACTIVITY = 100;
@@ -53,6 +52,22 @@ public class LauncherActivity extends Activity {
createIntent(baseActivity, intentToLaunch, false), REQUEST_CODE_DEFAULT);
}
+ /**
+ * Starts an activity by calling {@link Activity#startActivityForResult}.
+ *
+ * <p>Note: {@code requestCode} should not be 0. The value is reserved for internal use.
+ */
+ public static void startActivityForResultSafe(
+ Activity baseActivity, Intent intentToLaunch, int requestCode) {
+ if (requestCode == REQUEST_CODE_DEFAULT) {
+ throw new IllegalArgumentException("requestCode should not be 0.");
+ }
+ // To avoid the app termination when the new activity crashes, LauncherActivity should be
+ // started by calling startActivityForResult().
+ baseActivity.startActivityForResult(
+ createIntent(baseActivity, intentToLaunch, true), requestCode);
+ }
+
private static Intent createIntent(
Context context, Intent intentToLaunch, boolean requestResult) {
Intent intent = new Intent(context, LauncherActivity.class);
diff --git a/src/com/android/tv/MainActivity.java b/src/com/android/tv/MainActivity.java
index 22ee9321..427d562a 100644
--- a/src/com/android/tv/MainActivity.java
+++ b/src/com/android/tv/MainActivity.java
@@ -17,7 +17,6 @@
package com.android.tv;
import android.app.Activity;
-import android.app.PendingIntent;
import android.content.ActivityNotFoundException;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
@@ -67,18 +66,13 @@ import com.android.tv.analytics.SendChannelStatusRunnable;
import com.android.tv.analytics.SendConfigInfoRunnable;
import com.android.tv.analytics.Tracker;
import com.android.tv.common.BuildConfig;
-import com.android.tv.common.CommonPreferences;
import com.android.tv.common.MemoryManageable;
import com.android.tv.common.SoftPreconditions;
+import com.android.tv.common.TvCommonUtils;
import com.android.tv.common.TvContentRatingCache;
import com.android.tv.common.WeakHandler;
import com.android.tv.common.feature.CommonFeatures;
import com.android.tv.common.ui.setup.OnActionClickListener;
-import com.android.tv.common.util.CommonUtils;
-import com.android.tv.common.util.Debug;
-import com.android.tv.common.util.DurationTimer;
-import com.android.tv.common.util.PermissionUtils;
-import com.android.tv.common.util.SystemProperties;
import com.android.tv.data.Channel;
import com.android.tv.data.ChannelDataManager;
import com.android.tv.data.OnCurrentProgramUpdatedListener;
@@ -86,6 +80,7 @@ import com.android.tv.data.Program;
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;
@@ -95,17 +90,21 @@ 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.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.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.ChannelBannerView;
import com.android.tv.ui.InputBannerView;
import com.android.tv.ui.KeypadChannelSwitchView;
@@ -125,16 +124,19 @@ 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.CaptionSettings;
+import com.android.tv.util.Debug;
+import com.android.tv.util.DurationTimer;
import com.android.tv.util.ImageCache;
import com.android.tv.util.OnboardingUtils;
+import com.android.tv.util.PermissionUtils;
import com.android.tv.util.RecurringRunner;
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.TvTrackInfoUtils;
import com.android.tv.util.Utils;
import com.android.tv.util.ViewCache;
-import com.android.tv.util.account.AccountHelper;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayDeque;
@@ -146,7 +148,6 @@ import java.util.Set;
import java.util.concurrent.TimeUnit;
/** The main activity for the Live TV app. */
-@SuppressWarnings("TryWithResources") // TODO(b/62143348): remove when error prone check fixed
public class MainActivity extends Activity implements OnActionClickListener, OnPinCheckedListener {
private static final String TAG = "MainActivity";
private static final boolean DEBUG = false;
@@ -175,7 +176,6 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
// Tracker screen names.
public static final String SCREEN_NAME = "Main";
- private static final String SCREEN_PIP = "PIP";
private static final String SCREEN_BEHIND_NAME = "Behind";
private static final float REFRESH_RATE_EPSILON = 0.01f;
@@ -205,7 +205,6 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
}
private static final int REQUEST_CODE_START_SETUP_ACTIVITY = 1;
- private static final int REQUEST_CODE_NOW_PLAYING = 2;
private static final String KEY_INIT_CHANNEL_ID = "com.android.tv.init_channel_id";
@@ -240,7 +239,6 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
private final DurationTimer mTuneDurationTimer = new DurationTimer();
private DvrManager mDvrManager;
private ConflictChecker mDvrConflictChecker;
- private SetupUtils mSetupUtils;
private View mContentView;
private TunableTvView mTvView;
@@ -273,7 +271,6 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
private boolean mOtherActivityLaunched;
private PerformanceMonitor mPerformanceMonitor;
- private boolean mIsInPIPMode;
private boolean mIsFilmModeSet;
private float mDefaultRefreshRate;
@@ -310,9 +307,10 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
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.
+ // 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);
@@ -321,9 +319,10 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
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.
+ // is
+ // played under launcher with requestVisibleBehind(true), onResume
+ // will
+ // not be called. In this case, we need to resume TvView explicitly.
resumeTvIfNeeded();
}
break;
@@ -340,7 +339,6 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
tune(true);
}
break;
- default: // fall out
}
}
};
@@ -369,7 +367,7 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
public void onLoadFinished() {
Debug.getTimer(Debug.TAG_START_UP_TIMER)
.log("MainActivity.mChannelTunerListener.onLoadFinished");
- mSetupUtils.markNewChannelsBrowsable();
+ SetupUtils.getInstance(MainActivity.this).markNewChannelsBrowsable();
if (mActivityResumed) {
resumeTvIfNeeded();
}
@@ -407,16 +405,13 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
new TvInputCallback() {
@Override
public void onInputAdded(String inputId) {
- if (TvFeatures.TUNER.isEnabled(MainActivity.this)
+ if (Features.TUNER.isEnabled(MainActivity.this)
&& mTunerInputId.equals(inputId)
- && CommonPreferences.shouldShowSetupActivity(MainActivity.this)) {
- Intent intent =
- TvSingletons
- .getSingletons(MainActivity.this)
- .getTunerSetupIntent(MainActivity.this);
+ && TunerPreferences.shouldShowSetupActivity(MainActivity.this)) {
+ Intent intent = TunerSetupActivity.createSetupActivity(MainActivity.this);
startActivity(intent);
- CommonPreferences.setShouldShowSetupActivity(MainActivity.this, false);
- mSetupUtils.markAsKnownInput(mTunerInputId);
+ TunerPreferences.setShouldShowSetupActivity(MainActivity.this, false);
+ SetupUtils.getInstance(MainActivity.this).markAsKnownInput(mTunerInputId);
}
}
};
@@ -432,9 +427,7 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
@Override
protected void onCreate(Bundle savedInstanceState) {
- TvSingletons tvSingletons = TvSingletons.getSingletons(this);
- mPerformanceMonitor = tvSingletons.getPerformanceMonitor();
- TimerEvent timer = mPerformanceMonitor.startTimer();
+ TimerEvent timer = StubPerformanceMonitor.startBootstrapTimer();
DurationTimer startUpDebugTimer = Debug.getTimer(Debug.TAG_START_UP_TIMER);
if (!startUpDebugTimer.isStarted()
|| startUpDebugTimer.getDuration() > START_UP_TIMER_RESET_THRESHOLD_MS) {
@@ -443,18 +436,16 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
startUpDebugTimer.start();
}
startUpDebugTimer.log("MainActivity.onCreate");
- if (DEBUG) {
- Log.d(TAG, "onCreate()");
- }
- Starter.start(this);
+ if (DEBUG) Log.d(TAG, "onCreate()");
+ TvApplication.setCurrentRunningProcess(this, true);
super.onCreate(savedInstanceState);
- if (!tvSingletons.getTvInputManagerHelper().hasTvInputManager()) {
+ ApplicationSingletons applicationSingletons = TvApplication.getSingletons(this);
+ if (!applicationSingletons.getTvInputManagerHelper().hasTvInputManager()) {
Log.wtf(TAG, "Stopping because device does not have a TvInputManager");
finishAndRemoveTask();
return;
}
- mPerformanceMonitor = tvSingletons.getPerformanceMonitor();
- mSetupUtils = tvSingletons.getSetupUtils();
+ mPerformanceMonitor = applicationSingletons.getPerformanceMonitor();
TvApplication tvApplication = (TvApplication) getApplication();
mChannelDataManager = tvApplication.getChannelDataManager();
@@ -469,7 +460,7 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
if ((OnboardingUtils.isFirstRunWithCurrentVersion(this)
|| channelLoadedAndNoChannelAvailable)
&& !tuneToPassthroughInput
- && !CommonUtils.isRunningInTest()) {
+ && !TvCommonUtils.isRunningInTest()) {
startOnboardingActivity();
return;
}
@@ -504,9 +495,7 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
});
long channelId = Utils.getLastWatchedChannelId(this);
String inputId = Utils.getLastWatchedTunerInputId(this);
- if (!isPassthroughInput
- && inputId != null
- && channelId != Channel.INVALID_ID) {
+ if (!isPassthroughInput && inputId != null && channelId != Channel.INVALID_ID) {
mTvView.warmUpInput(inputId, TvContract.buildChannelUri(channelId));
}
@@ -515,10 +504,10 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
Toast.makeText(this, "Using Strict Mode for eng builds", Toast.LENGTH_SHORT).show();
}
mTracker = tvApplication.getTracker();
- if (TvFeatures.TUNER.isEnabled(this)) {
+ if (Features.TUNER.isEnabled(this)) {
mTvInputManagerHelper.addCallback(mTvInputCallback);
}
- mTunerInputId = tvSingletons.getEmbeddedTunerInputId();
+ mTunerInputId = TunerTvInputService.getInputId(this);
mProgramDataManager.addOnCurrentProgramUpdatedListener(
Channel.INVALID_ID, mOnCurrentProgramUpdatedListener);
mProgramDataManager.setPrefetchEnabled(true);
@@ -646,10 +635,7 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
mSearchFragment);
mAudioManagerHelper = new AudioManagerHelper(this, mTvView);
- Intent nowPlayingIntent = new Intent(this, MainActivity.class);
- PendingIntent pendingIntent =
- PendingIntent.getActivity(this, REQUEST_CODE_NOW_PLAYING, nowPlayingIntent, 0);
- mMediaSessionWrapper = new MediaSessionWrapper(this, pendingIntent);
+ mMediaSessionWrapper = new MediaSessionWrapper(this);
mTvViewUiManager.restoreDisplayMode(false);
if (!handleIntent(getIntent())) {
@@ -673,7 +659,7 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
// To avoid not updating Rating systems when changing language.
mTvInputManagerHelper.getContentRatingsManager().update();
if (CommonFeatures.DVR.isEnabled(this)
- && TvFeatures.SHOW_UPCOMING_CONFLICT_DIALOG.isEnabled(this)) {
+ && Features.SHOW_UPCOMING_CONFLICT_DIALOG.isEnabled(this)) {
mDvrConflictChecker = new ConflictChecker(this);
}
initForTest();
@@ -752,9 +738,7 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
@Override
protected void onNewIntent(Intent intent) {
- if (DEBUG) {
- Log.d(TAG, "onNewIntent(): " + intent);
- }
+ if (DEBUG) Log.d(TAG, "onNewIntent(): " + intent);
if (mOverlayManager == null) {
// It's called before onCreate. The intent will be handled at onCreate. b/30725058
return;
@@ -770,9 +754,7 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
@Override
protected void onStart() {
TimerEvent timer = mPerformanceMonitor.startTimer();
- if (DEBUG) {
- Log.d(TAG, "onStart()");
- }
+ if (DEBUG) Log.d(TAG, "onStart()");
super.onStart();
mScreenOffIntentReceived = false;
mActivityStarted = true;
@@ -787,9 +769,9 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
notificationIntent.setAction(NotificationService.ACTION_SHOW_RECOMMENDATION);
startService(notificationIntent);
}
- TvSingletons singletons = TvSingletons.getSingletons(this);
- singletons.getTunerInputController().executeNetworkTunerDiscoveryAsyncTask(this);
- singletons.getEpgFetcher().fetchImmediatelyIfNeeded();
+ TunerInputController.executeNetworkTunerDiscoveryAsyncTask(this);
+
+ EpgFetcher.getInstance(this).fetchImmediatelyIfNeeded();
mPerformanceMonitor.stopTimer(timer, EventNames.MAIN_ACTIVITY_ONSTART);
}
@@ -799,7 +781,6 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
Debug.getTimer(Debug.TAG_START_UP_TIMER).log("MainActivity.onResume start");
if (DEBUG) Log.d(TAG, "onResume()");
super.onResume();
- mIsInPIPMode = false;
if (!PermissionUtils.hasAccessAllEpg(this)
&& checkSelfPermission(PERMISSION_READ_TV_LISTINGS)
!= PackageManager.PERMISSION_GRANTED) {
@@ -837,7 +818,7 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
}
if (mChannelTuner.areAllChannelsLoaded()) {
- mSetupUtils.markNewChannelsBrowsable();
+ SetupUtils.getInstance(this).markNewChannelsBrowsable();
resumeTvIfNeeded();
}
mOverlayManager.showMenuWithTimeShiftPauseIfNeeded();
@@ -849,11 +830,13 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
mInputToSetUp = null;
} else if (mShowProgramGuide) {
mShowProgramGuide = false;
- // This will delay the start of the animation until after the Live Channel app is
- // shown. Without this the animation is completed before it is actually visible on
- // the screen.
mHandler.post(
new Runnable() {
+ // This will delay the start of the animation until after the Live Channel
+ // app is
+ // shown. Without this the animation is completed before it is actually
+ // visible on
+ // the screen.
@Override
public void run() {
mOverlayManager.showProgramGuide();
@@ -861,12 +844,16 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
});
} else if (mShowSelectInputView) {
mShowSelectInputView = false;
- // mShowSelectInputView is true when the activity is started/resumed because the
- // TV_INPUT button was pressed in a different app. This will delay the start of
- // the animation until after the Live Channel app is shown. Without this the
- // animation is completed before it is actually visible on the screen.
mHandler.post(
new Runnable() {
+ // mShowSelectInputView is true when the activity is started/resumed because
+ // the
+ // TV_INPUT button was pressed in a different app.
+ // This will delay the start of the animation until after the Live Channel
+ // app is
+ // shown. Without this the animation is completed before it is actually
+ // visible on
+ // the screen.
@Override
public void run() {
mOverlayManager.showSelectInputView();
@@ -894,17 +881,12 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
mShowLockedChannelsTemporarily = false;
mShouldTuneToTunerChannel = false;
if (!mVisibleBehind) {
- if (mIsInPIPMode) {
- mTracker.sendScreenView(SCREEN_PIP);
- } else {
- mTracker.sendScreenView("");
- mAudioManagerHelper.abandonAudioFocus();
- mMediaSessionWrapper.setPlaybackState(false);
- }
+ mAudioManagerHelper.abandonAudioFocus();
+ mMediaSessionWrapper.setPlaybackState(false);
+ mTracker.sendScreenView("");
} else {
mTracker.sendScreenView(SCREEN_BEHIND_NAME);
}
- TvSingletons.getSingletons(this).getExperimentLoader().asyncRefreshExperiments(this);
super.onPause();
}
@@ -942,11 +924,10 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
mOverlayManager
.getSideFragmentManager()
.show(new ParentalControlsFragment(), false);
- // fall through.
+ // Pass through.
case PinDialogFragment.PIN_DIALOG_TYPE_NEW_PIN:
mOverlayManager.getSideFragmentManager().showSidePanel(true);
break;
- default: // fall out
}
} else if (type == PinDialogFragment.PIN_DIALOG_TYPE_ENTER_PIN) {
mOverlayManager.getSideFragmentManager().hideAll(false);
@@ -1094,7 +1075,7 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
* @param calledByPopup If true, startSetupActivity is invoked from the setup fragment.
*/
public void startSetupActivity(TvInputInfo input, boolean calledByPopup) {
- Intent intent = CommonUtils.createSetupIntent(input);
+ Intent intent = TvCommonUtils.createSetupIntent(input);
if (intent == null) {
Toast.makeText(this, R.string.msg_no_setup_activity, Toast.LENGTH_SHORT).show();
return;
@@ -1238,6 +1219,15 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
LauncherActivity.startActivitySafe(this, intent);
}
+ /**
+ * Call {@link Activity#startActivityForResult} in a safe way.
+ *
+ * @see LauncherActivity
+ */
+ private void startActivityForResultSafe(Intent intent, int requestCode) {
+ LauncherActivity.startActivityForResultSafe(this, intent, requestCode);
+ }
+
/** Show settings fragment. */
public void showSettingsFragment() {
if (!mChannelTuner.areAllChannelsLoaded()) {
@@ -1330,39 +1320,31 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
@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(true);
- }
+ 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_NOW_PLAYING:
- // nothing needs to be done. onResume will restore everything.
- break;
- default:
- // do nothing
+ if (mTunePending) {
+ tune(true);
+ }
+ } else {
+ mInputIdUnderSetup = null;
+ }
+ if (!mIsSetupActivityCalledByPopup) {
+ mOverlayManager.getSideFragmentManager().showSidePanel(false);
+ }
}
if (data != null) {
String errorMessage = data.getStringExtra(LauncherActivity.ERROR_MESSAGE);
@@ -1554,7 +1536,7 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
mAudioManagerHelper.abandonAudioFocus();
mMediaSessionWrapper.setPlaybackState(false);
}
- TvSingletons.getSingletons(this)
+ TvApplication.getSingletons(this)
.getMainActivityWrapper()
.notifyCurrentChannelChange(this, null);
mChannelTuner.resetCurrentChannel();
@@ -1601,8 +1583,8 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
finish();
return;
}
-
- if (mSetupUtils.isFirstTune()) {
+ SetupUtils setupUtils = SetupUtils.getInstance(this);
+ if (setupUtils.isFirstTune()) {
if (!mChannelTuner.areAllChannelsLoaded()) {
// tune() will be called, once all channels are loaded.
stopTv("tune()", false);
@@ -1631,9 +1613,9 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
}
return;
}
- if (!CommonUtils.isRunningInTest()
+ if (!TvCommonUtils.isRunningInTest()
&& mShowNewSourcesFragment
- && mSetupUtils.hasUnrecognizedInput(mTvInputManagerHelper)) {
+ && setupUtils.hasUnrecognizedInput(mTvInputManagerHelper)) {
// Show new channel sources fragment.
runAfterAttachedToWindow(
new Runnable() {
@@ -1649,7 +1631,7 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
}
});
}
- mSetupUtils.onTuned();
+ setupUtils.onTuned();
if (mTuneParams != null) {
Long initChannelId = mTuneParams.getLong(KEY_INIT_CHANNEL_ID);
if (initChannelId == channel.getId()) {
@@ -1689,7 +1671,7 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
addToRecentChannels(channel.getId());
}
Utils.setLastWatchedChannel(this, channel);
- TvSingletons.getSingletons(this)
+ TvApplication.getSingletons(this)
.getMainActivityWrapper()
.notifyCurrentChannelChange(this, channel);
}
@@ -1989,7 +1971,7 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
}
if (mTvInputManagerHelper != null) {
mTvInputManagerHelper.clearTvInputLabels();
- if (TvFeatures.TUNER.isEnabled(this)) {
+ if (Features.TUNER.isEnabled(this)) {
mTvInputManagerHelper.removeCallback(mTvInputCallback);
}
}
@@ -2010,7 +1992,7 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
return false;
case KEY_EVENT_HANDLER_RESULT_PASSTHROUGH:
default:
- // fall through
+ // pass through
}
if (mSearchFragment.isVisible()) {
return super.onKeyDown(keyCode, event);
@@ -2048,7 +2030,6 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
mTracker.sendChannelDown();
}
return true;
- default: // fall out
}
}
return super.onKeyDown(keyCode, event);
@@ -2089,7 +2070,7 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
return false;
case KEY_EVENT_HANDLER_RESULT_PASSTHROUGH:
default:
- // fall through
+ // pass through
}
if (mSearchFragment.isVisible()) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
@@ -2119,7 +2100,6 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
case KeyEvent.KEYCODE_MENU:
showSettingsFragment();
return true;
- default: // fall out
}
} else {
if (KeypadChannelSwitchView.isChannelNumberKey(keyCode)) {
@@ -2187,74 +2167,82 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
if (!SystemProperties.USE_DEBUG_KEYS.getValue()) {
break;
}
- // fall through.
+ // Pass through.
case KeyEvent.KEYCODE_CAPTIONS:
- mOverlayManager.getSideFragmentManager().show(new ClosedCaptionFragment());
- return true;
+ {
+ mOverlayManager.getSideFragmentManager().show(new ClosedCaptionFragment());
+ return true;
+ }
case KeyEvent.KEYCODE_A:
if (!SystemProperties.USE_DEBUG_KEYS.getValue()) {
break;
}
- // fall through.
+ // Pass through.
case KeyEvent.KEYCODE_MEDIA_AUDIO_TRACK:
- mOverlayManager.getSideFragmentManager().show(new MultiAudioFragment());
- return true;
+ {
+ mOverlayManager.getSideFragmentManager().show(new MultiAudioFragment());
+ return true;
+ }
case KeyEvent.KEYCODE_INFO:
- mOverlayManager.showBanner();
- return true;
+ {
+ mOverlayManager.showBanner();
+ return true;
+ }
case KeyEvent.KEYCODE_MEDIA_RECORD:
case KeyEvent.KEYCODE_V:
- Channel currentChannel = getCurrentChannel();
- if (currentChannel != null && mDvrManager != null) {
- boolean isRecording =
- mDvrManager.getCurrentRecording(currentChannel.getId()) != null;
- if (!isRecording) {
- if (!mDvrManager.isChannelRecordable(currentChannel)) {
- Toast.makeText(
- this,
- R.string.dvr_msg_cannot_record_program,
- Toast.LENGTH_SHORT)
- .show();
+ {
+ Channel currentChannel = getCurrentChannel();
+ if (currentChannel != null && mDvrManager != null) {
+ boolean isRecording =
+ mDvrManager.getCurrentRecording(currentChannel.getId()) != null;
+ if (!isRecording) {
+ if (!mDvrManager.isChannelRecordable(currentChannel)) {
+ Toast.makeText(
+ this,
+ R.string.dvr_msg_cannot_record_program,
+ Toast.LENGTH_SHORT)
+ .show();
+ } else {
+ Program program =
+ mProgramDataManager.getCurrentProgram(
+ currentChannel.getId());
+ DvrUiHelper.checkStorageStatusAndShowErrorMessage(
+ this,
+ currentChannel.getInputId(),
+ new Runnable() {
+ @Override
+ public void run() {
+ DvrUiHelper.requestRecordingCurrentProgram(
+ MainActivity.this,
+ currentChannel,
+ program,
+ false);
+ }
+ });
+ }
} else {
- Program program =
- mProgramDataManager.getCurrentProgram(
- currentChannel.getId());
- DvrUiHelper.checkStorageStatusAndShowErrorMessage(
+ DvrUiHelper.showStopRecordingDialog(
this,
- currentChannel.getInputId(),
- new Runnable() {
+ currentChannel.getId(),
+ DvrStopRecordingFragment.REASON_USER_STOP,
+ new HalfSizedDialogFragment.OnActionClickListener() {
@Override
- public void run() {
- DvrUiHelper.requestRecordingCurrentProgram(
- MainActivity.this,
- currentChannel,
- program,
- false);
+ public void onActionClick(long actionId) {
+ if (actionId
+ == DvrStopRecordingFragment.ACTION_STOP) {
+ ScheduledRecording currentRecording =
+ mDvrManager.getCurrentRecording(
+ currentChannel.getId());
+ if (currentRecording != null) {
+ mDvrManager.stopRecording(currentRecording);
+ }
+ }
}
});
}
- } else {
- DvrUiHelper.showStopRecordingDialog(
- this,
- currentChannel.getId(),
- DvrStopRecordingFragment.REASON_USER_STOP,
- new HalfSizedDialogFragment.OnActionClickListener() {
- @Override
- public void onActionClick(long actionId) {
- if (actionId == DvrStopRecordingFragment.ACTION_STOP) {
- ScheduledRecording currentRecording =
- mDvrManager.getCurrentRecording(
- currentChannel.getId());
- if (currentRecording != null) {
- mDvrManager.stopRecording(currentRecording);
- }
- }
- }
- });
}
+ return true;
}
- return true;
- default: // fall out
}
}
if (keyCode == KeyEvent.KEYCODE_WINDOW) {
@@ -2292,7 +2280,6 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
case KeyEvent.KEYCODE_D:
mOverlayManager.getSideFragmentManager().show(new DeveloperOptionFragment());
return true;
- default: // fall out
}
}
return super.onKeyUp(keyCode, event);
@@ -2327,19 +2314,14 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
// We need to hide overlay first, before moving the activity to PIP. If not, UI will
// be shown during PIP stack resizing, because UI and its animation is stuck during
// PIP resizing.
- mIsInPIPMode = true;
- if (mOverlayManager.isOverlayOpened()) {
- mOverlayManager.hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_WITHOUT_ANIMATION);
- mHandler.post(
- new Runnable() {
- @Override
- public void run() {
- MainActivity.super.enterPictureInPictureMode();
- }
- });
- } else {
- MainActivity.super.enterPictureInPictureMode();
- }
+ mOverlayManager.hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_WITHOUT_ANIMATION);
+ mHandler.post(
+ new Runnable() {
+ @Override
+ public void run() {
+ MainActivity.super.enterPictureInPictureMode();
+ }
+ });
}
@Override
@@ -2421,7 +2403,7 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
} else if (channel.equals(mTvView.getCurrentChannel())) {
mOverlayManager.updateChannelBannerAndShowIfNeeded(
TvOverlayManager.UPDATE_CHANNEL_BANNER_REASON_TUNE);
- } else if (channel.equals(mChannelTuner.getCurrentChannel())) {
+ } else if (channel == mChannelTuner.getCurrentChannel()) {
// Channel banner is already updated in moveToAdjacentChannel
tune(false);
} else if (mChannelTuner.moveToChannel(channel)) {
@@ -2551,8 +2533,7 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
private void updateAvailabilityToast() {
if (mTvView.isVideoAvailable()
- || !Objects.equals(
- mTvView.getCurrentChannel(), mChannelTuner.getCurrentChannel())) {
+ || mTvView.getCurrentChannel() != mChannelTuner.getCurrentChannel()) {
return;
}
@@ -2616,7 +2597,7 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
// Initialize TV app for test. The setup process should be finished before the Live TV app is
// started. We only enable all the channels here.
private void initForTest() {
- if (!CommonUtils.isRunningInTest()) {
+ if (!TvCommonUtils.isRunningInTest()) {
return;
}
@@ -2688,7 +2669,6 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
sendMessageDelayed(Message.obtain(msg), getDelay(startTime));
mainActivity.moveToAdjacentChannel(true, true);
break;
- default: // fall out
}
}
@@ -2735,7 +2715,7 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
if (info.isVideoAvailable() && mTuneDurationTimer.isRunning()) {
mTracker.sendChannelTuneTime(info.getCurrentChannel(), mTuneDurationTimer.reset());
}
- if (info.isVideoOrAudioAvailable() && mChannel.equals(getCurrentChannel())) {
+ if (info.isVideoOrAudioAvailable() && mChannel == getCurrentChannel()) {
mOverlayManager.updateChannelBannerAndShowIfNeeded(
TvOverlayManager.UPDATE_CHANNEL_BANNER_REASON_UPDATE_STREAM_INFO);
}
diff --git a/src/com/android/tv/MediaSessionWrapper.java b/src/com/android/tv/MediaSessionWrapper.java
index b3472ba5..2cc898c3 100644
--- a/src/com/android/tv/MediaSessionWrapper.java
+++ b/src/com/android/tv/MediaSessionWrapper.java
@@ -16,7 +16,6 @@
package com.android.tv;
-import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
@@ -29,7 +28,6 @@ import android.media.tv.TvInputInfo;
import android.os.AsyncTask;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
-import android.support.annotation.VisibleForTesting;
import android.text.TextUtils;
import com.android.tv.data.Channel;
import com.android.tv.data.Program;
@@ -42,16 +40,14 @@ import com.android.tv.util.Utils;
*/
class MediaSessionWrapper {
private static final String MEDIA_SESSION_TAG = "com.android.tv.mediasession";
-
- private static final PlaybackState MEDIA_SESSION_STATE_PLAYING =
+ private static PlaybackState MEDIA_SESSION_STATE_PLAYING =
new PlaybackState.Builder()
.setState(
PlaybackState.STATE_PLAYING,
PlaybackState.PLAYBACK_POSITION_UNKNOWN,
1.0f)
.build();
-
- private static final PlaybackState MEDIA_SESSION_STATE_STOPPED =
+ private static PlaybackState MEDIA_SESSION_STATE_STOPPED =
new PlaybackState.Builder()
.setState(
PlaybackState.STATE_STOPPED,
@@ -64,7 +60,7 @@ class MediaSessionWrapper {
private int mNowPlayingCardWidth;
private int mNowPlayingCardHeight;
- MediaSessionWrapper(Context context, PendingIntent pendingIntent) {
+ MediaSessionWrapper(Context context) {
mContext = context;
mMediaSession = new MediaSession(context, MEDIA_SESSION_TAG);
mMediaSession.setCallback(
@@ -78,7 +74,6 @@ class MediaSessionWrapper {
mMediaSession.setFlags(
MediaSession.FLAG_HANDLES_MEDIA_BUTTONS
| MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS);
- mMediaSession.setSessionActivity(pendingIntent);
mNowPlayingCardWidth =
mContext.getResources().getDimensionPixelSize(R.dimen.notif_card_img_max_width);
mNowPlayingCardHeight =
@@ -156,7 +151,7 @@ class MediaSessionWrapper {
private String getChannelName(Channel channel) {
if (channel.isPassthrough()) {
TvInputInfo input =
- TvSingletons.getSingletons(mContext)
+ TvApplication.getSingletons(mContext)
.getTvInputManagerHelper()
.getTvInputInfo(channel.getInputId());
return Utils.loadLabel(mContext, input);
@@ -218,11 +213,6 @@ class MediaSessionWrapper {
}.execute();
}
- @VisibleForTesting
- MediaSession getMediaSession() {
- return mMediaSession;
- }
-
private static class ProgramPosterArtCallback
extends ImageLoader.ImageLoaderCallback<MediaSessionWrapper> {
private final Channel mChannel;
diff --git a/src/com/android/tv/SetupPassthroughActivity.java b/src/com/android/tv/SetupPassthroughActivity.java
index 40d38118..d1158682 100644
--- a/src/com/android/tv/SetupPassthroughActivity.java
+++ b/src/com/android/tv/SetupPassthroughActivity.java
@@ -26,14 +26,12 @@ import android.os.Handler;
import android.os.Looper;
import android.support.annotation.MainThread;
import android.util.Log;
-import com.android.tv.common.CommonConstants;
import com.android.tv.common.SoftPreconditions;
-import com.android.tv.common.experiments.Experiments;
+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.data.epg.EpgInputWhiteList;
-
+import com.android.tv.experiments.Experiments;
import com.android.tv.util.SetupUtils;
import com.android.tv.util.TvInputManagerHelper;
import com.android.tv.util.Utils;
@@ -55,47 +53,35 @@ public class SetupPassthroughActivity extends Activity {
private TvInputInfo mTvInputInfo;
private Intent mActivityAfterCompletion;
private boolean mEpgFetcherDuringScan;
- private EpgInputWhiteList mEpgInputWhiteList;
@Override
public void onCreate(Bundle savedInstanceState) {
if (DEBUG) Log.d(TAG, "onCreate");
super.onCreate(savedInstanceState);
- TvSingletons tvSingletons = TvSingletons.getSingletons(this);
- TvInputManagerHelper inputManager = tvSingletons.getTvInputManagerHelper();
+ ApplicationSingletons appSingletons = TvApplication.getSingletons(this);
+ TvInputManagerHelper inputManager = appSingletons.getTvInputManagerHelper();
Intent intent = getIntent();
- String inputId = intent.getStringExtra(CommonConstants.EXTRA_INPUT_ID);
+ String inputId = intent.getStringExtra(TvCommonConstants.EXTRA_INPUT_ID);
mTvInputInfo = inputManager.getTvInputInfo(inputId);
- mEpgInputWhiteList = new EpgInputWhiteList(tvSingletons.getRemoteConfig());
mActivityAfterCompletion =
- intent.getParcelableExtra(CommonConstants.EXTRA_ACTIVITY_AFTER_COMPLETION);
+ intent.getParcelableExtra(TvCommonConstants.EXTRA_ACTIVITY_AFTER_COMPLETION);
boolean needToFetchEpg =
- mTvInputInfo != null
- && Utils.isInternalTvInput(this, mTvInputInfo.getId())
- && Experiments.CLOUD_EPG.get();
+ 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;
}
if (savedInstanceState == null) {
- SoftPreconditions.checkArgument(
- CommonConstants.INTENT_ACTION_INPUT_SETUP.equals(intent.getAction()),
- TAG,
- "Unsupported action %s",
- intent.getAction());
+ 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;
}
- if (intent.getExtras() == null) {
- Log.w(TAG, "There is no extra info in the intent");
- finish();
- return;
- }
Intent setupIntent =
- intent.getExtras().getParcelable(CommonConstants.EXTRA_SETUP_INTENT);
+ 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.");
@@ -107,7 +93,7 @@ public class SetupPassthroughActivity extends Activity {
// If EXTRA_SETUP_INTENT is not removed, an infinite recursion happens during
// setupIntent.putExtras(intent.getExtras()).
Bundle extras = intent.getExtras();
- extras.remove(CommonConstants.EXTRA_SETUP_INTENT);
+ extras.remove(TvCommonConstants.EXTRA_SETUP_INTENT);
setupIntent.putExtras(extras);
try {
startActivityForResult(setupIntent, REQUEST_START_SETUP_ACTIVITY);
@@ -121,15 +107,14 @@ public class SetupPassthroughActivity extends Activity {
sScanTimeoutMonitor = new ScanTimeoutMonitor(this);
}
sScanTimeoutMonitor.startMonitoring();
- TvSingletons.getSingletons(this).getEpgFetcher().onChannelScanStarted();
+ EpgFetcher.getInstance(this).onChannelScanStarted();
}
}
}
@Override
public void onActivityResult(int requestCode, final int resultCode, final Intent data) {
- if (DEBUG)
- Log.d(TAG, "onActivityResult(" + requestCode + ", " + resultCode + ", " + data + ")");
+ if (DEBUG) Log.d(TAG, "onActivityResult");
if (sScanTimeoutMonitor != null) {
sScanTimeoutMonitor.stopMonitoring();
}
@@ -137,17 +122,15 @@ public class SetupPassthroughActivity extends Activity {
boolean setupComplete =
requestCode == REQUEST_START_SETUP_ACTIVITY && resultCode == Activity.RESULT_OK;
// Tells EpgFetcher that channel source setup is finished.
- EpgFetcher epgFetcher = TvSingletons.getSingletons(this).getEpgFetcher();
if (mEpgFetcherDuringScan) {
- epgFetcher.onChannelScanFinished();
+ EpgFetcher.getInstance(this).onChannelScanFinished();
}
if (!setupComplete) {
setResult(resultCode, data);
finish();
return;
}
- TvSingletons.getSingletons(this)
- .getSetupUtils()
+ SetupUtils.getInstance(this)
.onTvInputSetupFinished(
mTvInputInfo.getId(),
new Runnable() {
@@ -209,7 +192,7 @@ public class SetupPassthroughActivity extends Activity {
private ScanTimeoutMonitor(Context context) {
mContext = context.getApplicationContext();
- mChannelDataManager = TvSingletons.getSingletons(context).getChannelDataManager();
+ mChannelDataManager = TvApplication.getSingletons(context).getChannelDataManager();
}
private void startMonitoring() {
@@ -237,7 +220,7 @@ public class SetupPassthroughActivity extends Activity {
private void onScanTimedOut() {
stopMonitoring();
- TvSingletons.getSingletons(mContext).getEpgFetcher().onChannelScanFinished();
+ EpgFetcher.getInstance(mContext).onChannelScanFinished();
}
}
}
diff --git a/src/com/android/tv/Starter.java b/src/com/android/tv/Starter.java
deleted file mode 100644
index 22fda0bd..00000000
--- a/src/com/android/tv/Starter.java
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * 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.util.Log;
-
-/** Initializes TvApplication. */
-public interface Starter {
-
- /**
- * Initializes TvApplication.
- *
- * <p>Note: it should be called at the beginning of any Service.onCreate, Activity.onCreate, or
- * BroadcastReceiver.onCreate.
- */
- static void start(Context context) {
- // TODO(b/63064354) TvApplication should not have to know if it is "the main process"
- if (context.getApplicationContext() instanceof Starter) {
- Starter starter = (Starter) context.getApplicationContext();
- starter.start();
- } else {
- // Application context can be MockTvApplication.
- Log.w("Start", "It is not a context of TvApplication");
- }
- }
-
- void start();
-}
diff --git a/src/com/android/tv/TimeShiftManager.java b/src/com/android/tv/TimeShiftManager.java
index e37f190c..513fe3cd 100644
--- a/src/com/android/tv/TimeShiftManager.java
+++ b/src/com/android/tv/TimeShiftManager.java
@@ -146,8 +146,8 @@ public class TimeShiftManager {
DISABLE_ACTION_THRESHOLD + 3 * REQUEST_CURRENT_POSITION_INTERVAL;
/**
* The current position sent from TIS can not be exactly the same as the current system time due
- * to the elapsed time to pass the message from TIS to Live TV. So the boundary threshold
- * is necessary. The same goes for the recording start time. It's the same {@link
+ * to the elapsed time to pass the message from TIS to Live TV. So the boundary threshold is
+ * necessary. The same goes for the recording start time. It's the same {@link
* #REQUEST_CURRENT_POSITION_INTERVAL}.
*/
private static final long RECORDING_BOUNDARY_THRESHOLD = REQUEST_CURRENT_POSITION_INTERVAL;
@@ -1095,9 +1095,10 @@ public class TimeShiftManager {
SoftPreconditions.checkArgument(
endTimeMs - startTimeMs <= TWO_WEEKS_MS,
TAG,
- "createDummyProgram: long duration of dummy programs are requested ( %s , %s)",
- Utils.toTimeString(startTimeMs),
- Utils.toTimeString(endTimeMs));
+ "createDummyProgram: long duration of dummy programs are requested ("
+ + Utils.toTimeString(startTimeMs)
+ + ", "
+ + Utils.toTimeString(endTimeMs));
if (startTimeMs >= endTimeMs) {
return Collections.emptyList();
}
diff --git a/src/com/android/tv/TvApplication.java b/src/com/android/tv/TvApplication.java
index 01fe5b07..549ab6d8 100644
--- a/src/com/android/tv/TvApplication.java
+++ b/src/com/android/tv/TvApplication.java
@@ -18,6 +18,7 @@ package com.android.tv;
import android.annotation.TargetApi;
import android.app.Activity;
+import android.app.Application;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
@@ -28,24 +29,30 @@ 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;
-import com.android.tv.common.BaseApplication;
+import com.android.tv.analytics.Analytics;
+import com.android.tv.analytics.StubAnalytics;
+import com.android.tv.analytics.Tracker;
+import com.android.tv.common.BuildConfig;
+import com.android.tv.common.SharedPreferencesUtils;
+import com.android.tv.common.SoftPreconditions;
+import com.android.tv.common.TvCommonUtils;
import com.android.tv.common.feature.CommonFeatures;
-import com.android.tv.common.recording.RecordingStorageStatusManager;
import com.android.tv.common.ui.setup.animation.SetupAnimationHelper;
-import com.android.tv.common.util.Clock;
-import com.android.tv.common.util.Debug;
-import com.android.tv.common.util.SharedPreferencesUtils;
+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.data.epg.EpgFetcherImpl;
import com.android.tv.dvr.DvrDataManager;
import com.android.tv.dvr.DvrDataManagerImpl;
import com.android.tv.dvr.DvrManager;
@@ -53,65 +60,80 @@ 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;
import com.android.tv.util.Utils;
import java.util.List;
-/**
- * Live TV application.
- *
- * <p>This includes all the Google specific hooks.
- */
-public abstract class TvApplication extends BaseApplication implements TvSingletons, Starter {
+public class TvApplication extends Application implements ApplicationSingletons {
private static final String TAG = "TvApplication";
private static final boolean DEBUG = false;
+ private static final TimerEvent sAppStartTimer = StubPerformanceMonitor.startBootstrapTimer();
- /** Namespace for LiveChannels configs. LiveChannels configs are kept in piper. */
- public static final String CONFIGNS_P4 = "configns:p4";
+ /**
+ * 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
- * com.android.tv.tuner.TunerInputController} will recevice this intent to check the
- * existence of tuner input when the new version is first launched.
+ * 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();
private SelectInputActivity mSelectInputActivity;
+ private Analytics mAnalytics;
+ private Tracker mTracker;
+ private TvInputManagerHelper mTvInputManagerHelper;
private ChannelDataManager mChannelDataManager;
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;
- private RecordingStorageStatusManager mDvrStorageStatusManager;
@Nullable private InputSessionManager mInputSessionManager;
- // STOP-SHIP: Remove this variable when Tuner Process is split to another application.
+ private AccountHelper mAccountHelper;
// When this variable is null, we don't know in which process TvApplication runs.
private Boolean mRunningInMainProcess;
private PerformanceMonitor mPerformanceMonitor;
- private TvInputManagerHelper mTvInputManagerHelper;
- private boolean mStarted;
- private EpgFetcher mEpgFetcher;
- private TunerInputController mTunerInputController;
@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() {
@@ -122,6 +144,9 @@ public abstract class TvApplication extends BaseApplication implements TvSinglet
}
}
});
+ // TunerPreferences is used to enable/disable the tuner input even when TUNER feature is
+ // disabled.
+ TunerPreferences.initialize(this);
try {
PackageInfo pInfo = getPackageManager().getPackageInfo(getPackageName(), 0);
mVersionName = pInfo.versionName;
@@ -131,35 +156,66 @@ public abstract class TvApplication extends BaseApplication implements TvSinglet
}
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().penaltyDeath();
+ if (!TvCommonUtils.isRunningInTest()) {
+ threadPolicyBuilder.penaltyDialog();
+ }
+ StrictMode.setThreadPolicy(threadPolicyBuilder.build());
+ StrictMode.setVmPolicy(vmPolicyBuilder.build());
+ }
+ if (BuildConfig.ENG && !SystemProperties.ALLOW_ANALYTICS_IN_ENG.getValue()) {
+ mAnalytics = StubAnalytics.getInstance(this);
+ } else {
+ mAnalytics = StubAnalytics.getInstance(this);
+ }
+ mTracker = mAnalytics.getDefaultTracker();
+ 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.
- mEpgFetcher = EpgFetcherImpl.create(this);
SetupAnimationHelper.initialize(this);
- getTvInputManagerHelper();
Log.i(TAG, "Started Live TV " + mVersionName);
Debug.getTimer(Debug.TAG_START_UP_TIMER).log("finish TvApplication.onCreate");
+ getPerformanceMonitor().stopTimer(sAppStartTimer, EventNames.APPLICATION_ONCREATE);
}
- /** Initializes application. It is a noop if called twice. */
- @Override
- public void start() {
- if (mStarted) {
+ private void setCurrentRunningProcess(boolean isMainProcess) {
+ if (mRunningInMainProcess != null) {
+ SoftPreconditions.checkState(isMainProcess == mRunningInMainProcess);
return;
}
- mStarted = true;
- mRunningInMainProcess = true;
- Debug.getTimer(Debug.TAG_START_UP_TIMER).log("start TvApplication.start");
+ 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) {
getTvInputManagerHelper()
.addCallback(
new TvInputCallback() {
@Override
public void onInputAdded(String inputId) {
- if (TvFeatures.TUNER.isEnabled(TvApplication.this)
+ if (Features.TUNER.isEnabled(TvApplication.this)
&& TextUtils.equals(
- inputId, getEmbeddedTunerInputId())) {
+ inputId,
+ TunerTvInputService.getInputId(
+ TvApplication.this))) {
TunerInputInfoUtils.updateTunerInputInfo(
TvApplication.this);
}
@@ -171,7 +227,7 @@ public abstract class TvApplication extends BaseApplication implements TvSinglet
handleInputCountChanged();
}
});
- if (TvFeatures.TUNER.isEnabled(this)) {
+ 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(TvApplication.this);
@@ -181,14 +237,15 @@ public abstract class TvApplication extends BaseApplication implements TvSinglet
mDvrManager = new DvrManager(this);
mRecordingScheduler = RecordingScheduler.createScheduler(this);
}
- mEpgFetcher.startRoutineService();
+ 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.start");
+ Debug.getTimer(Debug.TAG_START_UP_TIMER)
+ .log("finish TvApplication.setCurrentRunningProcess");
}
private void checkTunerServiceOnFirstLaunch() {
@@ -198,24 +255,13 @@ public abstract class TvApplication extends BaseApplication implements TvSinglet
boolean isFirstLaunch = sharedPreferences.getBoolean(PREFERENCE_IS_FIRST_LAUNCH, true);
if (isFirstLaunch) {
if (DEBUG) Log.d(TAG, "Congratulations, it's the first launch!");
- getTunerInputController()
- .onCheckingUsbTunerStatus(this, 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();
}
}
- @Override
- public EpgFetcher getEpgFetcher() {
- return mEpgFetcher;
- }
-
- @Override
- public synchronized SetupUtils getSetupUtils() {
- return SetupUtils.createForTvSingletons(this);
- }
-
/** Returns the {@link DvrManager}. */
@Override
public DvrManager getDvrManager() {
@@ -253,6 +299,18 @@ public abstract class TvApplication extends BaseApplication implements TvSinglet
return mInputSessionManager;
}
+ /** Returns the {@link Analytics}. */
+ @Override
+ public Analytics getAnalytics() {
+ return mAnalytics;
+ }
+
+ /** Returns the default tracker. */
+ @Override
+ public Tracker getTracker() {
+ return mTracker;
+ }
+
/** Returns {@link ChannelDataManager}. */
@Override
public ChannelDataManager getChannelDataManager() {
@@ -315,19 +373,11 @@ public abstract class TvApplication extends BaseApplication implements TvSinglet
return mDvrDataManager;
}
- @Override
+ /** Returns {@link DvrStorageStatusManager}. */
@TargetApi(Build.VERSION_CODES.N)
- public RecordingStorageStatusManager getRecordingStorageStatusManager() {
- if (mDvrStorageStatusManager == null) {
- mDvrStorageStatusManager = new DvrStorageStatusManager(this);
- }
- return mDvrStorageStatusManager;
- }
-
- /** Returns the main activity information. */
@Override
- public MainActivityWrapper getMainActivityWrapper() {
- return mMainActivityWrapper;
+ public DvrStorageStatusManager getDvrStorageStatusManager() {
+ return mDvrStorageStatusManager;
}
/** Returns {@link TvInputManagerHelper}. */
@@ -340,14 +390,28 @@ public abstract class TvApplication extends BaseApplication implements TvSinglet
return mTvInputManagerHelper;
}
+ /** Returns the main activity information. */
+ @Override
+ public MainActivityWrapper getMainActivityWrapper() {
+ return mMainActivityWrapper;
+ }
+
+ /** Returns the {@link AccountHelper}. */
+ @Override
+ public AccountHelper getAccountHelper() {
+ if (mAccountHelper == null) {
+ mAccountHelper = new AccountHelper(getApplicationContext());
+ }
+ return mAccountHelper;
+ }
+
@Override
- public synchronized TunerInputController getTunerInputController() {
- if (mTunerInputController == null) {
- mTunerInputController =
- new TunerInputController(
- ComponentName.unflattenFromString(getEmbeddedTunerInputId()));
+ public RemoteConfig getRemoteConfig() {
+ if (mRemoteConfig == null) {
+ // No need to synchronize this, it does not hurt to create two and throw one away.
+ mRemoteConfig = DefaultConfigManager.createInstance(this).getRemoteConfig();
}
- return mTunerInputController;
+ return mRemoteConfig;
}
@Override
@@ -355,6 +419,14 @@ public abstract class TvApplication extends BaseApplication implements TvSinglet
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}.
@@ -441,10 +513,9 @@ public abstract class TvApplication extends BaseApplication implements TvSinglet
}
/**
- * Checks the input counts and enable/disable TvActivity. Also upda162 the input list in {@link
+ * Checks the input counts and enable/disable TvActivity. Also updates the input list in {@link
* SetupUtils}.
*/
- @Override
public void handleInputCountChanged() {
handleInputCountChanged(false, false, false);
}
@@ -453,8 +524,8 @@ public abstract class TvApplication extends BaseApplication implements TvSinglet
* Checks the input counts and enable/disable TvActivity. Also updates the input list in {@link
* SetupUtils}.
*
- * @param calledByTunerServiceChanged true if it is called when BaseTunerTvInputService is
- * enabled or disabled.
+ * @param calledByTunerServiceChanged true if it is called when TunerTvInputService is enabled
+ * or disabled.
* @param tunerServiceEnabled it's available only when calledByTunerServiceChanged is true.
* @param dontKillApp when TvActivity is enabled or disabled by this method, the app restarts by
* default. But, if dontKillApp is true, the app won't restart.
@@ -464,7 +535,7 @@ public abstract class TvApplication extends BaseApplication implements TvSinglet
TvInputManager inputManager = (TvInputManager) getSystemService(Context.TV_INPUT_SERVICE);
boolean enable =
(calledByTunerServiceChanged && tunerServiceEnabled)
- || TvFeatures.UNHIDE.isEnabled(TvApplication.this);
+ || Features.UNHIDE.isEnabled(TvApplication.this);
if (!enable) {
List<TvInputInfo> inputs = inputManager.getTvInputList();
boolean skipTunerInputCheck = false;
@@ -473,7 +544,7 @@ public abstract class TvApplication extends BaseApplication implements TvSinglet
for (TvInputInfo input : inputs) {
if (calledByTunerServiceChanged
&& !tunerServiceEnabled
- && getEmbeddedTunerInputId().equals(input.getId())) {
+ && TunerTvInputService.getInputId(this).equals(input.getId())) {
continue;
}
if (input.getType() == TvInputInfo.TYPE_TUNER) {
@@ -495,6 +566,34 @@ public abstract class TvApplication extends BaseApplication implements TvSinglet
name, newState, dontKillApp ? PackageManager.DONT_KILL_APP : 0);
Log.i(TAG, (enable ? "Un-hide" : "Hide") + " Live TV.");
}
- getSetupUtils().onInputListUpdated(inputManager);
+ SetupUtils.getInstance(TvApplication.this).onInputListUpdated(inputManager);
+ }
+
+ /** Returns the @{@link ApplicationSingletons} using the application context. */
+ public static ApplicationSingletons getSingletons(Context context) {
+ // No need to be "synchronized" because this doesn't create any instance.
+ if (sAppSingletons == null) {
+ sAppSingletons = (ApplicationSingletons) context.getApplicationContext();
+ }
+ return sAppSingletons;
+ }
+
+ /**
+ * Sets true, if TvApplication is running on the main process. If TvApplication runs on tuner
+ * process or other process, it sets false.
+ *
+ * <p>Note: it should be called at the beginning of Service.onCreate Activity.onCreate, or
+ * BroadcastReceiver.onCreate. When it is firstly called after launch, it runs process 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);
+ } else {
+ // Application context can be MockTvApplication.
+ Log.w(TAG, "It is not a context of TvApplication");
+ }
}
}
diff --git a/src/com/android/tv/TvFeatures.java b/src/com/android/tv/TvFeatures.java
deleted file mode 100644
index 64141e8c..00000000
--- a/src/com/android/tv/TvFeatures.java
+++ /dev/null
@@ -1,103 +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;
-
-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 com.android.tv.common.experiments.Experiments;
-import com.android.tv.common.feature.CommonFeatures;
-import com.android.tv.common.feature.ExperimentFeature;
-import com.android.tv.common.feature.Feature;
-import com.android.tv.common.feature.FeatureUtils;
-import com.android.tv.common.feature.GServiceFeature;
-import com.android.tv.common.feature.PropertyFeature;
-import com.android.tv.common.feature.Sdk;
-import com.android.tv.common.feature.TestableFeature;
-import com.android.tv.common.util.PermissionUtils;
-
-
-
-/**
- * List of {@link Feature} for the Live TV App.
- *
- * <p>Remove the {@code Feature} once it is launched.
- */
-public final class TvFeatures extends CommonFeatures {
-
- /** When enabled use system setting for turning on analytics. */
- public static final Feature ANALYTICS_OPT_IN =
- ExperimentFeature.from(Experiments.ENABLE_ANALYTICS_VIA_CHECKBOX);
-
- /**
- * Analytics that include sensitive information such as channel or program identifiers.
- *
- * <p>See <a href="http://b/22062676">b/22062676</a>
- */
- public static final Feature ANALYTICS_V2 = AND(ON, ANALYTICS_OPT_IN);
-
- public static final Feature EPG_SEARCH =
- PropertyFeature.create("feature_tv_use_epg_search", false);
-
- private static final String GSERVICE_KEY_UNHIDE = "live_channels_unhide";
- /** A flag which indicates that LC app is unhidden even when there is no input. */
- public static final Feature UNHIDE =
- OR(
- new GServiceFeature(GSERVICE_KEY_UNHIDE, false),
- new Feature() {
- @Override
- public boolean isEnabled(Context context) {
- // If LC app runs as non-system app, we unhide the app.
- return !PermissionUtils.hasAccessAllEpg(context);
- }
- });
-
- public static final Feature PICTURE_IN_PICTURE =
- new Feature() {
- private Boolean 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. */
- public static final Feature SHOW_UPCOMING_CONFLICT_DIALOG = OFF;
-
- /** Use input blacklist to disable partner's tuner input. */
- public static final Feature USE_PARTNER_INPUT_BLACKLIST = ON;
-
- @VisibleForTesting
- public static final Feature TEST_FEATURE = PropertyFeature.create("test_feature", false);
-
- private TvFeatures() {}
-}
diff --git a/src/com/android/tv/analytics/SendChannelStatusRunnable.java b/src/com/android/tv/analytics/SendChannelStatusRunnable.java
index 2f208828..601e82f7 100644
--- a/src/com/android/tv/analytics/SendChannelStatusRunnable.java
+++ b/src/com/android/tv/analytics/SendChannelStatusRunnable.java
@@ -31,8 +31,7 @@ import java.util.concurrent.TimeUnit;
*
* <p>
*
- * <p>This should only be started from a user activity like {@link
- * com.android.tv.MainActivity}.
+ * <p>This should only be started from a user activity like {@link com.android.tv.MainActivity}.
*/
@MainThread
public class SendChannelStatusRunnable implements Runnable {
diff --git a/src/com/android/tv/app/LiveTvApplication.java b/src/com/android/tv/app/LiveTvApplication.java
deleted file mode 100644
index 1c4f1522..00000000
--- a/src/com/android/tv/app/LiveTvApplication.java
+++ /dev/null
@@ -1,137 +0,0 @@
-/*
- * 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.app;
-
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.media.tv.TvContract;
-import com.android.tv.tuner.tvinput.LiveTvTunerTvInputService;
-import com.android.tv.TvApplication;
-import com.android.tv.analytics.Analytics;
-import com.android.tv.analytics.StubAnalytics;
-import com.android.tv.analytics.Tracker;
-import com.android.tv.common.CommonConstants;
-import com.android.tv.common.config.DefaultConfigManager;
-import com.android.tv.common.config.api.RemoteConfig;
-import com.android.tv.common.experiments.ExperimentLoader;
-import com.android.tv.common.util.CommonUtils;
-import com.android.tv.data.epg.EpgReader;
-import com.android.tv.data.epg.StubEpgReader;
-import com.android.tv.perf.PerformanceMonitor;
-import com.android.tv.perf.StubPerformanceMonitor;
-import com.android.tv.tuner.setup.LiveTvTunerSetupActivity;
-import com.android.tv.util.account.AccountHelper;
-import com.android.tv.util.account.AccountHelperImpl;
-import javax.inject.Provider;
-
-/** The top level application for Live TV. */
-public class LiveTvApplication extends TvApplication {
- protected static final String TV_ACTIVITY_CLASS_NAME = "com.android.tv.TvActivity";
-
- private final StubPerformanceMonitor performanceMonitor = new StubPerformanceMonitor();
- private final Provider<EpgReader> mEpgReaderProvider =
- new Provider<EpgReader>() {
-
- @Override
- public EpgReader get() {
- return new StubEpgReader(LiveTvApplication.this);
- }
- };
-
- private AccountHelper mAccountHelper;
- private Analytics mAnalytics;
- private Tracker mTracker;
- private String mEmbeddedInputId;
- private RemoteConfig mRemoteConfig;
- private ExperimentLoader mExperimentLoader;
-
- /** Returns the {@link AccountHelperImpl}. */
- @Override
- public AccountHelper getAccountHelper() {
- if (mAccountHelper == null) {
- mAccountHelper = new AccountHelperImpl(getApplicationContext());
- }
- return mAccountHelper;
- }
-
- @Override
- public synchronized PerformanceMonitor getPerformanceMonitor() {
- return performanceMonitor;
- }
-
- @Override
- public Provider<EpgReader> providesEpgReader() {
- return mEpgReaderProvider;
- }
-
- @Override
- public ExperimentLoader getExperimentLoader() {
- mExperimentLoader = new ExperimentLoader();
- return mExperimentLoader;
- }
-
- /** Returns the {@link Analytics}. */
- @Override
- public synchronized Analytics getAnalytics() {
- if (mAnalytics == null) {
- mAnalytics = StubAnalytics.getInstance(this);
- }
- return mAnalytics;
- }
-
- /** Returns the default tracker. */
- @Override
- public synchronized Tracker getTracker() {
- if (mTracker == null) {
- mTracker = getAnalytics().getDefaultTracker();
- }
- return mTracker;
- }
-
- @Override
- public Intent getTunerSetupIntent(Context context) {
- // Make an intent to launch the setup activity of TV tuner input.
- Intent intent =
- CommonUtils.createSetupIntent(
- new Intent(context, LiveTvTunerSetupActivity.class), mEmbeddedInputId);
- intent.putExtra(CommonConstants.EXTRA_INPUT_ID, mEmbeddedInputId);
- Intent tvActivityIntent = new Intent();
- tvActivityIntent.setComponent(new ComponentName(context, TV_ACTIVITY_CLASS_NAME));
- intent.putExtra(CommonConstants.EXTRA_ACTIVITY_AFTER_COMPLETION, tvActivityIntent);
- return intent;
- }
-
- @Override
- public synchronized String getEmbeddedTunerInputId() {
- if (mEmbeddedInputId == null) {
- mEmbeddedInputId =
- TvContract.buildInputId(
- new ComponentName(this, LiveTvTunerTvInputService.class));
- }
- return mEmbeddedInputId;
- }
-
- @Override
- public RemoteConfig getRemoteConfig() {
- if (mRemoteConfig == null) {
- // No need to synchronize this, it does not hurt to create two and throw one away.
- mRemoteConfig = DefaultConfigManager.createInstance(this).getRemoteConfig();
- }
- return mRemoteConfig;
- }
-}
diff --git a/src/com/android/tv/config/ConfigKeys.java b/src/com/android/tv/config/ConfigKeys.java
new file mode 100644
index 00000000..135017ae
--- /dev/null
+++ b/src/com/android/tv/config/ConfigKeys.java
@@ -0,0 +1,23 @@
+/*
+ * 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.config;
+
+/** Static list of config keys. */
+public final class ConfigKeys {
+
+ private ConfigKeys() {}
+}
diff --git a/src/com/android/tv/config/DefaultConfigManager.java b/src/com/android/tv/config/DefaultConfigManager.java
new file mode 100644
index 00000000..4d754d1f
--- /dev/null
+++ b/src/com/android/tv/config/DefaultConfigManager.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.config;
+
+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();
+ }
+
+ private StubRemoteConfig mRemoteConfig = new StubRemoteConfig();
+
+ public RemoteConfig getRemoteConfig() {
+ return mRemoteConfig;
+ }
+
+ private static class StubRemoteConfig implements RemoteConfig {
+ @Override
+ public void fetch(OnRemoteConfigUpdatedListener listener) {}
+
+ @Override
+ public String getString(String key) {
+ return null;
+ }
+
+ @Override
+ 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
new file mode 100644
index 00000000..d72a1f3f
--- /dev/null
+++ b/src/com/android/tv/config/RemoteConfig.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.config;
+
+/**
+ * Manages Live TV Configuration, allowing remote updates.
+ *
+ * <p>This is a thin wrapper around <a
+ * href="https://firebase.google.com/docs/remote-config/"></a>Firebase Remote Config</a>
+ */
+public interface RemoteConfig {
+
+ /** Notified on successful completion of a {@link #fetch)} */
+ interface OnRemoteConfigUpdatedListener {
+ void onRemoteConfigUpdated();
+ }
+
+ /** Starts a fetch and notifies {@code listener} on successful completion. */
+ void fetch(OnRemoteConfigUpdatedListener listener);
+
+ /** Gets value as a string corresponding to the specified key. */
+ String getString(String key);
+
+ /** 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/RemoteConfigFeature.java b/src/com/android/tv/config/RemoteConfigFeature.java
new file mode 100644
index 00000000..c22446be
--- /dev/null
+++ b/src/com/android/tv/config/RemoteConfigFeature.java
@@ -0,0 +1,40 @@
+/*
+ * 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.config;
+
+import android.content.Context;
+import com.android.tv.TvApplication;
+import com.android.tv.common.feature.Feature;
+
+/** A {@link Feature} controlled by a {@link RemoteConfig} boolean. */
+public class RemoteConfigFeature implements Feature {
+ private final String mKey;
+
+ /** Creates a {@link RemoteConfigFeature for the {@code key}. */
+ public static RemoteConfigFeature fromKey(String key) {
+ return new RemoteConfigFeature(key);
+ }
+
+ private RemoteConfigFeature(String key) {
+ mKey = key;
+ }
+
+ @Override
+ public boolean isEnabled(Context context) {
+ return TvApplication.getSingletons(context).getRemoteConfig().getBoolean(mKey);
+ }
+}
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/CustomAction.java b/src/com/android/tv/customization/CustomAction.java
new file mode 100644
index 00000000..77a5ae5e
--- /dev/null
+++ b/src/com/android/tv/customization/CustomAction.java
@@ -0,0 +1,68 @@
+/*
+ * 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.customization;
+
+import android.content.Intent;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.NonNull;
+
+/** Describes a custom option defined in customization package. This will be added to main menu. */
+public class CustomAction implements Comparable<CustomAction> {
+ private static final int POSITION_THRESHOLD = 100;
+
+ private final int mPositionPriority;
+ private final String mTitle;
+ private final Drawable mIconDrawable;
+ private final Intent mIntent;
+
+ public CustomAction(int positionPriority, String title, Drawable iconDrawable, Intent intent) {
+ mPositionPriority = positionPriority;
+ mTitle = title;
+ mIconDrawable = iconDrawable;
+ mIntent = intent;
+ }
+
+ /**
+ * Returns if this option comes before the existing items. Note that custom options can only be
+ * placed at the front or back. (i.e. cannot be added in the middle of existing options.)
+ *
+ * @return {@code true} if it goes to the beginning. {@code false} if it goes to the end.
+ */
+ public boolean isFront() {
+ return mPositionPriority < POSITION_THRESHOLD;
+ }
+
+ @Override
+ public int compareTo(@NonNull CustomAction another) {
+ return mPositionPriority - another.mPositionPriority;
+ }
+
+ /** Returns title. */
+ public String getTitle() {
+ return mTitle;
+ }
+
+ /** Returns icon drawable. */
+ public Drawable getIconDrawable() {
+ return mIconDrawable;
+ }
+
+ /** Returns intent to launch when this option is clicked. */
+ public Intent getIntent() {
+ return mIntent;
+ }
+}
diff --git a/src/com/android/tv/customization/TvCustomizationManager.java b/src/com/android/tv/customization/TvCustomizationManager.java
new file mode 100644
index 00000000..7d21c6d2
--- /dev/null
+++ b/src/com/android/tv/customization/TvCustomizationManager.java
@@ -0,0 +1,271 @@
+/*
+ * 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.customization;
+
+import android.content.Context;
+import android.content.Intent;
+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;
+import java.util.List;
+import java.util.Map;
+
+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";
+
+ /** Row IDs to share customized actions. Only rows listed below can have customized action. */
+ 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<>();
+ INTENT_CATEGORY_TO_ROW_ID.put(CATEGORY_TV_CUSTOMIZATION + ".OPTIONS_ROW", ID_OPTIONS_ROW);
+ INTENT_CATEGORY_TO_ROW_ID.put(CATEGORY_TV_CUSTOMIZATION + ".PARTNER_ROW", ID_PARTNER_ROW);
+ }
+
+ 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 mPartnerRowTitle;
+ private final Map<String, List<CustomAction>> mRowIdToCustomActionsMap = new HashMap<>();
+
+ public TvCustomizationManager(Context context) {
+ mContext = context;
+ mInitialized = false;
+ }
+
+ /**
+ * 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. */
+ public void initialize() {
+ if (mInitialized) {
+ return;
+ }
+ mInitialized = true;
+ if (!TextUtils.isEmpty(getCustomizationPackageName(mContext))) {
+ buildCustomActions();
+ buildPartnerRow();
+ }
+ }
+
+ private void buildCustomActions() {
+ mRowIdToCustomActionsMap.clear();
+ PackageManager pm = mContext.getPackageManager();
+ for (String intentCategory : INTENT_CATEGORY_TO_ROW_ID.keySet()) {
+ Intent customOptionIntent = new Intent(Intent.ACTION_MAIN);
+ customOptionIntent.addCategory(intentCategory);
+
+ List<ResolveInfo> activities =
+ pm.queryIntentActivities(
+ customOptionIntent,
+ PackageManager.GET_RECEIVERS
+ | PackageManager.GET_RESOLVED_FILTER
+ | PackageManager.GET_META_DATA);
+ for (ResolveInfo info : activities) {
+ String packageName = info.activityInfo.packageName;
+ if (!TextUtils.equals(packageName, sCustomizationPackage)) {
+ Log.w(
+ TAG,
+ "A customization package "
+ + sCustomizationPackage
+ + " already exist. Ignoring "
+ + packageName);
+ continue;
+ }
+
+ int position = info.filter.getPriority();
+ String title = info.loadLabel(pm).toString();
+ Drawable drawable = info.loadIcon(pm);
+ Intent intent = new Intent(Intent.ACTION_MAIN);
+ intent.addCategory(intentCategory);
+ intent.setClassName(sCustomizationPackage, info.activityInfo.name);
+
+ String rowId = INTENT_CATEGORY_TO_ROW_ID.get(intentCategory);
+ List<CustomAction> actions = mRowIdToCustomActionsMap.get(rowId);
+ if (actions == null) {
+ actions = new ArrayList<>();
+ mRowIdToCustomActionsMap.put(rowId, actions);
+ }
+ actions.add(new CustomAction(position, title, drawable, intent));
+ }
+ }
+ // Sort items by position
+ for (List<CustomAction> actions : mRowIdToCustomActionsMap.values()) {
+ Collections.sort(actions);
+ }
+
+ if (DEBUG) {
+ Log.d(TAG, "Dumping custom actions");
+ for (String id : mRowIdToCustomActionsMap.keySet()) {
+ for (CustomAction action : mRowIdToCustomActionsMap.get(id)) {
+ Log.d(
+ TAG,
+ "Custom row rowId="
+ + id
+ + " title="
+ + action.getTitle()
+ + " class="
+ + action.getIntent());
+ }
+ }
+ Log.d(TAG, "Dumping custom actions - end of dump");
+ }
+ }
+
+ /**
+ * Returns custom actions for given row id.
+ *
+ * <p>Row ID is one of ID_OPTIONS_ROW or ID_PARTNER_ROW.
+ */
+ public List<CustomAction> getCustomActions(String rowId) {
+ return mRowIdToCustomActionsMap.get(rowId);
+ }
+
+ private void buildPartnerRow() {
+ mPartnerRowTitle = null;
+ Resources res;
+ try {
+ res = mContext.getPackageManager().getResourcesForApplication(sCustomizationPackage);
+ } catch (NameNotFoundException e) {
+ Log.w(TAG, "Could not get resources for package " + sCustomizationPackage);
+ return;
+ }
+ int resId =
+ res.getIdentifier(RES_ID_PARTNER_ROW_TITLE, RES_TYPE_STRING, sCustomizationPackage);
+ if (resId != 0) {
+ mPartnerRowTitle = res.getString(resId);
+ }
+ if (DEBUG) Log.d(TAG, "Partner row title [" + mPartnerRowTitle + "]");
+ }
+
+ /** Returns partner row title. */
+ public String getPartnerRowTitle() {
+ return mPartnerRowTitle;
+ }
+}
diff --git a/src/com/android/tv/data/Channel.java b/src/com/android/tv/data/Channel.java
index 1204a49f..eda188e4 100644
--- a/src/com/android/tv/data/Channel.java
+++ b/src/com/android/tv/data/Channel.java
@@ -28,8 +28,7 @@ import android.support.annotation.UiThread;
import android.support.annotation.VisibleForTesting;
import android.text.TextUtils;
import android.util.Log;
-import com.android.tv.common.CommonConstants;
-import com.android.tv.common.util.CommonUtils;
+import com.android.tv.common.TvCommonConstants;
import com.android.tv.util.ImageLoader;
import com.android.tv.util.TvInputManagerHelper;
import com.android.tv.util.Utils;
@@ -125,7 +124,7 @@ public final class Channel {
channel.mAppLinkIconUri = cursor.getString(index++);
channel.mAppLinkPosterArtUri = cursor.getString(index++);
channel.mAppLinkIntentUri = cursor.getString(index++);
- if (CommonUtils.isBundledInput(channel.mInputId)) {
+ if (Utils.isBundledInput(channel.mInputId)) {
channel.mRecordingProhibited = cursor.getInt(index++) != 0;
}
return channel;
@@ -626,7 +625,7 @@ public final class Channel {
if (intent.resolveActivityInfo(pm, 0) != null) {
mAppLinkIntent = intent;
mAppLinkIntent.putExtra(
- CommonConstants.EXTRA_APP_LINK_CHANNEL_URI, getUri().toString());
+ TvCommonConstants.EXTRA_APP_LINK_CHANNEL_URI, getUri().toString());
mAppLinkType = APP_LINK_TYPE_CHANNEL;
return;
} else {
@@ -643,7 +642,7 @@ public final class Channel {
mAppLinkIntent = pm.getLeanbackLaunchIntentForPackage(mPackageName);
if (mAppLinkIntent != null) {
mAppLinkIntent.putExtra(
- CommonConstants.EXTRA_APP_LINK_CHANNEL_URI, getUri().toString());
+ TvCommonConstants.EXTRA_APP_LINK_CHANNEL_URI, getUri().toString());
mAppLinkType = APP_LINK_TYPE_APP;
}
}
diff --git a/src/com/android/tv/data/ChannelDataManager.java b/src/com/android/tv/data/ChannelDataManager.java
index 68fbdb6a..e4d1cd85 100644
--- a/src/com/android/tv/data/ChannelDataManager.java
+++ b/src/com/android/tv/data/ChannelDataManager.java
@@ -38,11 +38,11 @@ import android.support.annotation.VisibleForTesting;
import android.util.ArraySet;
import android.util.Log;
import android.util.MutableInt;
+import com.android.tv.common.SharedPreferencesUtils;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.common.WeakHandler;
-import com.android.tv.common.util.PermissionUtils;
-import com.android.tv.common.util.SharedPreferencesUtils;
import com.android.tv.util.AsyncDbTask;
+import com.android.tv.util.PermissionUtils;
import com.android.tv.util.TvInputManagerHelper;
import com.android.tv.util.Utils;
import java.io.IOException;
@@ -62,7 +62,6 @@ import java.util.concurrent.CopyOnWriteArraySet;
* methods are called in only the main thread.
*/
@AnyThread
-@SuppressWarnings("TryWithResources") // TODO(b/62143348): remove when error prone check fixed
public class ChannelDataManager {
private static final String TAG = "ChannelDataManager";
private static final boolean DEBUG = false;
diff --git a/src/com/android/tv/data/ChannelLogoFetcher.java b/src/com/android/tv/data/ChannelLogoFetcher.java
index 8aaaf73a..2dc43102 100644
--- a/src/com/android/tv/data/ChannelLogoFetcher.java
+++ b/src/com/android/tv/data/ChannelLogoFetcher.java
@@ -28,10 +28,10 @@ import android.os.RemoteException;
import android.support.annotation.MainThread;
import android.text.TextUtils;
import android.util.Log;
-import com.android.tv.common.util.PermissionUtils;
-import com.android.tv.common.util.SharedPreferencesUtils;
+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.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
@@ -42,7 +42,6 @@ import java.util.Map;
* 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.
*/
-@SuppressWarnings("TryWithResources") // TODO(b/62143348): remove when error prone check fixed
public class ChannelLogoFetcher {
private static final String TAG = "ChannelLogoFetcher";
private static final boolean DEBUG = false;
diff --git a/src/com/android/tv/data/ChannelNumber.java b/src/com/android/tv/data/ChannelNumber.java
index 1623b33d..63f8a972 100644
--- a/src/com/android/tv/data/ChannelNumber.java
+++ b/src/com/android/tv/data/ChannelNumber.java
@@ -19,7 +19,7 @@ package com.android.tv.data;
import android.support.annotation.NonNull;
import android.text.TextUtils;
import android.view.KeyEvent;
-import com.android.tv.common.util.StringUtils;
+import com.android.tv.util.StringUtils;
import java.util.Objects;
/** A convenience class to handle channel number. */
@@ -43,23 +43,6 @@ public final class ChannelNumber implements Comparable<ChannelNumber> {
reset();
}
- /**
- * {@code lhs} and {@code rhs} are equivalent if {@link ChannelNumber#compare(String, String)}
- * is 0 or if only one has a delimiter and both {@link ChannelNumber#majorNumber} equals.
- */
- public static boolean equivalent(String lhs, String rhs) {
- if (compare(lhs, rhs) == 0) {
- return true;
- }
- // Match if only one has delimiter
- ChannelNumber lhsNumber = parseChannelNumber(lhs);
- ChannelNumber rhsNumber = parseChannelNumber(rhs);
- return lhsNumber != null
- && rhsNumber != null
- && lhsNumber.hasDelimiter != rhsNumber.hasDelimiter
- && lhsNumber.majorNumber.equals(rhsNumber.majorNumber);
- }
-
public void reset() {
setChannelNumber("", false, "");
}
diff --git a/src/com/android/tv/data/InternalDataUtils.java b/src/com/android/tv/data/InternalDataUtils.java
index 99a3d4e8..4c30d395 100644
--- a/src/com/android/tv/data/InternalDataUtils.java
+++ b/src/com/android/tv/data/InternalDataUtils.java
@@ -33,7 +33,6 @@ import java.util.List;
* android.media.tv.TvContract.Programs#COLUMN_INTERNAL_PROVIDER_DATA} field in the {@link
* android.media.tv.TvContract.Programs}.
*/
-@SuppressWarnings("TryWithResources") // TODO(b/62143348): remove when error prone check fixed
public final class InternalDataUtils {
private static final boolean DEBUG = false;
private static final String TAG = "InternalDataUtils";
diff --git a/src/com/android/tv/data/Lineup.java b/src/com/android/tv/data/Lineup.java
index 4393cd3d..0f11c1cc 100644
--- a/src/com/android/tv/data/Lineup.java
+++ b/src/com/android/tv/data/Lineup.java
@@ -19,45 +19,23 @@ package com.android.tv.data;
import android.support.annotation.IntDef;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
-import java.util.Collections;
-import java.util.List;
/** A class that represents a lineup. */
public class Lineup {
/** The ID of this lineup. */
- public String getId() {
- return id;
- }
+ public final String id;
/** The type associated with this lineup. */
- public int getType() {
- return type;
- }
-
- /** The human readable name associated with this lineup. */
- public String getName() {
- return name;
- }
+ public final int type;
/** The human readable name associated with this lineup. */
- public String getLocation() {
- return location;
- }
-
- /** An unmodifiable list of channel numbers that this lineup has. */
- public List<String> getChannels() {
- return channels;
- }
+ public final String name;
- private final String id;
-
- private final int type;
-
- private final String name;
-
- private final String location;
-
- private final List<String> channels;
+ /**
+ * Location this lineup can be found. This is a human readable description of a geographic
+ * location.
+ */
+ public final String location;
@Retention(RetentionPolicy.SOURCE)
@IntDef({
@@ -66,9 +44,7 @@ public class Lineup {
LINEUP_BROADCAST_DIGITAL,
LINEUP_BROADCAST_ANALOG,
LINEUP_IPTV,
- LINEUP_MVPD,
- LINEUP_INTERNET,
- LINEUP_OTHER
+ LINEUP_MVPD
})
public @interface LineupType {}
@@ -88,23 +64,16 @@ public class Lineup {
public static final int LINEUP_IPTV = 4;
/**
- * Indicates the lineup is either satellite, cable or IPTV but we are not sure which specific
+ * Indicates the lineup is either satelite, cable or IPTV but we are not sure which specific
* type.
*/
public static final int LINEUP_MVPD = 5;
- /** Lineup type for Internet. */
- public static final int LINEUP_INTERNET = 6;
-
- /** Lineup type for other. */
- public static final int LINEUP_OTHER = 7;
-
/** Creates a lineup. */
- public Lineup(String id, int type, String name, String location, List<String> channels) {
+ public Lineup(String id, int type, String name, String location) {
this.id = id;
this.type = type;
this.name = name;
this.location = location;
- this.channels = Collections.unmodifiableList(channels);
}
}
diff --git a/src/com/android/tv/data/PreviewDataManager.java b/src/com/android/tv/data/PreviewDataManager.java
index ac78147b..b103a5d7 100644
--- a/src/com/android/tv/data/PreviewDataManager.java
+++ b/src/com/android/tv/data/PreviewDataManager.java
@@ -36,7 +36,7 @@ import android.support.media.tv.PreviewProgram;
import android.util.Log;
import android.util.Pair;
import com.android.tv.R;
-import com.android.tv.common.util.PermissionUtils;
+import com.android.tv.util.PermissionUtils;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.HashMap;
@@ -47,22 +47,22 @@ import java.util.concurrent.CopyOnWriteArraySet;
/** Class to manage the preview data. */
@TargetApi(Build.VERSION_CODES.O)
@MainThread
-@SuppressWarnings("TryWithResources") // TODO(b/62143348): remove when error prone check fixed
public class PreviewDataManager {
private static final String TAG = "PreviewDataManager";
- private static final boolean DEBUG = false;
+ // 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})
+ @IntDef({(int) TYPE_DEFAULT_PREVIEW_CHANNEL, (int) TYPE_RECORDED_PROGRAM_PREVIEW_CHANNEL})
@Retention(RetentionPolicy.SOURCE)
public @interface PreviewChannelType {}
/** Type of default preview channel */
- public static final int TYPE_DEFAULT_PREVIEW_CHANNEL = 1;
+ public static final long TYPE_DEFAULT_PREVIEW_CHANNEL = 1;
/** Type of recorded program channel */
- public static final int TYPE_RECORDED_PROGRAM_PREVIEW_CHANNEL = 2;
+ public static final long TYPE_RECORDED_PROGRAM_PREVIEW_CHANNEL = 2;
private final Context mContext;
private final ContentResolver mContentResolver;
@@ -604,8 +604,7 @@ public class PreviewDataManager {
.setPosterArtUri(program.getPosterArtUri())
.setIntentUri(program.getIntentUri())
.setPreviewVideoUri(program.getPreviewVideoUri())
- .setInternalProviderId(Long.toString(program.getId()))
- .setContentId(program.getIntentUri().toString());
+ .setInternalProviderId(Long.toString(program.getId()));
return builder.build();
}
diff --git a/src/com/android/tv/data/PreviewProgramContent.java b/src/com/android/tv/data/PreviewProgramContent.java
index 252092b0..845ca9d4 100644
--- a/src/com/android/tv/data/PreviewProgramContent.java
+++ b/src/com/android/tv/data/PreviewProgramContent.java
@@ -17,18 +17,17 @@
package com.android.tv.data;
import android.content.Context;
+import android.media.tv.TvContract;
import android.net.Uri;
-import android.support.annotation.VisibleForTesting;
-import android.support.media.tv.TvContractCompat;
import android.text.TextUtils;
import android.util.Pair;
-import com.android.tv.TvSingletons;
+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 {
- @VisibleForTesting static final String PARAM_INPUT = "input";
+ private static final String PARAM_INPUT = "input";
private long mId;
private long mPreviewChannelId;
@@ -44,30 +43,17 @@ public class PreviewProgramContent {
public static PreviewProgramContent createFromProgram(
Context context, long previewChannelId, Program program) {
Channel channel =
- TvSingletons.getSingletons(context)
+ TvApplication.getSingletons(context)
.getChannelDataManager()
.getChannel(program.getChannelId());
- return channel == null ? null : createFromProgram(previewChannelId, program, channel);
- }
-
- /** Create preview program content from {@link RecordedProgram} */
- public static PreviewProgramContent createFromRecordedProgram(
- Context context, long previewChannelId, RecordedProgram recordedProgram) {
- Channel channel =
- TvSingletons.getSingletons(context)
- .getChannelDataManager()
- .getChannel(recordedProgram.getChannelId());
- return createFromRecordedProgram(previewChannelId, recordedProgram, channel);
- }
-
- @VisibleForTesting
- static PreviewProgramContent createFromProgram(
- long previewChannelId, Program program, Channel channel) {
+ if (channel == null) {
+ return null;
+ }
String channelDisplayName = channel.getDisplayName();
return new PreviewProgramContent.Builder()
.setId(program.getId())
.setPreviewChannelId(previewChannelId)
- .setType(TvContractCompat.PreviewPrograms.TYPE_CHANNEL)
+ .setType(TvContract.PreviewPrograms.TYPE_CHANNEL)
.setLive(true)
.setTitle(program.getTitle())
.setDescription(
@@ -82,15 +68,22 @@ public class PreviewProgramContent {
.build();
}
- @VisibleForTesting
- static PreviewProgramContent createFromRecordedProgram(
- long previewChannelId, RecordedProgram recordedProgram, Channel channel) {
- String channelDisplayName = channel == null ? null : channel.getDisplayName();
- Uri recordedProgramUri = TvContractCompat.buildRecordedProgramUri(recordedProgram.getId());
+ /** 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(TvContractCompat.PreviewPrograms.TYPE_CLIP)
+ .setType(TvContract.PreviewPrograms.TYPE_CLIP)
.setTitle(recordedProgram.getTitle())
.setDescription(channelDisplayName != null ? channelDisplayName : "")
.setPosterArtUri(Uri.parse(recordedProgram.getPosterArtUri()))
diff --git a/src/com/android/tv/data/Program.java b/src/com/android/tv/data/Program.java
index 30a3033e..f47a3a06 100644
--- a/src/com/android/tv/data/Program.java
+++ b/src/com/android/tv/data/Program.java
@@ -33,9 +33,8 @@ import android.support.annotation.VisibleForTesting;
import android.text.TextUtils;
import android.util.Log;
import com.android.tv.common.BuildConfig;
+import com.android.tv.common.CollectionUtils;
import com.android.tv.common.TvContentRatingCache;
-import com.android.tv.common.util.CollectionUtils;
-import com.android.tv.common.util.CommonUtils;
import com.android.tv.util.ImageLoader;
import com.android.tv.util.Utils;
import java.io.Serializable;
@@ -129,7 +128,7 @@ public final class Program extends BaseProgram implements Comparable<Program>, P
builder.setEndTimeUtcMillis(cursor.getLong(index++));
builder.setVideoWidth((int) cursor.getLong(index++));
builder.setVideoHeight((int) cursor.getLong(index++));
- if (CommonUtils.isInBundledPackageSet(packageName)) {
+ if (Utils.isInBundledPackageSet(packageName)) {
InternalDataUtils.deserializeInternalProviderData(cursor.getBlob(index), builder);
}
index++;
@@ -476,9 +475,6 @@ public final class Program extends BaseProgram implements Comparable<Program>, P
public static ContentValues toContentValues(Program program) {
ContentValues values = new ContentValues();
values.put(TvContract.Programs.COLUMN_CHANNEL_ID, program.getChannelId());
- if (!TextUtils.isEmpty(program.getPackageName())) {
- values.put(Programs.COLUMN_PACKAGE_NAME, program.getPackageName());
- }
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) {
diff --git a/src/com/android/tv/data/ProgramDataManager.java b/src/com/android/tv/data/ProgramDataManager.java
index 3a7693a4..639ac99a 100644
--- a/src/com/android/tv/data/ProgramDataManager.java
+++ b/src/com/android/tv/data/ProgramDataManager.java
@@ -35,8 +35,8 @@ import android.util.LongSparseArray;
import android.util.LruCache;
import com.android.tv.common.MemoryManageable;
import com.android.tv.common.SoftPreconditions;
-import com.android.tv.common.util.Clock;
import com.android.tv.util.AsyncDbTask;
+import com.android.tv.util.Clock;
import com.android.tv.util.MultiLongSparseArray;
import com.android.tv.util.Utils;
import java.util.ArrayList;
@@ -52,7 +52,6 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
@MainThread
-@SuppressWarnings("TryWithResources") // TODO(b/62143348): remove when error prone check fixed
public class ProgramDataManager implements MemoryManageable {
private static final String TAG = "ProgramDataManager";
private static final boolean DEBUG = false;
diff --git a/src/com/android/tv/data/WatchedHistoryManager.java b/src/com/android/tv/data/WatchedHistoryManager.java
index 25ba7716..8c9756b0 100644
--- a/src/com/android/tv/data/WatchedHistoryManager.java
+++ b/src/com/android/tv/data/WatchedHistoryManager.java
@@ -1,18 +1,3 @@
-/*
- * 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;
@@ -27,7 +12,7 @@ import android.support.annotation.NonNull;
import android.support.annotation.VisibleForTesting;
import android.support.annotation.WorkerThread;
import android.util.Log;
-import com.android.tv.common.util.SharedPreferencesUtils;
+import com.android.tv.common.SharedPreferencesUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -43,7 +28,6 @@ import java.util.concurrent.TimeUnit;
*
* <p>Note that this class is not thread safe. Please use this on one thread.
*/
-@SuppressWarnings("TryWithResources") // TODO(b/62143348): remove when error prone check fixed
public class WatchedHistoryManager {
private static final String TAG = "WatchedHistoryManager";
private static final boolean DEBUG = false;
diff --git a/src/com/android/tv/data/epg/AutoValue_EpgReader_EpgChannel.java b/src/com/android/tv/data/epg/AutoValue_EpgReader_EpgChannel.java
deleted file mode 100644
index 90d109d7..00000000
--- a/src/com/android/tv/data/epg/AutoValue_EpgReader_EpgChannel.java
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF 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 com.android.tv.data.Channel;
-
-/**
- * Hand copy of generated Autovalue class.
- *
- * TODO get autovalue working
- */
-final class AutoValue_EpgReader_EpgChannel extends EpgReader.EpgChannel {
-
- private final Channel channel;
- private final String epgChannelId;
-
- AutoValue_EpgReader_EpgChannel(
- Channel channel,
- String epgChannelId) {
- if (channel == null) {
- throw new NullPointerException("Null channel");
- }
- this.channel = channel;
- if (epgChannelId == null) {
- throw new NullPointerException("Null epgChannelId");
- }
- this.epgChannelId = epgChannelId;
- }
-
- @Override
- public Channel getChannel() {
- return channel;
- }
-
- @Override
- public String getEpgChannelId() {
- return epgChannelId;
- }
-
- @Override
- public String toString() {
- return "EpgChannel{"
- + "channel=" + channel + ", "
- + "epgChannelId=" + epgChannelId
- + "}";
- }
-
- @Override
- public boolean equals(Object o) {
- if (o == this) {
- return true;
- }
- if (o instanceof EpgReader.EpgChannel) {
- EpgReader.EpgChannel that = (EpgReader.EpgChannel) o;
- return (this.channel.equals(that.getChannel()))
- && (this.epgChannelId.equals(that.getEpgChannelId()));
- }
- return false;
- }
-
- @Override
- public int hashCode() {
- int h = 1;
- h *= 1000003;
- h ^= this.channel.hashCode();
- h *= 1000003;
- h ^= this.epgChannelId.hashCode();
- return h;
- }
-
-}
-
diff --git a/src/com/android/tv/data/epg/EpgFetchHelper.java b/src/com/android/tv/data/epg/EpgFetchHelper.java
index 30123ee5..89d5f494 100644
--- a/src/com/android/tv/data/epg/EpgFetchHelper.java
+++ b/src/com/android/tv/data/epg/EpgFetchHelper.java
@@ -27,15 +27,13 @@ import android.preference.PreferenceManager;
import android.support.annotation.WorkerThread;
import android.text.TextUtils;
import android.util.Log;
-import com.android.tv.common.util.Clock;
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 EpgFetcher} */
-@SuppressWarnings("TryWithResources") // TODO(b/62143348): remove when error prone check fixed
+/** 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;
@@ -66,14 +64,13 @@ class EpgFetchHelper {
* @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, Clock clock, long channelId, List<Program> fetchedPrograms) {
+ 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 = clock.currentTimeMillis();
+ long startTimeMs = System.currentTimeMillis();
long endTimeMs = startTimeMs + PROGRAM_QUERY_DURATION_MS;
List<Program> oldPrograms = queryPrograms(context, channelId, startTimeMs, endTimeMs);
int oldProgramsIndex = 0;
diff --git a/src/com/android/tv/data/epg/EpgFetchService.java b/src/com/android/tv/data/epg/EpgFetchService.java
deleted file mode 100644
index aa4f3588..00000000
--- a/src/com/android/tv/data/epg/EpgFetchService.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * 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.app.job.JobParameters;
-import android.app.job.JobService;
-import com.android.tv.Starter;
-import com.android.tv.TvSingletons;
-import com.android.tv.data.ChannelDataManager;
-
-/** JobService to Fetch EPG data. */
-public class EpgFetchService extends JobService {
- private EpgFetcher mEpgFetcher;
- private ChannelDataManager mChannelDataManager;
-
- @Override
- public void onCreate() {
- super.onCreate();
- Starter.start(this);
- TvSingletons tvSingletons = TvSingletons.getSingletons(getApplicationContext());
- mEpgFetcher = tvSingletons.getEpgFetcher();
- mChannelDataManager = tvSingletons.getChannelDataManager();
- }
-
- @Override
- public boolean onStartJob(JobParameters params) {
- if (!mChannelDataManager.isDbLoadFinished()) {
- mChannelDataManager.addListener(
- new ChannelDataManager.Listener() {
- @Override
- public void onLoadFinished() {
- mChannelDataManager.removeListener(this);
- if (!mEpgFetcher.executeFetchTaskIfPossible(
- EpgFetchService.this, params)) {
- jobFinished(params, false);
- }
- }
-
- @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;
- }
-}
diff --git a/src/com/android/tv/data/epg/EpgFetcher.java b/src/com/android/tv/data/epg/EpgFetcher.java
index 9c24613d..b10bdc1b 100644
--- a/src/com/android/tv/data/epg/EpgFetcher.java
+++ b/src/com/android/tv/data/epg/EpgFetcher.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2017 The Android Open Source Project
+ * 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.
@@ -16,13 +16,138 @@
package com.android.tv.data.epg;
+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.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.support.annotation.AnyThread;
import android.support.annotation.MainThread;
+import android.support.annotation.Nullable;
+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.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.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.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.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
-/** Fetch EPG routinely or on-demand during channel scanning */
-public interface EpgFetcher {
+/**
+ * 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 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 FETCH_DURING_SCAN_WAIT_TIME_MS = TimeUnit.SECONDS.toMillis(10);
+
+ 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 int DEFAULT_ROUTINE_INTERVAL_HOUR = 4;
+ private static final String KEY_ROUTINE_INTERVAL = "live_channels_epg_fetcher_interval_hour";
+
+ 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 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);
+ }
+ return sInstance;
+ }
+
+ /** Creates and returns {@link EpgReader}. */
+ public static EpgReader createEpgReader(Context context, String region) {
+ return new StubEpgReader(context);
+ }
+
+ private EpgFetcher(Context context) {
+ 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
@@ -30,30 +155,590 @@ public interface EpgFetcher {
* channel scanning of tuner input is started.
*/
@MainThread
- void startRoutineService();
+ 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
- void fetchImmediatelyIfNeeded();
+ public void fetchImmediatelyIfNeeded() {
+ if (TvCommonUtils.isRunningInTest()) {
+ // Do not run EpgFetcher in test.
+ return;
+ }
+ new AsyncTask<Void, Void, Long>() {
+ @Override
+ protected Long doInBackground(Void... args) {
+ return EpgFetchHelper.getLastEpgUpdatedTimestamp(mContext);
+ }
+
+ @Override
+ 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);
+ }
/** Fetches EPG immediately. */
@MainThread
- void fetchImmediately();
+ public void fetchImmediately() {
+ if (!mChannelDataManager.isDbLoadFinished()) {
+ mChannelDataManager.addListener(
+ new ChannelDataManager.Listener() {
+ @Override
+ public void onLoadFinished() {
+ mChannelDataManager.removeListener(this);
+ executeFetchTaskIfPossible(null, null);
+ }
+
+ @Override
+ public void onChannelListUpdated() {}
+
+ @Override
+ public void onChannelBrowsableChanged() {}
+ });
+ } else {
+ executeFetchTaskIfPossible(null, null);
+ }
+ }
/** Notifies EPG fetch service that channel scanning is started. */
@MainThread
- void onChannelScanStarted();
+ public void onChannelScanStarted() {
+ if (mScanStarted || !Features.ENABLE_CLOUD_EPG_REGION.isEnabled(mContext)) {
+ return;
+ }
+ mScanStarted = true;
+ stopFetchingJob();
+ synchronized (mFetchDuringScanHandlerLock) {
+ if (mFetchDuringScanHandler == null) {
+ HandlerThread thread = new HandlerThread("EpgFetchDuringScan");
+ thread.start();
+ mFetchDuringScanHandler = new FetchDuringScanHandler(thread.getLooper());
+ }
+ mFetchDuringScanHandler.sendEmptyMessage(MSG_PREPARE_FETCH_DURING_SCAN);
+ }
+ Log.i(TAG, "EPG fetching on channel scanning started.");
+ }
/** Notifies EPG fetch service that channel scanning is finished. */
@MainThread
- void onChannelScanFinished();
+ public void onChannelScanFinished() {
+ if (!mScanStarted) {
+ return;
+ }
+ mScanStarted = false;
+ mFetchDuringScanHandler.sendEmptyMessage(MSG_FINISH_FETCH_DURING_SCAN);
+ }
@MainThread
- boolean executeFetchTaskIfPossible(JobService jobService, JobParameters params);
+ 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.");
+ }
+ }
@MainThread
- void stopFetchingJob();
+ 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;
+ }
+
+ @MainThread
+ 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;
+ }
+ if (!TextUtils.isEmpty(EpgFetchHelper.getLastLineupId(mContext))) {
+ return true;
+ }
+ if (!TextUtils.isEmpty(PostalCodeUtils.getLastPostalCode(mContext))) {
+ return true;
+ }
+ return true;
+ }
+
+ @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;
+ }
+
+ @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;
+ }
+ }
+
+ @WorkerThread
+ private Integer prepareFetchEpg(boolean forceUpdatePossibleLineups) {
+ if (!mEpgReader.isAvailable()) {
+ Log.i(TAG, "EPG reader is temporarily unavailable.");
+ return REASON_EPG_READER_NOT_READY;
+ }
+ // 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;
+ }
+ } 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;
+ }
+ } 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;
+ }
+
+ @WorkerThread
+ private void batchFetchEpg(List<Channel> channels, long durationSec) {
+ Log.i(TAG, "Start batch fetching (" + durationSec + ")...." + channels.size());
+ if (channels.size() == 0) {
+ return;
+ }
+ List<Long> queryChannelIds = new ArrayList<>(QUERY_CHANNEL_COUNT);
+ for (Channel channel : channels) {
+ queryChannelIds.add(channel.getId());
+ if (queryChannelIds.size() >= QUERY_CHANNEL_COUNT) {
+ batchUpdateEpg(mEpgReader.getPrograms(queryChannelIds, durationSec));
+ queryChannelIds.clear();
+ }
+ }
+ if (!queryChannelIds.isEmpty()) {
+ batchUpdateEpg(mEpgReader.getPrograms(queryChannelIds, durationSec));
+ }
+ }
+
+ @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);
+ }
+ }
+
+ @Nullable
+ @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 maxLineupId;
+ }
+
+ @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());
+ }
+ }
+ numbers.retainAll(mEpgReader.getChannelNumbers(lineupId));
+ return numbers.size();
+ }
+
+ public static class EpgFetchService extends JobService {
+ private EpgFetcher mEpgFetcher;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ TvApplication.setCurrentRunningProcess(this, true);
+ mEpgFetcher = EpgFetcher.getInstance(this);
+ }
+
+ @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);
+ }
+ }
+
+ @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;
+ }
+ }
+
+ 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;
+ }
+
+ @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 {
+ Log.i(TAG, "Failed to get lineup id");
+ return REASON_NO_EPG_DATA_RETURNED;
+ }
+ 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;
+ }
+ 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);
+ }
+ EpgFetchHelper.setLastEpgUpdatedTimestamp(mContext, mEpgTimeStamp);
+ if (DEBUG) Log.d(TAG, "Fetching EPG is finished.");
+ return null;
+ } finally {
+ TrafficStats.setThreadStatsTag(oldTag);
+ }
+ }
+
+ @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);
+ }
+ mPerformanceMonitor.stopTimer(mTimerEvent, EventNames.FETCH_EPG_TASK);
+ mPerformanceMonitor.recordMemory(EventNames.FETCH_EPG_TASK);
+ }
+
+ @Override
+ protected void onCancelled(Integer failureReason) {
+ clearUnusedLineups(null);
+ jobFinished(false);
+ }
+
+ private void jobFinished(boolean reschedule) {
+ if (mService != null && mParams != null) {
+ // Task is executed from JobService, need to report jobFinished.
+ mService.jobFinished(mParams, reschedule);
+ }
+ }
+ }
+
+ @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
+ }
+ };
+
+ @AnyThread
+ private FetchDuringScanHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_PREPARE_FETCH_DURING_SCAN:
+ case MSG_RETRY_PREPARE_FETCH_DURING_SCAN:
+ onPrepareFetchDuringScan();
+ break;
+ 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 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/EpgFetcherImpl.java b/src/com/android/tv/data/epg/EpgFetcherImpl.java
deleted file mode 100644
index 523fc50c..00000000
--- a/src/com/android/tv/data/epg/EpgFetcherImpl.java
+++ /dev/null
@@ -1,814 +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.data.epg;
-
-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.database.Cursor;
-import android.media.tv.TvContract;
-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.support.annotation.AnyThread;
-import android.support.annotation.MainThread;
-import android.support.annotation.Nullable;
-import android.support.annotation.VisibleForTesting;
-import android.support.annotation.WorkerThread;
-import android.text.TextUtils;
-import android.util.Log;
-import com.android.tv.TvFeatures;
-import com.android.tv.TvSingletons;
-import com.android.tv.common.BuildConfig;
-import com.android.tv.common.SoftPreconditions;
-import com.android.tv.common.config.RemoteConfigUtils;
-import com.android.tv.common.util.Clock;
-import com.android.tv.common.util.CommonUtils;
-import com.android.tv.common.util.LocationUtils;
-import com.android.tv.common.util.NetworkTrafficTags;
-import com.android.tv.common.util.PermissionUtils;
-import com.android.tv.common.util.PostalCodeUtils;
-import com.android.tv.data.Channel;
-import com.android.tv.data.ChannelDataManager;
-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.util.Utils;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.TimeUnit;
-
-/**
- * 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}.
- */
-@SuppressWarnings("TryWithResources") // TODO(b/62143348): remove when error prone check fixed
-public class EpgFetcherImpl implements EpgFetcher {
- private static final String TAG = "EpgFetcherImpl";
- private static final boolean DEBUG = false;
-
- private static final int EPG_ROUTINELY_FETCHING_JOB_ID = 101;
-
- private static final long INITIAL_BACKOFF_MS = TimeUnit.SECONDS.toMillis(10);
-
- @VisibleForTesting static final int REASON_EPG_READER_NOT_READY = 1;
- @VisibleForTesting static final int REASON_LOCATION_INFO_UNAVAILABLE = 2;
- @VisibleForTesting static final int REASON_LOCATION_PERMISSION_NOT_GRANTED = 3;
- @VisibleForTesting static final int REASON_NO_EPG_DATA_RETURNED = 4;
- @VisibleForTesting static final int REASON_NO_NEW_EPG = 5;
- @VisibleForTesting static final int REASON_ERROR = 6;
- @VisibleForTesting static final int REASON_CLOUD_EPG_FAILURE = 7;
- @VisibleForTesting static final int REASON_NO_BUILT_IN_CHANNELS = 8;
-
- private static final long FETCH_DURING_SCAN_WAIT_TIME_MS = TimeUnit.SECONDS.toMillis(10);
-
- 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 int DEFAULT_ROUTINE_INTERVAL_HOUR = 4;
- private static final String KEY_ROUTINE_INTERVAL = "live_channels_epg_fetcher_interval_hour";
-
- 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 final Context mContext;
- private final ChannelDataManager mChannelDataManager;
- private final EpgReader mEpgReader;
- 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;
- private Clock mClock;
-
- public static EpgFetcher create(Context context) {
- context = context.getApplicationContext();
- TvSingletons tvSingletons = TvSingletons.getSingletons(context);
- ChannelDataManager channelDataManager = tvSingletons.getChannelDataManager();
- PerformanceMonitor performanceMonitor = tvSingletons.getPerformanceMonitor();
- EpgReader epgReader = tvSingletons.providesEpgReader().get();
- Clock clock = tvSingletons.getClock();
- int routineIntervalMs =
- (int)
- RemoteConfigUtils.getRemoteConfig(
- context, KEY_ROUTINE_INTERVAL, DEFAULT_ROUTINE_INTERVAL_HOUR);
-
- return new EpgFetcherImpl(
- context,
- channelDataManager,
- epgReader,
- performanceMonitor,
- clock,
- routineIntervalMs);
- }
-
- @VisibleForTesting
- EpgFetcherImpl(
- Context context,
- ChannelDataManager channelDataManager,
- EpgReader epgReader,
- PerformanceMonitor performanceMonitor,
- Clock clock,
- long routineIntervalMs) {
- mContext = context;
- mChannelDataManager = channelDataManager;
- mEpgReader = epgReader;
- mPerformanceMonitor = performanceMonitor;
- mClock = clock;
- mRoutineIntervalMs =
- routineIntervalMs <= 0
- ? TimeUnit.HOURS.toMillis(DEFAULT_ROUTINE_INTERVAL_HOUR)
- : TimeUnit.HOURS.toMillis(routineIntervalMs);
- mEpgDataExpiredTimeLimitMs = routineIntervalMs * 2;
- mFastFetchDurationSec = FAST_FETCH_DURATION_SEC + routineIntervalMs / 1000;
- }
-
- private static Set<Channel> getExistingChannelsForMyPackage(Context context) {
- HashSet<Channel> channels = new HashSet<>();
- String selection = null;
- String[] selectionArgs = null;
- String myPackageName = context.getPackageName();
- if (PermissionUtils.hasAccessAllEpg(context)) {
- selection = "package_name=?";
- selectionArgs = new String[] {myPackageName};
- }
- try (Cursor c =
- context.getContentResolver()
- .query(
- TvContract.Channels.CONTENT_URI,
- Channel.PROJECTION,
- selection,
- selectionArgs,
- null)) {
- if (c != null) {
- while (c.moveToNext()) {
- Channel channel = Channel.fromCursor(c);
- if (DEBUG) Log.d(TAG, "Found " + channel);
- if (myPackageName.equals(channel.getPackageName())) {
- channels.add(channel);
- }
- }
- }
- }
- if (DEBUG)
- Log.d(TAG, "Found " + channels.size() + " channels for package " + myPackageName);
- return channels;
- }
-
- @Override
- @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.");
- }
-
- @Override
- @MainThread
- public void fetchImmediatelyIfNeeded() {
- if (CommonUtils.isRunningInTest()) {
- // Do not run EpgFetcher in test.
- return;
- }
- new AsyncTask<Void, Void, Long>() {
- @Override
- protected Long doInBackground(Void... args) {
- return EpgFetchHelper.getLastEpgUpdatedTimestamp(mContext);
- }
-
- @Override
- protected void onPostExecute(Long result) {
- if (mClock.currentTimeMillis() - EpgFetchHelper.getLastEpgUpdatedTimestamp(mContext)
- > mEpgDataExpiredTimeLimitMs) {
- Log.i(TAG, "EPG data expired. Start fetching immediately.");
- fetchImmediately();
- }
- }
- }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
- }
-
- @Override
- @MainThread
- public void fetchImmediately() {
- if (DEBUG) Log.d(TAG, "fetchImmediately");
- if (!mChannelDataManager.isDbLoadFinished()) {
- mChannelDataManager.addListener(
- new ChannelDataManager.Listener() {
- @Override
- public void onLoadFinished() {
- mChannelDataManager.removeListener(this);
- executeFetchTaskIfPossible(null, null);
- }
-
- @Override
- public void onChannelListUpdated() {}
-
- @Override
- public void onChannelBrowsableChanged() {}
- });
- } else {
- executeFetchTaskIfPossible(null, null);
- }
- }
-
- @Override
- @MainThread
- public void onChannelScanStarted() {
- if (mScanStarted || !TvFeatures.ENABLE_CLOUD_EPG_REGION.isEnabled(mContext)) {
- return;
- }
- mScanStarted = true;
- stopFetchingJob();
- synchronized (mFetchDuringScanHandlerLock) {
- if (mFetchDuringScanHandler == null) {
- HandlerThread thread = new HandlerThread("EpgFetchDuringScan");
- thread.start();
- mFetchDuringScanHandler = new FetchDuringScanHandler(thread.getLooper());
- }
- mFetchDuringScanHandler.sendEmptyMessage(MSG_PREPARE_FETCH_DURING_SCAN);
- }
- Log.i(TAG, "EPG fetching on channel scanning started.");
- }
-
- @Override
- @MainThread
- public void onChannelScanFinished() {
- if (!mScanStarted) {
- return;
- }
- mScanStarted = false;
- mFetchDuringScanHandler.sendEmptyMessage(MSG_FINISH_FETCH_DURING_SCAN);
- }
-
- @MainThread
- @Override
- public 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.");
- }
- }
-
- @MainThread
- @Override
- public boolean executeFetchTaskIfPossible(JobService service, JobParameters params) {
- if (DEBUG) Log.d(TAG, "executeFetchTaskIfPossible");
- SoftPreconditions.checkState(mChannelDataManager.isDbLoadFinished());
- if (!CommonUtils.isRunningInTest() && checkFetchPrerequisite()) {
- mFetchTask = createFetchTask(service, params);
- mFetchTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
- return true;
- }
- return false;
- }
-
- @VisibleForTesting
- FetchAsyncTask createFetchTask(JobService service, JobParameters params) {
- return new FetchAsyncTask(service, params);
- }
-
- @MainThread
- private boolean checkFetchPrerequisite() {
- if (DEBUG) Log.d(TAG, "Check prerequisite of routinely fetching job.");
- if (!TvFeatures.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;
- }
- return true;
- }
-
- @MainThread
- private int getTunerChannelCount() {
- for (TvInputInfo input :
- TvSingletons.getSingletons(mContext)
- .getTvInputManagerHelper()
- .getTvInputInfos(true, true)) {
- String inputId = input.getId();
- if (Utils.isInternalTvInput(mContext, inputId)) {
- return mChannelDataManager.getChannelCountForInput(inputId);
- }
- }
- return 0;
- }
-
- @AnyThread
- private void clearUnusedLineups(@Nullable String lineupId) {
- synchronized (mPossibleLineupsLock) {
- if (mPossibleLineups == null) {
- return;
- }
- for (Lineup lineup : mPossibleLineups) {
- if (!TextUtils.equals(lineupId, lineup.getId())) {
- mEpgReader.clearCachedChannels(lineup.getId());
- }
- }
- mPossibleLineups = null;
- }
- }
-
- @WorkerThread
- private Integer prepareFetchEpg(boolean forceUpdatePossibleLineups) {
- if (!mEpgReader.isAvailable()) {
- Log.i(TAG, "EPG reader is temporarily unavailable.");
- return REASON_EPG_READER_NOT_READY;
- }
- // 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;
- }
- } 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;
- }
- } 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.
- String lastPostalCode = PostalCodeUtils.getLastPostalCode(mContext);
- List<Lineup> possibleLineups = mEpgReader.getLineups(lastPostalCode);
- if (possibleLineups.isEmpty()) {
- Log.i(TAG, "No lineups found for " + lastPostalCode);
- return REASON_NO_EPG_DATA_RETURNED;
- }
- for (Lineup lineup : possibleLineups) {
- mEpgReader.preloadChannels(lineup.getId());
- }
- synchronized (mPossibleLineupsLock) {
- mPossibleLineups = possibleLineups;
- }
- EpgFetchHelper.setLastLineupId(mContext, null);
- }
- return null;
- }
-
- @WorkerThread
- private void batchFetchEpg(Set<EpgReader.EpgChannel> epgChannels, long durationSec) {
- Log.i(TAG, "Start batch fetching (" + durationSec + ")...." + epgChannels.size());
- if (epgChannels.size() == 0) {
- return;
- }
- Set<EpgReader.EpgChannel> batch = new HashSet<>(QUERY_CHANNEL_COUNT);
- for (EpgReader.EpgChannel epgChannel : epgChannels) {
- batch.add(epgChannel);
- if (batch.size() >= QUERY_CHANNEL_COUNT) {
- batchUpdateEpg(mEpgReader.getPrograms(batch, durationSec));
- batch.clear();
- }
- }
- if (!batch.isEmpty()) {
- batchUpdateEpg(mEpgReader.getPrograms(batch, durationSec));
- }
- }
-
- @WorkerThread
- private void batchUpdateEpg(Map<EpgReader.EpgChannel, Collection<Program>> allPrograms) {
- for (Map.Entry<EpgReader.EpgChannel, Collection<Program>> entry : allPrograms.entrySet()) {
- List<Program> programs = new ArrayList(entry.getValue());
- if (programs == null) {
- continue;
- }
- Collections.sort(programs);
- Log.i(
- TAG,
- "Batch fetched " + programs.size() + " programs for channel " + entry.getKey());
- EpgFetchHelper.updateEpgData(
- mContext, mClock, entry.getKey().getChannel().getId(), programs);
- }
- }
-
- @Nullable
- @WorkerThread
- private String pickBestLineupId(Set<Channel> currentChannels) {
- String maxLineupId = null;
- synchronized (mPossibleLineupsLock) {
- if (mPossibleLineups == null) {
- return null;
- }
- int maxCount = 0;
- for (Lineup lineup : mPossibleLineups) {
- int count = getMatchedChannelCount(lineup.getId(), currentChannels);
- Log.i(TAG, lineup.getName() + " (" + lineup.getId() + ") - " + count + " matches");
- if (count > maxCount) {
- maxCount = count;
- maxLineupId = lineup.getId();
- }
- }
- }
- return maxLineupId;
- }
-
- @WorkerThread
- private int getMatchedChannelCount(String lineupId, Set<Channel> currentChannels) {
- // Construct a list of display numbers for existing channels.
- if (currentChannels.isEmpty()) {
- if (DEBUG) Log.d(TAG, "No existing channel to compare");
- return 0;
- }
- List<String> numbers = new ArrayList<>(currentChannels.size());
- for (Channel channel : currentChannels) {
- // We only support channels from internal tuner inputs.
- if (Utils.isInternalTvInput(mContext, channel.getInputId())) {
- numbers.add(channel.getDisplayNumber());
- }
- }
- numbers.retainAll(mEpgReader.getChannelNumbers(lineupId));
- return numbers.size();
- }
-
- @VisibleForTesting
- class FetchAsyncTask extends AsyncTask<Void, Void, Integer> {
- private final JobService mService;
- private final JobParameters mParams;
- private Set<Channel> mCurrentChannels;
- private TimerEvent mTimerEvent;
-
- private FetchAsyncTask(JobService service, JobParameters params) {
- mService = service;
- mParams = params;
- }
-
- @Override
- protected void onPreExecute() {
- mTimerEvent = mPerformanceMonitor.startTimer();
- mCurrentChannels = new HashSet<>(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 builtInResult = fetchEpgForBuiltInTuner();
- boolean anyCloudEpgFailure = false;
- boolean anyCloudEpgSuccess = false;
- return builtInResult;
- } finally {
- TrafficStats.setThreadStatsTag(oldTag);
- }
- }
-
- private Set<Channel> getExistingChannelsFor(String inputId) {
- Set<Channel> result = new HashSet<>();
- try (Cursor cursor =
- mContext.getContentResolver()
- .query(
- TvContract.buildChannelsUriForInput(inputId),
- Channel.PROJECTION,
- null,
- null,
- null)) {
- while (cursor.moveToNext()) {
- result.add(Channel.fromCursor(cursor));
- }
- return result;
- }
- }
-
- private Integer fetchEpgForBuiltInTuner() {
- try {
- 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(mCurrentChannels) : 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 {
- Log.i(TAG, "Failed to get lineup id");
- return REASON_NO_EPG_DATA_RETURNED;
- }
- Set<Channel> existingChannelsForMyPackage =
- getExistingChannelsForMyPackage(mContext);
- if (existingChannelsForMyPackage.isEmpty()) {
- return REASON_NO_BUILT_IN_CHANNELS;
- }
- return fetchEpgFor(lineupId, existingChannelsForMyPackage);
- } catch (Exception e) {
- Log.w(TAG, "Failed to update EPG for builtin tuner", e);
- return REASON_ERROR;
- }
- }
-
- @Nullable
- private Integer fetchEpgFor(String lineupId, Set<Channel> existingChannels) {
- if (DEBUG) {
- Log.d(
- TAG,
- "Starting Fetching EPG is for "
- + lineupId
- + " with channelCount "
- + existingChannels.size());
- }
- final Set<EpgReader.EpgChannel> channels =
- mEpgReader.getChannels(existingChannels, 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 for " + lineupId);
- return REASON_NO_EPG_DATA_RETURNED;
- }
- if (mClock.currentTimeMillis() - EpgFetchHelper.getLastEpgUpdatedTimestamp(mContext)
- > mEpgDataExpiredTimeLimitMs) {
- batchFetchEpg(channels, mFastFetchDurationSec);
- }
- new Handler(mContext.getMainLooper())
- .post(
- new Runnable() {
- @Override
- public void run() {
- ChannelLogoFetcher.startFetchingChannelLogos(
- mContext, asChannelList(channels));
- }
- });
- for (EpgReader.EpgChannel epgChannel : channels) {
- if (this.isCancelled()) {
- return null;
- }
- List<Program> programs = new ArrayList<>(mEpgReader.getPrograms(epgChannel));
- // InterruptedException might be caught by RPC, we should check it here.
- Collections.sort(programs);
- Log.i(
- TAG,
- "Fetched "
- + programs.size()
- + " programs for channel "
- + epgChannel.getChannel());
- EpgFetchHelper.updateEpgData(
- mContext, mClock, epgChannel.getChannel().getId(), programs);
- }
- EpgFetchHelper.setLastEpgUpdatedTimestamp(mContext, mEpgTimeStamp);
- if (DEBUG) Log.d(TAG, "Fetching EPG is for " + lineupId);
- return null;
- }
-
- @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);
- }
- mPerformanceMonitor.stopTimer(mTimerEvent, EventNames.FETCH_EPG_TASK);
- mPerformanceMonitor.recordMemory(EventNames.FETCH_EPG_TASK);
- }
-
- @Override
- protected void onCancelled(Integer failureReason) {
- clearUnusedLineups(null);
- jobFinished(false);
- }
-
- private void jobFinished(boolean reschedule) {
- if (mService != null && mParams != null) {
- // Task is executed from JobService, need to report jobFinished.
- mService.jobFinished(mParams, reschedule);
- }
- }
- }
-
- private List<Channel> asChannelList(Set<EpgReader.EpgChannel> epgChannels) {
- List<Channel> result = new ArrayList<>(epgChannels.size());
- for (EpgReader.EpgChannel epgChannel : epgChannels) {
- result.add(epgChannel.getChannel());
- }
- return result;
- }
-
- @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,
- getExistingChannelsForMyPackage(mContext))
- .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,
- getExistingChannelsForMyPackage(mContext))
- .sendToTarget();
- }
- }
-
- @Override
- public void onChannelBrowsableChanged() {
- // Do nothing
- }
- };
-
- @AnyThread
- private FetchDuringScanHandler(Looper looper) {
- super(looper);
- }
-
- @Override
- public void handleMessage(Message msg) {
- switch (msg.what) {
- case MSG_PREPARE_FETCH_DURING_SCAN:
- case MSG_RETRY_PREPARE_FETCH_DURING_SCAN:
- onPrepareFetchDuringScan();
- break;
- case MSG_CHANNEL_UPDATED_DURING_SCAN:
- if (!hasMessages(MSG_CHANNEL_UPDATED_DURING_SCAN)) {
- onChannelUpdatedDuringScan((Set<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;
- default:
- // do nothing
- }
- }
-
- 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(Set<Channel> currentChannels) {
- String lineupId = pickBestLineupId(currentChannels);
- 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 : currentChannels) {
- currentChannelIds.add(channel.getId());
- }
- mFetchedChannelIdsDuringScan.retainAll(currentChannelIds);
- Set<EpgReader.EpgChannel> newChannels = new HashSet<>();
- for (EpgReader.EpgChannel epgChannel :
- mEpgReader.getChannels(currentChannels, mPossibleLineupId)) {
- if (!mFetchedChannelIdsDuringScan.contains(epgChannel.getChannel().getId())) {
- newChannels.add(epgChannel);
- mFetchedChannelIdsDuringScan.add(epgChannel.getChannel().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/EpgInputWhiteList.java b/src/com/android/tv/data/epg/EpgInputWhiteList.java
deleted file mode 100644
index de0478fc..00000000
--- a/src/com/android/tv/data/epg/EpgInputWhiteList.java
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * 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.support.annotation.Nullable;
-import android.support.annotation.VisibleForTesting;
-import android.text.TextUtils;
-import android.util.Log;
-import com.android.tv.common.BuildConfig;
-import com.android.tv.common.config.api.RemoteConfig;
-import com.android.tv.common.experiments.Experiments;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.Set;
-
-/** Checks if a package or a input is white listed. */
-public final class EpgInputWhiteList {
- private static final boolean DEBUG = false;
- private static final String TAG = "EpgInputWhiteList";
- @VisibleForTesting public static final String KEY = "live_channels_3rd_party_epg_inputs";
- private static final String QA_DEV_INPUTS =
- "com.example.partnersupportsampletvinput/.SampleTvInputService";
-
- /** Returns the package portion of a inputId */
- @Nullable
- public static String getPackageFromInput(@Nullable String inputId) {
- return inputId == null ? null : inputId.substring(0, inputId.indexOf("/"));
- }
-
- private final RemoteConfig remoteConfig;
-
- public EpgInputWhiteList(RemoteConfig remoteConfig) {
- this.remoteConfig = remoteConfig;
- }
-
- public boolean isInputWhiteListed(String inputId) {
- return getWhiteListedInputs().contains(inputId);
- }
-
- public boolean isPackageWhiteListed(String packageName) {
- if (DEBUG) Log.d(TAG, "isPackageWhiteListed " + packageName);
- Set<String> whiteList = getWhiteListedInputs();
- for (String good : whiteList) {
- try {
- String goodPackage = getPackageFromInput(good);
- if (goodPackage.equals(packageName)) {
- return true;
- }
- } catch (Exception e) {
- if (DEBUG) Log.d(TAG, "Error parsing package name of " + good, e);
- continue;
- }
- }
- return false;
- }
-
- private Set<String> getWhiteListedInputs() {
- Set<String> result = toInputSet(remoteConfig.getString(KEY));
- if (BuildConfig.ENG || Experiments.ENABLE_QA_FEATURES.get()) {
- HashSet<String> moreInputs = new HashSet<>(toInputSet(QA_DEV_INPUTS));
- if (result.isEmpty()) {
- result = moreInputs;
- } else {
- result.addAll(moreInputs);
- }
- }
- if (DEBUG) Log.d(TAG, "getWhiteListedInputs " + result);
- return result;
- }
-
- private Set<String> toInputSet(String value) {
- if (TextUtils.isEmpty(value)) {
- return Collections.EMPTY_SET;
- }
- return new HashSet(Arrays.asList(value.split(",")));
- }
-}
diff --git a/src/com/android/tv/data/epg/EpgReader.java b/src/com/android/tv/data/epg/EpgReader.java
index 9c881439..d10a852c 100644
--- a/src/com/android/tv/data/epg/EpgReader.java
+++ b/src/com/android/tv/data/epg/EpgReader.java
@@ -23,27 +23,12 @@ import com.android.tv.data.Channel;
import com.android.tv.data.Lineup;
import com.android.tv.data.Program;
import com.android.tv.dvr.data.SeriesInfo;
-import java.util.Collection;
import java.util.List;
import java.util.Map;
-import java.util.Set;
/** An interface used to retrieve the EPG data. This class should be used in worker thread. */
@WorkerThread
public interface EpgReader {
-
- /** Value class that holds a EpgChannelId and its corresponding Channel */
- // TODO(b/72052568): Get autovalue to work in aosp
- abstract class EpgChannel {
- public static EpgChannel createEpgChannel(Channel channel, String epgChannelId) {
- return new AutoValue_EpgReader_EpgChannel(channel, epgChannelId);
- }
-
- public abstract Channel getChannel();
-
- public abstract String getEpgChannelId();
- }
-
/** Checks if the reader is available. */
boolean isAvailable();
@@ -70,7 +55,7 @@ public interface EpgReader {
* 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.
*/
- Set<EpgChannel> getChannels(Set<Channel> inputChannels, @NonNull String lineupId);
+ List<Channel> getChannels(@NonNull String lineupId);
/** Pre-loads and caches channels for a given lineup. */
void preloadChannels(@NonNull String lineupId);
@@ -80,19 +65,18 @@ public interface EpgReader {
void clearCachedChannels(@NonNull String lineupId);
/**
- * Returns the programs for the given channel. Must call {@link #getChannels(Set, String)}
+ * 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(EpgChannel epgChannel);
+ List<Program> getPrograms(long channelId);
/**
* 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.
*/
- Map<EpgChannel, Collection<Program>> getPrograms(
- @NonNull Set<EpgChannel> epgChannels, long duration);
+ 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);
diff --git a/src/com/android/tv/data/epg/StubEpgReader.java b/src/com/android/tv/data/epg/StubEpgReader.java
index 9a87619d..49409a1d 100644
--- a/src/com/android/tv/data/epg/StubEpgReader.java
+++ b/src/com/android/tv/data/epg/StubEpgReader.java
@@ -22,11 +22,9 @@ import com.android.tv.data.Channel;
import com.android.tv.data.Lineup;
import com.android.tv.data.Program;
import com.android.tv.dvr.data.SeriesInfo;
-import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
-import java.util.Set;
/** A stub class to read EPG. */
public class StubEpgReader implements EpgReader {
@@ -58,8 +56,8 @@ public class StubEpgReader implements EpgReader {
}
@Override
- public Set<EpgChannel> getChannels(Set<Channel> inputChannels, @NonNull String lineupId) {
- return Collections.emptySet();
+ public List<Channel> getChannels(@NonNull String lineupId) {
+ return Collections.emptyList();
}
@Override
@@ -73,13 +71,12 @@ public class StubEpgReader implements EpgReader {
}
@Override
- public List<Program> getPrograms(EpgChannel epgChannel) {
+ public List<Program> getPrograms(long channelId) {
return Collections.emptyList();
}
@Override
- public Map<EpgChannel, Collection<Program>> getPrograms(
- @NonNull Set<EpgChannel> channels, long duration) {
+ public Map<Long, List<Program>> getPrograms(@NonNull List<Long> channelIds, long duration) {
return Collections.emptyMap();
}
diff --git a/src/com/android/tv/dialog/DvrHistoryDialogFragment.java b/src/com/android/tv/dialog/DvrHistoryDialogFragment.java
index 173a2891..442a663d 100644
--- a/src/com/android/tv/dialog/DvrHistoryDialogFragment.java
+++ b/src/com/android/tv/dialog/DvrHistoryDialogFragment.java
@@ -30,8 +30,9 @@ 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.TvSingletons;
+import com.android.tv.TvApplication;
import com.android.tv.data.Channel;
import com.android.tv.data.ChannelDataManager;
import com.android.tv.dvr.DvrDataManager;
@@ -44,7 +45,6 @@ import java.util.List;
/** Displays the DVR history. */
@TargetApi(VERSION_CODES.N)
-@SuppressWarnings("AndroidApiChecker") // TODO(b/32513850) remove when error prone is updated
public class DvrHistoryDialogFragment extends SafeDismissDialogFragment {
public static final String DIALOG_TAG = DvrHistoryDialogFragment.class.getSimpleName();
@@ -53,7 +53,7 @@ public class DvrHistoryDialogFragment extends SafeDismissDialogFragment {
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
- TvSingletons singletons = TvSingletons.getSingletons(getContext());
+ ApplicationSingletons singletons = TvApplication.getSingletons(getContext());
DvrDataManager dataManager = singletons.getDvrDataManager();
ChannelDataManager channelDataManager = singletons.getChannelDataManager();
for (ScheduledRecording schedule : dataManager.getAllScheduledRecordings()) {
diff --git a/src/com/android/tv/dialog/PinDialogFragment.java b/src/com/android/tv/dialog/PinDialogFragment.java
index 71f45fbe..ccc3a983 100644
--- a/src/com/android/tv/dialog/PinDialogFragment.java
+++ b/src/com/android/tv/dialog/PinDialogFragment.java
@@ -45,13 +45,13 @@ import android.widget.FrameLayout;
import android.widget.TextView;
import android.widget.Toast;
import com.android.tv.R;
-import com.android.tv.TvSingletons;
+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 DEBUG = false;
+ private static final boolean DEBUG = true;
/** PIN code dialog for unlock channel */
public static final int PIN_DIALOG_TYPE_UNLOCK_CHANNEL = 0;
@@ -68,7 +68,7 @@ public class PinDialogFragment extends SafeDismissDialogFragment {
/** PIN code dialog for set new PIN */
public static final int PIN_DIALOG_TYPE_NEW_PIN = 3;
- // PIN code dialog for checking old PIN. Only used in this class.
+ // PIN code dialog for checking old PIN. This is internal only.
private static final int PIN_DIALOG_TYPE_OLD_PIN = 4;
/** PIN code dialog for unlocking DVR playback */
@@ -192,7 +192,7 @@ public class PinDialogFragment extends SafeDismissDialogFragment {
mTitleView.setText(
getString(
R.string.pin_enter_unlock_dvr,
- TvSingletons.getSingletons(getContext())
+ TvApplication.getSingletons(getContext())
.getTvInputManagerHelper()
.getContentRatingsManager()
.getDisplayNameForRating(tvContentRating)));
diff --git a/src/com/android/tv/dialog/SafeDismissDialogFragment.java b/src/com/android/tv/dialog/SafeDismissDialogFragment.java
index 6eb67dfd..18460cb6 100644
--- a/src/com/android/tv/dialog/SafeDismissDialogFragment.java
+++ b/src/com/android/tv/dialog/SafeDismissDialogFragment.java
@@ -19,7 +19,7 @@ package com.android.tv.dialog;
import android.app.Activity;
import android.app.DialogFragment;
import com.android.tv.MainActivity;
-import com.android.tv.TvSingletons;
+import com.android.tv.TvApplication;
import com.android.tv.analytics.HasTrackerLabel;
import com.android.tv.analytics.Tracker;
@@ -37,7 +37,7 @@ public abstract class SafeDismissDialogFragment extends DialogFragment implement
if (activity instanceof MainActivity) {
mActivity = (MainActivity) activity;
}
- mTracker = TvSingletons.getSingletons(activity).getTracker();
+ mTracker = TvApplication.getSingletons(activity).getTracker();
if (mDismissPending) {
mDismissPending = false;
dismiss();
diff --git a/src/com/android/tv/dvr/BaseDvrDataManager.java b/src/com/android/tv/dvr/BaseDvrDataManager.java
index 0befba9c..342e4b21 100644
--- a/src/com/android/tv/dvr/BaseDvrDataManager.java
+++ b/src/com/android/tv/dvr/BaseDvrDataManager.java
@@ -25,11 +25,11 @@ import android.util.ArraySet;
import android.util.Log;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.common.feature.CommonFeatures;
-import com.android.tv.common.util.Clock;
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;
import java.util.Arrays;
import java.util.Collection;
diff --git a/src/com/android/tv/dvr/DvrDataManagerImpl.java b/src/com/android/tv/dvr/DvrDataManagerImpl.java
index 28006b08..17ea63a0 100644
--- a/src/com/android/tv/dvr/DvrDataManagerImpl.java
+++ b/src/com/android/tv/dvr/DvrDataManagerImpl.java
@@ -38,12 +38,9 @@ import android.text.TextUtils;
import android.util.ArraySet;
import android.util.Log;
import android.util.Range;
-import com.android.tv.TvSingletons;
+import com.android.tv.TvApplication;
import com.android.tv.common.SoftPreconditions;
-import com.android.tv.common.recording.RecordingStorageStatusManager;
-import com.android.tv.common.recording.RecordingStorageStatusManager.OnStorageMountChangedListener;
-import com.android.tv.common.util.Clock;
-import com.android.tv.common.util.CommonUtils;
+import com.android.tv.dvr.DvrStorageStatusManager.OnStorageMountChangedListener;
import com.android.tv.dvr.data.IdGenerator;
import com.android.tv.dvr.data.RecordedProgram;
import com.android.tv.dvr.data.ScheduledRecording;
@@ -61,9 +58,11 @@ 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.TvUriMatcher;
+import com.android.tv.util.Utils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@@ -115,7 +114,7 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
private boolean mRecordedProgramLoadFinished;
private final Set<AsyncTask> mPendingTasks = new ArraySet<>();
private DvrDbSync mDbSync;
- private RecordingStorageStatusManager mStorageStatusManager;
+ private DvrStorageStatusManager mStorageStatusManager;
private final TvInputCallback mInputCallback =
new TvInputCallback() {
@@ -141,7 +140,7 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
@Override
public void onStorageMountChanged(boolean storageMounted) {
for (TvInputInfo input : mInputManager.getTvInputInfos(true, true)) {
- if (CommonUtils.isBundledInput(input.getId())) {
+ if (Utils.isBundledInput(input.getId())) {
if (storageMounted) {
unhideInput(input.getId());
} else {
@@ -170,9 +169,8 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
public DvrDataManagerImpl(Context context, Clock clock) {
super(context, clock);
mContext = context;
- mInputManager = TvSingletons.getSingletons(context).getTvInputManagerHelper();
- mStorageStatusManager =
- TvSingletons.getSingletons(context).getRecordingStorageStatusManager();
+ mInputManager = TvApplication.getSingletons(context).getTvInputManagerHelper();
+ mStorageStatusManager = TvApplication.getSingletons(context).getDvrStorageStatusManager();
}
public void start() {
@@ -610,8 +608,9 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
SoftPreconditions.checkArgument(
previousSeries == null,
TAG,
- "Attempt to add series" + " recording with the duplicate series ID: %s",
- r.getSeriesId());
+ "Attempt to add series"
+ + " recording with the duplicate series ID: "
+ + r.getSeriesId());
}
if (mDvrLoadFinished) {
notifySeriesRecordingAdded(seriesRecordings);
@@ -780,14 +779,13 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
if (!SoftPreconditions.checkArgument(
mSeriesRecordings.containsKey(r.getId()),
TAG,
- "Non Existing Series ID: %s",
- r)) {
+ "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 updated: %s", r);
+ old1.equals(old2), TAG, "Series ID cannot be" + " updated: " + r);
}
if (mDvrLoadFinished) {
notifySeriesRecordingChanged(seriesRecordings);
@@ -797,8 +795,7 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
private boolean isInputAvailable(String inputId) {
return mInputManager.hasTvInputInfo(inputId)
- && (!CommonUtils.isBundledInput(inputId)
- || mStorageStatusManager.isStorageMounted());
+ && (!Utils.isBundledInput(inputId) || mStorageStatusManager.isStorageMounted());
}
private void removeDeletedSchedules(ScheduledRecording... addedSchedules) {
diff --git a/src/com/android/tv/dvr/DvrManager.java b/src/com/android/tv/dvr/DvrManager.java
index 247e1bc5..50751d95 100644
--- a/src/com/android/tv/dvr/DvrManager.java
+++ b/src/com/android/tv/dvr/DvrManager.java
@@ -36,10 +36,10 @@ import android.support.annotation.VisibleForTesting;
import android.support.annotation.WorkerThread;
import android.util.Log;
import android.util.Range;
-import com.android.tv.TvSingletons;
+import com.android.tv.ApplicationSingletons;
+import com.android.tv.TvApplication;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.common.feature.CommonFeatures;
-import com.android.tv.common.util.CommonUtils;
import com.android.tv.data.Channel;
import com.android.tv.data.Program;
import com.android.tv.dvr.DvrDataManager.OnRecordedProgramLoadFinishedListener;
@@ -78,9 +78,9 @@ public class DvrManager {
public DvrManager(Context context) {
SoftPreconditions.checkFeatureEnabled(context, CommonFeatures.DVR, TAG);
mAppContext = context.getApplicationContext();
- TvSingletons tvSingletons = TvSingletons.getSingletons(context);
- mDataManager = (WritableDvrDataManager) tvSingletons.getDvrDataManager();
- mScheduleManager = tvSingletons.getDvrScheduleManager();
+ ApplicationSingletons appSingletons = TvApplication.getSingletons(context);
+ mDataManager = (WritableDvrDataManager) appSingletons.getDvrDataManager();
+ mScheduleManager = appSingletons.getDvrScheduleManager();
if (mDataManager.isInitialized() && mScheduleManager.isInitialized()) {
createSeriesRecordingsForRecordedProgramsIfNeeded(mDataManager.getRecordedPrograms());
} else {
@@ -666,7 +666,7 @@ public class DvrManager {
return false;
}
Program program =
- TvSingletons.getSingletons(mAppContext)
+ TvApplication.getSingletons(mAppContext)
.getProgramDataManager()
.getCurrentProgram(channel.getId());
return program == null || !program.isRecordingProhibited();
@@ -683,7 +683,7 @@ public class DvrManager {
return false;
}
Channel channel =
- TvSingletons.getSingletons(mAppContext)
+ TvApplication.getSingletons(mAppContext)
.getChannelDataManager()
.getChannel(program.getChannelId());
if (channel == null || channel.isRecordingProhibited()) {
@@ -833,7 +833,7 @@ public class DvrManager {
if (!recordedProgramPath.exists()) {
if (DEBUG) Log.d(TAG, "File to delete not exist: " + recordedProgramPath);
} else {
- CommonUtils.deleteDirOrFile(recordedProgramPath);
+ Utils.deleteDirOrFile(recordedProgramPath);
if (DEBUG) {
Log.d(TAG, "Sucessfully deleted files of the recorded program: " + dataUri);
}
diff --git a/src/com/android/tv/dvr/DvrScheduleManager.java b/src/com/android/tv/dvr/DvrScheduleManager.java
index cbb89290..62f93c8b 100644
--- a/src/com/android/tv/dvr/DvrScheduleManager.java
+++ b/src/com/android/tv/dvr/DvrScheduleManager.java
@@ -25,7 +25,8 @@ import android.support.annotation.NonNull;
import android.support.annotation.VisibleForTesting;
import android.util.ArraySet;
import android.util.Range;
-import com.android.tv.TvSingletons;
+import com.android.tv.ApplicationSingletons;
+import com.android.tv.TvApplication;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.data.Channel;
import com.android.tv.data.ChannelDataManager;
@@ -50,7 +51,6 @@ import java.util.concurrent.CopyOnWriteArraySet;
/** A class to manage the schedules. */
@TargetApi(Build.VERSION_CODES.N)
@MainThread
-@SuppressWarnings("AndroidApiChecker") // TODO(b/32513850) remove when error prone is updated
public class DvrScheduleManager {
private static final String TAG = "DvrScheduleManager";
@@ -94,9 +94,9 @@ public class DvrScheduleManager {
public DvrScheduleManager(Context context) {
mContext = context;
- TvSingletons tvSingletons = TvSingletons.getSingletons(context);
- mDataManager = (DvrDataManagerImpl) tvSingletons.getDvrDataManager();
- mChannelDataManager = tvSingletons.getChannelDataManager();
+ ApplicationSingletons appSingletons = TvApplication.getSingletons(context);
+ mDataManager = (DvrDataManagerImpl) appSingletons.getDvrDataManager();
+ mChannelDataManager = appSingletons.getChannelDataManager();
if (mDataManager.isDvrScheduleLoadFinished() && mChannelDataManager.isDbLoadFinished()) {
buildData();
} else {
@@ -126,7 +126,7 @@ public class DvrScheduleManager {
TvInputInfo input =
Utils.getTvInputInfoForInputId(mContext, schedule.getInputId());
if (!SoftPreconditions.checkArgument(
- input != null, TAG, "Input was removed for : %s", schedule)) {
+ input != null, TAG, "Input was removed for : " + schedule)) {
// Input removed.
mInputScheduleMap.remove(schedule.getInputId());
mInputConflictInfoMap.remove(schedule.getInputId());
@@ -190,7 +190,7 @@ public class DvrScheduleManager {
TvInputInfo input =
Utils.getTvInputInfoForInputId(mContext, schedule.getInputId());
if (!SoftPreconditions.checkArgument(
- input != null, TAG, "Input was removed for : %s", schedule)) {
+ input != null, TAG, "Input was removed for : " + schedule)) {
// Input removed.
mInputScheduleMap.remove(schedule.getInputId());
mInputConflictInfoMap.remove(schedule.getInputId());
diff --git a/src/com/android/tv/dvr/DvrStorageStatusManager.java b/src/com/android/tv/dvr/DvrStorageStatusManager.java
index fe5a47b8..a2f4bda8 100644
--- a/src/com/android/tv/dvr/DvrStorageStatusManager.java
+++ b/src/com/android/tv/dvr/DvrStorageStatusManager.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2017 The Android Open Source Project
+ * 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.
@@ -11,56 +11,272 @@
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
- * limitations under the License.
+ * limitations under the License
*/
+
package com.android.tv.dvr;
+import android.content.BroadcastReceiver;
import android.content.ContentProviderOperation;
import android.content.ContentResolver;
import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
import android.content.OperationApplicationException;
import android.database.Cursor;
+import android.media.tv.TvContract;
import android.media.tv.TvInputInfo;
import android.net.Uri;
import android.os.AsyncTask;
+import android.os.Environment;
+import android.os.Looper;
import android.os.RemoteException;
-import android.support.media.tv.TvContractCompat;
+import android.os.StatFs;
+import android.support.annotation.AnyThread;
+import android.support.annotation.IntDef;
+import android.support.annotation.WorkerThread;
import android.util.Log;
-import com.android.tv.TvSingletons;
-import com.android.tv.common.recording.RecordingStorageStatusManager;
-import com.android.tv.common.util.CommonUtils;
+import com.android.tv.TvApplication;
+import com.android.tv.common.SoftPreconditions;
+import com.android.tv.common.feature.CommonFeatures;
import com.android.tv.util.TvInputManagerHelper;
+import com.android.tv.util.Utils;
import java.io.File;
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArraySet;
-/** A class for extending TV app-specific function to {@link RecordingStorageStatusManager}. */
-@SuppressWarnings("TryWithResources") // TODO(b/62143348): remove when error prone check fixed
-public class DvrStorageStatusManager extends RecordingStorageStatusManager {
+/** Signals DVR storage status change such as plugging/unplugging. */
+public class DvrStorageStatusManager {
private static final String TAG = "DvrStorageStatusManager";
+ private static final boolean DEBUG = false;
- private final Context mContext;
- private CleanUpDbTask mCleanUpDbTask;
+ /** Minimum storage size to support DVR */
+ public static final long MIN_STORAGE_SIZE_FOR_DVR_IN_BYTES = 50 * 1024 * 1024 * 1024L; // 50GB
+
+ private static final long MIN_FREE_STORAGE_SIZE_FOR_DVR_IN_BYTES =
+ 10 * 1024 * 1024 * 1024L; // 10GB
+ private static final String RECORDING_DATA_SUB_PATH = "/recording";
private static final String[] PROJECTION = {
- TvContractCompat.RecordedPrograms._ID,
- TvContractCompat.RecordedPrograms.COLUMN_PACKAGE_NAME,
- TvContractCompat.RecordedPrograms.COLUMN_RECORDING_DATA_URI
+ TvContract.RecordedPrograms._ID,
+ TvContract.RecordedPrograms.COLUMN_PACKAGE_NAME,
+ TvContract.RecordedPrograms.COLUMN_RECORDING_DATA_URI
};
private static final int BATCH_OPERATION_COUNT = 100;
- public DvrStorageStatusManager(Context context) {
- super(context);
+ @IntDef({
+ STORAGE_STATUS_OK,
+ STORAGE_STATUS_TOTAL_CAPACITY_TOO_SMALL,
+ STORAGE_STATUS_FREE_SPACE_INSUFFICIENT,
+ STORAGE_STATUS_MISSING
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface StorageStatus {}
+
+ /** Current storage is OK to record a program. */
+ public static final int STORAGE_STATUS_OK = 0;
+
+ /** Current storage's total capacity is smaller than DVR requirement. */
+ public static final int STORAGE_STATUS_TOTAL_CAPACITY_TOO_SMALL = 1;
+
+ /** Current storage's free space is insufficient to record programs. */
+ public static final int STORAGE_STATUS_FREE_SPACE_INSUFFICIENT = 2;
+
+ /** Current storage is missing. */
+ public static final int STORAGE_STATUS_MISSING = 3;
+
+ private final Context mContext;
+ private final Set<OnStorageMountChangedListener> mOnStorageMountChangedListeners =
+ new CopyOnWriteArraySet<>();
+ private final boolean mRunningInMainProcess;
+ private MountedStorageStatus mMountedStorageStatus;
+ private boolean mStorageValid;
+ private CleanUpDbTask mCleanUpDbTask;
+
+ private class MountedStorageStatus {
+ private final boolean mStorageMounted;
+ private final File mStorageMountedDir;
+ private final long mStorageMountedCapacity;
+
+ private MountedStorageStatus(boolean mounted, File mountedDir, long capacity) {
+ mStorageMounted = mounted;
+ mStorageMountedDir = mountedDir;
+ mStorageMountedCapacity = capacity;
+ }
+
+ private boolean isValidForDvr() {
+ return mStorageMounted && mStorageMountedCapacity >= MIN_STORAGE_SIZE_FOR_DVR_IN_BYTES;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (!(other instanceof MountedStorageStatus)) {
+ return false;
+ }
+ MountedStorageStatus status = (MountedStorageStatus) other;
+ return mStorageMounted == status.mStorageMounted
+ && Objects.equals(mStorageMountedDir, status.mStorageMountedDir)
+ && mStorageMountedCapacity == status.mStorageMountedCapacity;
+ }
+ }
+
+ public interface OnStorageMountChangedListener {
+
+ /**
+ * Listener for DVR storage status change.
+ *
+ * @param storageMounted {@code true} when DVR possible storage is mounted, {@code false}
+ * otherwise.
+ */
+ void onStorageMountChanged(boolean storageMounted);
+ }
+
+ private final class StorageStatusBroadcastReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ MountedStorageStatus result = getStorageStatusInternal();
+ if (mMountedStorageStatus.equals(result)) {
+ return;
+ }
+ mMountedStorageStatus = result;
+ if (result.mStorageMounted && mRunningInMainProcess) {
+ // Cleans up DB in LC process.
+ // Tuner process is not always on.
+ if (mCleanUpDbTask != null) {
+ mCleanUpDbTask.cancel(true);
+ }
+ mCleanUpDbTask = new CleanUpDbTask();
+ mCleanUpDbTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+ boolean valid = result.isValidForDvr();
+ if (valid == mStorageValid) {
+ return;
+ }
+ mStorageValid = valid;
+ for (OnStorageMountChangedListener l : mOnStorageMountChangedListeners) {
+ l.onStorageMountChanged(valid);
+ }
+ }
+ }
+
+ /**
+ * Creates DvrStorageStatusManager.
+ *
+ * @param context {@link Context}
+ */
+ public DvrStorageStatusManager(final Context context, boolean runningInMainProcess) {
mContext = context;
+ mRunningInMainProcess = runningInMainProcess;
+ mMountedStorageStatus = getStorageStatusInternal();
+ mStorageValid = mMountedStorageStatus.isValidForDvr();
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(Intent.ACTION_MEDIA_MOUNTED);
+ filter.addAction(Intent.ACTION_MEDIA_UNMOUNTED);
+ filter.addAction(Intent.ACTION_MEDIA_EJECT);
+ filter.addAction(Intent.ACTION_MEDIA_REMOVED);
+ filter.addAction(Intent.ACTION_MEDIA_BAD_REMOVAL);
+ filter.addDataScheme(ContentResolver.SCHEME_FILE);
+ mContext.registerReceiver(new StorageStatusBroadcastReceiver(), filter);
+ }
+
+ /**
+ * Adds the listener for receiving storage status change.
+ *
+ * @param listener
+ */
+ public void addListener(OnStorageMountChangedListener listener) {
+ mOnStorageMountChangedListeners.add(listener);
+ }
+
+ /** Removes the current listener. */
+ public void removeListener(OnStorageMountChangedListener listener) {
+ mOnStorageMountChangedListeners.remove(listener);
+ }
+
+ /** Returns true if a storage is mounted. */
+ public boolean isStorageMounted() {
+ return mMountedStorageStatus.mStorageMounted;
+ }
+
+ /** Returns the path to DVR recording data directory. This can take for a while sometimes. */
+ @WorkerThread
+ public File getRecordingRootDataDirectory() {
+ SoftPreconditions.checkState(Looper.myLooper() != Looper.getMainLooper());
+ if (mMountedStorageStatus.mStorageMountedDir == null) {
+ return null;
+ }
+ File root = mContext.getExternalFilesDir(null);
+ String rootPath;
+ try {
+ rootPath = root != null ? root.getCanonicalPath() : null;
+ } catch (IOException | SecurityException e) {
+ return null;
+ }
+ return rootPath == null ? null : new File(rootPath + RECORDING_DATA_SUB_PATH);
+ }
+
+ /**
+ * Returns the current storage status for DVR recordings.
+ *
+ * @return {@link StorageStatus}
+ */
+ @AnyThread
+ public @StorageStatus int getDvrStorageStatus() {
+ MountedStorageStatus status = mMountedStorageStatus;
+ if (status.mStorageMountedDir == null) {
+ return STORAGE_STATUS_MISSING;
+ }
+ if (CommonFeatures.FORCE_RECORDING_UNTIL_NO_SPACE.isEnabled(mContext)) {
+ return STORAGE_STATUS_OK;
+ }
+ if (status.mStorageMountedCapacity < MIN_STORAGE_SIZE_FOR_DVR_IN_BYTES) {
+ return STORAGE_STATUS_TOTAL_CAPACITY_TOO_SMALL;
+ }
+ try {
+ StatFs statFs = new StatFs(status.mStorageMountedDir.toString());
+ if (statFs.getAvailableBytes() < MIN_FREE_STORAGE_SIZE_FOR_DVR_IN_BYTES) {
+ return STORAGE_STATUS_FREE_SPACE_INSUFFICIENT;
+ }
+ } catch (IllegalArgumentException e) {
+ // In rare cases, storage status change was not notified yet.
+ SoftPreconditions.checkState(false);
+ return STORAGE_STATUS_FREE_SPACE_INSUFFICIENT;
+ }
+ return STORAGE_STATUS_OK;
}
- @Override
- protected void cleanUpDbIfNeeded() {
- if (mCleanUpDbTask != null) {
- mCleanUpDbTask.cancel(true);
+ /**
+ * Returns whether the storage has sufficient storage.
+ *
+ * @return {@code true} when there is sufficient storage, {@code false} otherwise
+ */
+ public boolean isStorageSufficient() {
+ return getDvrStorageStatus() == STORAGE_STATUS_OK;
+ }
+
+ private MountedStorageStatus getStorageStatusInternal() {
+ boolean storageMounted =
+ Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
+ File storageMountedDir = storageMounted ? Environment.getExternalStorageDirectory() : null;
+ storageMounted = storageMounted && storageMountedDir != null;
+ long storageMountedCapacity = 0L;
+ if (storageMounted) {
+ try {
+ StatFs statFs = new StatFs(storageMountedDir.toString());
+ storageMountedCapacity = statFs.getTotalBytes();
+ } catch (IllegalArgumentException e) {
+ Log.e(TAG, "Storage mount status was changed.");
+ storageMounted = false;
+ storageMountedDir = null;
+ }
}
- mCleanUpDbTask = new CleanUpDbTask();
- mCleanUpDbTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ return new MountedStorageStatus(storageMounted, storageMountedDir, storageMountedCapacity);
}
private class CleanUpDbTask extends AsyncTask<Void, Void, Boolean> {
@@ -72,11 +288,11 @@ public class DvrStorageStatusManager extends RecordingStorageStatusManager {
@Override
protected Boolean doInBackground(Void... params) {
- @StorageStatus int storageStatus = getDvrStorageStatus();
- if (storageStatus == STORAGE_STATUS_MISSING) {
+ @DvrStorageStatusManager.StorageStatus int storageStatus = getDvrStorageStatus();
+ if (storageStatus == DvrStorageStatusManager.STORAGE_STATUS_MISSING) {
return null;
}
- if (storageStatus == STORAGE_STATUS_TOTAL_CAPACITY_TOO_SMALL) {
+ if (storageStatus == DvrStorageStatusManager.STORAGE_STATUS_TOTAL_CAPACITY_TOO_SMALL) {
return true;
}
List<ContentProviderOperation> ops = getDeleteOps();
@@ -94,7 +310,7 @@ public class DvrStorageStatusManager extends RecordingStorageStatusManager {
ArrayList<ContentProviderOperation> batchOps =
new ArrayList<>(ops.subList(i, toIndex));
try {
- mContext.getContentResolver().applyBatch(TvContractCompat.AUTHORITY, batchOps);
+ mContext.getContentResolver().applyBatch(TvContract.AUTHORITY, batchOps);
} catch (RemoteException | OperationApplicationException e) {
Log.e(TAG, "Failed to clean up RecordedPrograms.", e);
}
@@ -105,16 +321,16 @@ public class DvrStorageStatusManager extends RecordingStorageStatusManager {
@Override
protected void onPostExecute(Boolean forgetStorage) {
if (forgetStorage != null && forgetStorage == true) {
- DvrManager dvrManager = TvSingletons.getSingletons(mContext).getDvrManager();
+ DvrManager dvrManager = TvApplication.getSingletons(mContext).getDvrManager();
TvInputManagerHelper tvInputManagerHelper =
- TvSingletons.getSingletons(mContext).getTvInputManagerHelper();
+ TvApplication.getSingletons(mContext).getTvInputManagerHelper();
List<TvInputInfo> tvInputInfoList =
tvInputManagerHelper.getTvInputInfos(true, false);
if (tvInputInfoList == null || tvInputInfoList.isEmpty()) {
return;
}
for (TvInputInfo info : tvInputInfoList) {
- if (CommonUtils.isBundledInput(info.getId())) {
+ if (Utils.isBundledInput(info.getId())) {
dvrManager.forgetStorage(info.getId());
}
}
@@ -129,7 +345,7 @@ public class DvrStorageStatusManager extends RecordingStorageStatusManager {
try (Cursor c =
mContentResolver.query(
- TvContractCompat.RecordedPrograms.CONTENT_URI,
+ TvContract.RecordedPrograms.CONTENT_URI,
PROJECTION,
null,
null,
@@ -138,8 +354,10 @@ public class DvrStorageStatusManager extends RecordingStorageStatusManager {
return null;
}
while (c.moveToNext()) {
- @StorageStatus int storageStatus = getDvrStorageStatus();
- if (isCancelled() || storageStatus == STORAGE_STATUS_MISSING) {
+ @DvrStorageStatusManager.StorageStatus
+ int storageStatus = getDvrStorageStatus();
+ if (isCancelled()
+ || storageStatus == DvrStorageStatusManager.STORAGE_STATUS_MISSING) {
ops.clear();
break;
}
@@ -150,7 +368,7 @@ public class DvrStorageStatusManager extends RecordingStorageStatusManager {
continue;
}
Uri dataUri = Uri.parse(dataUriString);
- if (!CommonUtils.isInBundledPackageSet(packageName)
+ if (!Utils.isInBundledPackageSet(packageName)
|| dataUri == null
|| dataUri.getPath() == null
|| !ContentResolver.SCHEME_FILE.equals(dataUri.getScheme())) {
@@ -160,7 +378,7 @@ public class DvrStorageStatusManager extends RecordingStorageStatusManager {
if (!recordedProgramDir.exists()) {
ops.add(
ContentProviderOperation.newDelete(
- TvContractCompat.buildRecordedProgramUri(
+ 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 8616962f..7da2bfc9 100644
--- a/src/com/android/tv/dvr/DvrWatchedPositionManager.java
+++ b/src/com/android/tv/dvr/DvrWatchedPositionManager.java
@@ -20,7 +20,7 @@ import android.content.Context;
import android.content.SharedPreferences;
import android.media.tv.TvInputManager;
import android.support.annotation.IntDef;
-import com.android.tv.common.util.SharedPreferencesUtils;
+import com.android.tv.common.SharedPreferencesUtils;
import com.android.tv.dvr.data.RecordedProgram;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
diff --git a/src/com/android/tv/dvr/data/RecordedProgram.java b/src/com/android/tv/dvr/data/RecordedProgram.java
index e1fbca8c..18841ae5 100644
--- a/src/com/android/tv/dvr/data/RecordedProgram.java
+++ b/src/com/android/tv/dvr/data/RecordedProgram.java
@@ -30,10 +30,10 @@ 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.common.util.CommonUtils;
import com.android.tv.data.BaseProgram;
import com.android.tv.data.GenreItems;
import com.android.tv.data.InternalDataUtils;
+import com.android.tv.util.Utils;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
@@ -118,7 +118,7 @@ public class RecordedProgram extends BaseProgram {
.setInternalProviderFlag3(cursor.getInt(index++))
.setInternalProviderFlag4(cursor.getInt(index++))
.setVersionNumber(cursor.getInt(index++));
- if (CommonUtils.isInBundledPackageSet(builder.mPackageName)) {
+ if (Utils.isInBundledPackageSet(builder.mPackageName)) {
InternalDataUtils.deserializeInternalProviderData(cursor.getBlob(index), builder);
}
return builder.build();
diff --git a/src/com/android/tv/dvr/data/ScheduledRecording.java b/src/com/android/tv/dvr/data/ScheduledRecording.java
index aa1dfc72..7de37ebc 100644
--- a/src/com/android/tv/dvr/data/ScheduledRecording.java
+++ b/src/com/android/tv/dvr/data/ScheduledRecording.java
@@ -16,25 +16,23 @@
package com.android.tv.dvr.data;
-import android.annotation.TargetApi;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
-import android.os.Build;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.IntDef;
import android.text.TextUtils;
import android.util.Range;
import com.android.tv.R;
-import com.android.tv.TvSingletons;
+import com.android.tv.TvApplication;
import com.android.tv.common.SoftPreconditions;
-import com.android.tv.common.util.CommonUtils;
import com.android.tv.data.Channel;
import com.android.tv.data.Program;
import com.android.tv.dvr.DvrScheduleManager;
import com.android.tv.dvr.provider.DvrContract.Schedules;
import com.android.tv.util.CompositeComparator;
+import com.android.tv.util.Utils;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Collection;
@@ -42,8 +40,6 @@ import java.util.Comparator;
import java.util.Objects;
/** A data class for one recording contents. */
-@TargetApi(Build.VERSION_CODES.N)
-@SuppressWarnings("AndroidApiChecker") // TODO(b/32513850) remove when error prone is updated
public final class ScheduledRecording implements Parcelable {
private static final String TAG = "ScheduledRecording";
@@ -659,7 +655,7 @@ public final class ScheduledRecording implements Parcelable {
return mProgramTitle;
}
Channel channel =
- TvSingletons.getSingletons(context).getChannelDataManager().getChannel(mChannelId);
+ TvApplication.getSingletons(context).getChannelDataManager().getChannel(mChannelId);
return channel != null
? channel.getDisplayName()
: context.getString(R.string.no_program_information);
@@ -673,7 +669,7 @@ public final class ScheduledRecording implements Parcelable {
case Schedules.TYPE_PROGRAM:
return TYPE_PROGRAM;
default:
- SoftPreconditions.checkArgument(false, TAG, "Unknown recording type %s", type);
+ SoftPreconditions.checkArgument(false, TAG, "Unknown recording type " + type);
return TYPE_TIMED;
}
}
@@ -686,7 +682,7 @@ public final class ScheduledRecording implements Parcelable {
case TYPE_PROGRAM:
return Schedules.TYPE_PROGRAM;
default:
- SoftPreconditions.checkArgument(false, TAG, "Unknown recording type %s", type);
+ SoftPreconditions.checkArgument(false, TAG, "Unknown recording type " + type);
return Schedules.TYPE_TIMED;
}
}
@@ -712,7 +708,7 @@ public final class ScheduledRecording implements Parcelable {
case Schedules.STATE_RECORDING_CANCELED:
return STATE_RECORDING_CANCELED;
default:
- SoftPreconditions.checkArgument(false, TAG, "Unknown recording state %s", state);
+ SoftPreconditions.checkArgument(false, TAG, "Unknown recording state" + state);
return STATE_RECORDING_NOT_STARTED;
}
}
@@ -738,7 +734,7 @@ public final class ScheduledRecording implements Parcelable {
case STATE_RECORDING_CANCELED:
return Schedules.STATE_RECORDING_CANCELED;
default:
- SoftPreconditions.checkArgument(false, TAG, "Unknown recording state %s", state);
+ SoftPreconditions.checkArgument(false, TAG, "Unknown recording state" + state);
return Schedules.STATE_RECORDING_NOT_STARTED;
}
}
@@ -769,12 +765,12 @@ public final class ScheduledRecording implements Parcelable {
+ ",type="
+ mType
+ ",startTime="
- + CommonUtils.toIsoDateTimeString(mStartTimeMs)
+ + Utils.toIsoDateTimeString(mStartTimeMs)
+ "("
+ mStartTimeMs
+ ")"
+ ",endTime="
- + CommonUtils.toIsoDateTimeString(mEndTimeMs)
+ + Utils.toIsoDateTimeString(mEndTimeMs)
+ "("
+ mEndTimeMs
+ ")"
diff --git a/src/com/android/tv/dvr/data/SeriesRecording.java b/src/com/android/tv/dvr/data/SeriesRecording.java
index 96b3425a..1fd1cea3 100644
--- a/src/com/android/tv/dvr/data/SeriesRecording.java
+++ b/src/com/android/tv/dvr/data/SeriesRecording.java
@@ -568,7 +568,7 @@ public class SeriesRecording implements Parcelable {
mLongDescription,
mSeriesId,
mChannelOption,
- Arrays.hashCode(mCanonicalGenreIds),
+ mCanonicalGenreIds,
mPosterUri,
mPhotoUri,
mState);
diff --git a/src/com/android/tv/dvr/provider/AsyncDvrDbTask.java b/src/com/android/tv/dvr/provider/AsyncDvrDbTask.java
index db18e609..ad00bec8 100644
--- a/src/com/android/tv/dvr/provider/AsyncDvrDbTask.java
+++ b/src/com/android/tv/dvr/provider/AsyncDvrDbTask.java
@@ -20,18 +20,17 @@ import android.content.Context;
import android.database.Cursor;
import android.os.AsyncTask;
import android.support.annotation.Nullable;
-import com.android.tv.common.concurrent.NamedThreadFactory;
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;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/** {@link AsyncTask} that defaults to executing on its own single threaded Executor Service. */
-@SuppressWarnings("TryWithResources") // TODO(b/62143348): remove when error prone check fixed
public abstract class AsyncDvrDbTask<Params, Progress, Result>
extends AsyncTask<Params, Progress, Result> {
private static final NamedThreadFactory THREAD_FACTORY =
diff --git a/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java b/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java
index 0fb96d1b..fb793a0e 100644
--- a/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java
+++ b/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java
@@ -34,7 +34,7 @@ import com.android.tv.dvr.provider.DvrContract.SeriesRecordings;
/** A data class for one recorded contents. */
public class DvrDatabaseHelper extends SQLiteOpenHelper {
private static final String TAG = "DvrDatabaseHelper";
- private static final boolean DEBUG = false;
+ private static final boolean DEBUG = true;
private static final int DATABASE_VERSION = 17;
private static final String DB_NAME = "dvr.db";
diff --git a/src/com/android/tv/dvr/provider/DvrDbSync.java b/src/com/android/tv/dvr/provider/DvrDbSync.java
index 8bd16221..1cdeef24 100644
--- a/src/com/android/tv/dvr/provider/DvrDbSync.java
+++ b/src/com/android/tv/dvr/provider/DvrDbSync.java
@@ -29,7 +29,7 @@ import android.os.Looper;
import android.support.annotation.MainThread;
import android.support.annotation.VisibleForTesting;
import android.util.Log;
-import com.android.tv.TvSingletons;
+import com.android.tv.TvApplication;
import com.android.tv.data.ChannelDataManager;
import com.android.tv.data.Program;
import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener;
@@ -140,8 +140,8 @@ public class DvrDbSync {
this(
context,
dataManager,
- TvSingletons.getSingletons(context).getChannelDataManager(),
- TvSingletons.getSingletons(context).getDvrManager(),
+ TvApplication.getSingletons(context).getChannelDataManager(),
+ TvApplication.getSingletons(context).getDvrManager(),
SeriesRecordingScheduler.getInstance(context));
}
diff --git a/src/com/android/tv/dvr/provider/EpisodicProgramLoadTask.java b/src/com/android/tv/dvr/provider/EpisodicProgramLoadTask.java
index 7cdc7b73..e9ca11e5 100644
--- a/src/com/android/tv/dvr/provider/EpisodicProgramLoadTask.java
+++ b/src/com/android/tv/dvr/provider/EpisodicProgramLoadTask.java
@@ -25,9 +25,8 @@ import android.net.Uri;
import android.os.Build;
import android.support.annotation.Nullable;
import android.support.annotation.WorkerThread;
-import com.android.tv.TvSingletons;
+import com.android.tv.TvApplication;
import com.android.tv.common.SoftPreconditions;
-import com.android.tv.common.util.PermissionUtils;
import com.android.tv.data.Program;
import com.android.tv.dvr.DvrDataManager;
import com.android.tv.dvr.data.ScheduledRecording;
@@ -35,6 +34,7 @@ import com.android.tv.dvr.data.SeasonEpisodeNumber;
import com.android.tv.dvr.data.SeriesRecording;
import com.android.tv.util.AsyncDbTask.AsyncProgramQueryTask;
import com.android.tv.util.AsyncDbTask.CursorFilter;
+import com.android.tv.util.PermissionUtils;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@@ -91,7 +91,7 @@ public abstract class EpisodicProgramLoadTask {
*/
public EpisodicProgramLoadTask(Context context, Collection<SeriesRecording> seriesRecordings) {
mContext = context.getApplicationContext();
- mDataManager = TvSingletons.getSingletons(context).getDvrDataManager();
+ mDataManager = TvApplication.getSingletons(context).getDvrDataManager();
mSeriesRecordings.addAll(seriesRecordings);
}
diff --git a/src/com/android/tv/dvr/recorder/ConflictChecker.java b/src/com/android/tv/dvr/recorder/ConflictChecker.java
index f5bc7b9f..732815cd 100644
--- a/src/com/android/tv/dvr/recorder/ConflictChecker.java
+++ b/src/com/android/tv/dvr/recorder/ConflictChecker.java
@@ -27,10 +27,11 @@ import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.ArraySet;
import android.util.Log;
+import com.android.tv.ApplicationSingletons;
import com.android.tv.InputSessionManager;
import com.android.tv.InputSessionManager.OnTvViewChannelChangeListener;
import com.android.tv.MainActivity;
-import com.android.tv.TvSingletons;
+import com.android.tv.TvApplication;
import com.android.tv.common.WeakHandler;
import com.android.tv.data.Channel;
import com.android.tv.data.ChannelDataManager;
@@ -39,7 +40,6 @@ 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.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -88,35 +88,21 @@ public class ConflictChecker {
new ScheduledRecordingListener() {
@Override
public void onScheduledRecordingAdded(ScheduledRecording... scheduledRecordings) {
- if (DEBUG) {
- Log.d(
- TAG,
- "onScheduledRecordingAdded: "
- + Arrays.toString(scheduledRecordings));
- }
+ if (DEBUG) Log.d(TAG, "onScheduledRecordingAdded: " + scheduledRecordings);
mHandler.sendEmptyMessage(MSG_CHECK_CONFLICT);
}
@Override
public void onScheduledRecordingRemoved(ScheduledRecording... scheduledRecordings) {
- if (DEBUG) {
- Log.d(
- TAG,
- "onScheduledRecordingRemoved: "
- + Arrays.toString(scheduledRecordings));
- }
+ if (DEBUG) Log.d(TAG, "onScheduledRecordingRemoved: " + scheduledRecordings);
mHandler.sendEmptyMessage(MSG_CHECK_CONFLICT);
}
@Override
public void onScheduledRecordingStatusChanged(
ScheduledRecording... scheduledRecordings) {
- if (DEBUG) {
- Log.d(
- TAG,
- "onScheduledRecordingStatusChanged: "
- + Arrays.toString(scheduledRecordings));
- }
+ if (DEBUG)
+ Log.d(TAG, "onScheduledRecordingStatusChanged: " + scheduledRecordings);
mHandler.sendEmptyMessage(MSG_CHECK_CONFLICT);
}
};
@@ -133,10 +119,10 @@ public class ConflictChecker {
public ConflictChecker(MainActivity mainActivity) {
mMainActivity = mainActivity;
- TvSingletons tvSingletons = TvSingletons.getSingletons(mainActivity);
- mChannelDataManager = tvSingletons.getChannelDataManager();
- mScheduleManager = tvSingletons.getDvrScheduleManager();
- mSessionManager = tvSingletons.getInputSessionManager();
+ ApplicationSingletons appSingletons = TvApplication.getSingletons(mainActivity);
+ mChannelDataManager = appSingletons.getChannelDataManager();
+ mScheduleManager = appSingletons.getDvrScheduleManager();
+ mSessionManager = appSingletons.getInputSessionManager();
}
/** Starts checking the conflict. */
diff --git a/src/com/android/tv/dvr/recorder/DvrRecordingService.java b/src/com/android/tv/dvr/recorder/DvrRecordingService.java
index 9fdbf062..3b21bab2 100644
--- a/src/com/android/tv/dvr/recorder/DvrRecordingService.java
+++ b/src/com/android/tv/dvr/recorder/DvrRecordingService.java
@@ -29,15 +29,15 @@ 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.Starter;
-import com.android.tv.TvSingletons;
+import com.android.tv.TvApplication;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.common.feature.CommonFeatures;
-import com.android.tv.common.util.Clock;
import com.android.tv.dvr.WritableDvrDataManager;
+import com.android.tv.util.Clock;
import com.android.tv.util.RecurringRunner;
/**
@@ -114,12 +114,12 @@ public class DvrRecordingService extends Service {
@Override
public void onCreate() {
- Starter.start(this);
+ TvApplication.setCurrentRunningProcess(this, true);
if (DEBUG) Log.d(TAG, "onCreate");
super.onCreate();
SoftPreconditions.checkFeatureEnabled(this, CommonFeatures.DVR, TAG);
sInstance = this;
- TvSingletons singletons = TvSingletons.getSingletons(this);
+ ApplicationSingletons singletons = TvApplication.getSingletons(this);
WritableDvrDataManager dataManager =
(WritableDvrDataManager) singletons.getDvrDataManager();
mSessionManager = singletons.getInputSessionManager();
@@ -183,6 +183,7 @@ public class DvrRecordingService extends Service {
@VisibleForTesting
protected void startForegroundInternal(boolean hasUpcomingRecording) {
+ // STOPSHIP: Replace the content title with real UX strings
Notification.Builder builder =
new Notification.Builder(this)
.setContentTitle(mContentTitle)
@@ -203,6 +204,7 @@ public class DvrRecordingService extends Service {
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,
diff --git a/src/com/android/tv/dvr/recorder/DvrStartRecordingReceiver.java b/src/com/android/tv/dvr/recorder/DvrStartRecordingReceiver.java
index bb5ea99d..f7521d6a 100644
--- a/src/com/android/tv/dvr/recorder/DvrStartRecordingReceiver.java
+++ b/src/com/android/tv/dvr/recorder/DvrStartRecordingReceiver.java
@@ -21,16 +21,15 @@ import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.support.annotation.RequiresApi;
-import com.android.tv.Starter;
-import com.android.tv.TvSingletons;
+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) {
- Starter.start(context);
- RecordingScheduler scheduler = TvSingletons.getSingletons(context).getRecordingScheduler();
+ TvApplication.setCurrentRunningProcess(context, true);
+ RecordingScheduler scheduler = TvApplication.getSingletons(context).getRecordingScheduler();
if (scheduler != null) {
scheduler.updateAndStartServiceIfNeeded();
}
diff --git a/src/com/android/tv/dvr/recorder/InputTaskScheduler.java b/src/com/android/tv/dvr/recorder/InputTaskScheduler.java
index 722e75fc..ff46c7c3 100644
--- a/src/com/android/tv/dvr/recorder/InputTaskScheduler.java
+++ b/src/com/android/tv/dvr/recorder/InputTaskScheduler.java
@@ -26,13 +26,13 @@ import android.util.ArrayMap;
import android.util.Log;
import android.util.LongSparseArray;
import com.android.tv.InputSessionManager;
-import com.android.tv.common.util.Clock;
import com.android.tv.data.Channel;
import com.android.tv.data.ChannelDataManager;
import com.android.tv.dvr.DvrDataManager;
import com.android.tv.dvr.DvrManager;
import com.android.tv.dvr.WritableDvrDataManager;
import com.android.tv.dvr.data.ScheduledRecording;
+import com.android.tv.util.Clock;
import com.android.tv.util.CompositeComparator;
import java.util.ArrayList;
import java.util.Collections;
@@ -443,7 +443,6 @@ public class InputTaskScheduler {
break;
case MSG_UPDATE_SCHEDULED_RECORDING:
handleUpdateSchedule((ScheduledRecording) msg.obj);
- break;
case MSG_BUILD_SCHEDULE:
handleBuildSchedule();
break;
diff --git a/src/com/android/tv/dvr/recorder/RecordingScheduler.java b/src/com/android/tv/dvr/recorder/RecordingScheduler.java
index d631d84f..ea54f8c3 100644
--- a/src/com/android/tv/dvr/recorder/RecordingScheduler.java
+++ b/src/com/android/tv/dvr/recorder/RecordingScheduler.java
@@ -31,10 +31,10 @@ 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.TvSingletons;
+import com.android.tv.TvApplication;
import com.android.tv.common.SoftPreconditions;
-import com.android.tv.common.util.Clock;
import com.android.tv.data.ChannelDataManager;
import com.android.tv.data.ChannelDataManager.Listener;
import com.android.tv.dvr.DvrDataManager;
@@ -43,6 +43,7 @@ import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener;
import com.android.tv.dvr.DvrManager;
import com.android.tv.dvr.WritableDvrDataManager;
import com.android.tv.dvr.data.ScheduledRecording;
+import com.android.tv.util.Clock;
import com.android.tv.util.TvInputManagerHelper;
import com.android.tv.util.Utils;
import java.util.Arrays;
@@ -119,10 +120,10 @@ public class RecordingScheduler extends TvInputCallback implements ScheduledReco
*/
public static RecordingScheduler createScheduler(Context context) {
SoftPreconditions.checkState(
- TvSingletons.getSingletons(context).getRecordingScheduler() == null);
+ TvApplication.getSingletons(context).getRecordingScheduler() == null);
HandlerThread handlerThread = new HandlerThread(HANDLER_THREAD_NAME);
handlerThread.start();
- TvSingletons singletons = TvSingletons.getSingletons(context);
+ ApplicationSingletons singletons = TvApplication.getSingletons(context);
return new RecordingScheduler(
handlerThread.getLooper(),
singletons.getDvrManager(),
diff --git a/src/com/android/tv/dvr/recorder/RecordingTask.java b/src/com/android/tv/dvr/recorder/RecordingTask.java
index 4bd73e8a..85c6a0d5 100644
--- a/src/com/android/tv/dvr/recorder/RecordingTask.java
+++ b/src/com/android/tv/dvr/recorder/RecordingTask.java
@@ -33,15 +33,14 @@ import android.widget.Toast;
import com.android.tv.InputSessionManager;
import com.android.tv.InputSessionManager.RecordingSession;
import com.android.tv.R;
-import com.android.tv.TvSingletons;
+import com.android.tv.TvApplication;
import com.android.tv.common.SoftPreconditions;
-import com.android.tv.common.util.Clock;
-import com.android.tv.common.util.CommonUtils;
import com.android.tv.data.Channel;
import com.android.tv.dvr.DvrManager;
import com.android.tv.dvr.WritableDvrDataManager;
import com.android.tv.dvr.data.ScheduledRecording;
import com.android.tv.dvr.recorder.InputTaskScheduler.HandlerWrapper;
+import com.android.tv.util.Clock;
import com.android.tv.util.Utils;
import java.util.Comparator;
import java.util.concurrent.TimeUnit;
@@ -179,7 +178,7 @@ public class RecordingTask extends RecordingCallback
release();
return false;
default:
- SoftPreconditions.checkArgument(false, TAG, "unexpected message type %s", msg);
+ SoftPreconditions.checkArgument(false, TAG, "unexpected message type " + msg);
break;
}
return true;
@@ -254,7 +253,7 @@ public class RecordingTask extends RecordingCallback
new Runnable() {
@Override
public void run() {
- if (TvSingletons.getSingletons(mContext)
+ if (TvApplication.getSingletons(mContext)
.getMainActivityWrapper()
.isResumed()) {
ScheduledRecording scheduledRecording =
@@ -282,7 +281,7 @@ public class RecordingTask extends RecordingCallback
}
}
});
- // fall through
+ // Pass through
default:
failAndQuit();
break;
@@ -427,7 +426,7 @@ public class RecordingTask extends RecordingCallback
+ " with a delay of "
+ delay / 1000
+ " seconds to arrive at "
- + CommonUtils.toIsoDateTimeString(when));
+ + Utils.toIsoDateTimeString(when));
}
return mHandler.sendEmptyMessageDelayed(what, delay);
}
diff --git a/src/com/android/tv/dvr/recorder/ScheduledProgramReaper.java b/src/com/android/tv/dvr/recorder/ScheduledProgramReaper.java
index f30308f3..c59d4a93 100644
--- a/src/com/android/tv/dvr/recorder/ScheduledProgramReaper.java
+++ b/src/com/android/tv/dvr/recorder/ScheduledProgramReaper.java
@@ -18,10 +18,10 @@ package com.android.tv.dvr.recorder;
import android.support.annotation.MainThread;
import android.support.annotation.VisibleForTesting;
-import com.android.tv.common.util.Clock;
import com.android.tv.dvr.WritableDvrDataManager;
import com.android.tv.dvr.data.ScheduledRecording;
import com.android.tv.dvr.data.SeriesRecording;
+import com.android.tv.util.Clock;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
diff --git a/src/com/android/tv/dvr/recorder/SeriesRecordingScheduler.java b/src/com/android/tv/dvr/recorder/SeriesRecordingScheduler.java
index 4f7a789b..05f876ad 100644
--- a/src/com/android/tv/dvr/recorder/SeriesRecordingScheduler.java
+++ b/src/com/android/tv/dvr/recorder/SeriesRecordingScheduler.java
@@ -27,13 +27,13 @@ import android.text.TextUtils;
import android.util.ArraySet;
import android.util.Log;
import android.util.LongSparseArray;
-import com.android.tv.TvSingletons;
+import com.android.tv.ApplicationSingletons;
+import com.android.tv.TvApplication;
+import com.android.tv.common.CollectionUtils;
+import com.android.tv.common.SharedPreferencesUtils;
import com.android.tv.common.SoftPreconditions;
-import com.android.tv.common.experiments.Experiments;
-import com.android.tv.common.util.CollectionUtils;
-import com.android.tv.common.util.SharedPreferencesUtils;
import com.android.tv.data.Program;
-import com.android.tv.data.epg.EpgReader;
+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;
@@ -44,6 +44,8 @@ import com.android.tv.dvr.data.SeasonEpisodeNumber;
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;
@@ -56,7 +58,6 @@ import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
-import javax.inject.Provider;
/**
* Creates the {@link com.android.tv.dvr.data.ScheduledRecording}s for the {@link
@@ -207,9 +208,9 @@ public class SeriesRecordingScheduler {
private SeriesRecordingScheduler(Context context) {
mContext = context.getApplicationContext();
- TvSingletons tvSingletons = TvSingletons.getSingletons(context);
- mDvrManager = tvSingletons.getDvrManager();
- mDataManager = (WritableDvrDataManager) tvSingletons.getDvrDataManager();
+ ApplicationSingletons appSingletons = TvApplication.getSingletons(context);
+ mDvrManager = appSingletons.getDvrManager();
+ mDataManager = (WritableDvrDataManager) appSingletons.getDvrDataManager();
mSharedPreferences =
context.getSharedPreferences(
SharedPreferencesUtils.SHARED_PREF_SERIES_RECORDINGS, Context.MODE_PRIVATE);
@@ -262,10 +263,7 @@ public class SeriesRecordingScheduler {
private void executeFetchSeriesInfoTask(SeriesRecording seriesRecording) {
if (Experiments.CLOUD_EPG.get()) {
- FetchSeriesInfoTask task =
- new FetchSeriesInfoTask(
- seriesRecording,
- TvSingletons.getSingletons(mContext).providesEpgReader());
+ FetchSeriesInfoTask task = new FetchSeriesInfoTask(seriesRecording);
task.execute();
mFetchSeriesInfoTasks.put(seriesRecording.getId(), task);
}
@@ -536,18 +534,16 @@ public class SeriesRecordingScheduler {
}
private class FetchSeriesInfoTask extends AsyncTask<Void, Void, SeriesInfo> {
- private final SeriesRecording mSeriesRecording;
- private final Provider<EpgReader> mEpgReaderProvider;
+ private SeriesRecording mSeriesRecording;
- FetchSeriesInfoTask(
- SeriesRecording seriesRecording, Provider<EpgReader> epgReaderProvider) {
+ FetchSeriesInfoTask(SeriesRecording seriesRecording) {
mSeriesRecording = seriesRecording;
- mEpgReaderProvider = epgReaderProvider;
}
@Override
protected SeriesInfo doInBackground(Void... voids) {
- return mEpgReaderProvider.get().getSeriesInfo(mSeriesRecording.getSeriesId());
+ return EpgFetcher.createEpgReader(mContext, LocationUtils.getCurrentCountry(mContext))
+ .getSeriesInfo(mSeriesRecording.getSeriesId());
}
@Override
diff --git a/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java b/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java
index fce94230..f4077e44 100644
--- a/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java
@@ -25,7 +25,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.R;
-import com.android.tv.TvSingletons;
+import com.android.tv.TvApplication;
import com.android.tv.data.Program;
import com.android.tv.dvr.DvrManager;
import com.android.tv.dvr.data.RecordedProgram;
@@ -49,7 +49,7 @@ public class DvrAlreadyRecordedFragment extends DvrGuidedStepFragment {
public void onAttach(Context context) {
super.onAttach(context);
mProgram = getArguments().getParcelable(DvrHalfSizedDialogFragment.KEY_PROGRAM);
- DvrManager dvrManager = TvSingletons.getSingletons(context).getDvrManager();
+ DvrManager dvrManager = TvApplication.getSingletons(context).getDvrManager();
mDuplicate =
dvrManager.getRecordedProgram(
mProgram.getTitle(),
diff --git a/src/com/android/tv/dvr/ui/DvrAlreadyScheduledFragment.java b/src/com/android/tv/dvr/ui/DvrAlreadyScheduledFragment.java
index 456ad830..f27ec5c5 100644
--- a/src/com/android/tv/dvr/ui/DvrAlreadyScheduledFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrAlreadyScheduledFragment.java
@@ -26,7 +26,7 @@ import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
import android.support.v17.leanback.widget.GuidedAction;
import android.text.format.DateUtils;
import com.android.tv.R;
-import com.android.tv.TvSingletons;
+import com.android.tv.TvApplication;
import com.android.tv.data.Program;
import com.android.tv.dvr.DvrManager;
import com.android.tv.dvr.data.ScheduledRecording;
@@ -50,7 +50,7 @@ public class DvrAlreadyScheduledFragment extends DvrGuidedStepFragment {
public void onAttach(Context context) {
super.onAttach(context);
mProgram = getArguments().getParcelable(DvrHalfSizedDialogFragment.KEY_PROGRAM);
- DvrManager dvrManager = TvSingletons.getSingletons(context).getDvrManager();
+ DvrManager dvrManager = TvApplication.getSingletons(context).getDvrManager();
mDuplicate =
dvrManager.getScheduledRecording(
mProgram.getTitle(),
diff --git a/src/com/android/tv/dvr/ui/DvrChannelRecordDurationOptionFragment.java b/src/com/android/tv/dvr/ui/DvrChannelRecordDurationOptionFragment.java
index 24a6fcd3..e247b82b 100644
--- a/src/com/android/tv/dvr/ui/DvrChannelRecordDurationOptionFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrChannelRecordDurationOptionFragment.java
@@ -22,7 +22,7 @@ import android.support.v17.leanback.app.GuidedStepFragment;
import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
import android.support.v17.leanback.widget.GuidedAction;
import com.android.tv.R;
-import com.android.tv.TvSingletons;
+import com.android.tv.TvApplication;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.data.Channel;
import com.android.tv.dvr.DvrManager;
@@ -42,7 +42,7 @@ public class DvrChannelRecordDurationOptionFragment extends DvrGuidedStepFragmen
if (args != null) {
long channelId = args.getLong(DvrHalfSizedDialogFragment.KEY_CHANNEL_ID);
mChannel =
- TvSingletons.getSingletons(getContext())
+ TvApplication.getSingletons(getContext())
.getChannelDataManager()
.getChannel(channelId);
}
@@ -90,7 +90,7 @@ public class DvrChannelRecordDurationOptionFragment extends DvrGuidedStepFragmen
@Override
public void onTrackedGuidedActionClicked(GuidedAction action) {
- DvrManager dvrManager = TvSingletons.getSingletons(getContext()).getDvrManager();
+ DvrManager dvrManager = TvApplication.getSingletons(getContext()).getDvrManager();
long duration = mDurations.get((int) action.getId());
long startTimeMs = System.currentTimeMillis();
long endTimeMs = System.currentTimeMillis() + duration;
diff --git a/src/com/android/tv/dvr/ui/DvrConflictFragment.java b/src/com/android/tv/dvr/ui/DvrConflictFragment.java
index 641f86c1..80011acd 100644
--- a/src/com/android/tv/dvr/ui/DvrConflictFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrConflictFragment.java
@@ -29,7 +29,7 @@ import android.view.View;
import android.view.ViewGroup;
import com.android.tv.MainActivity;
import com.android.tv.R;
-import com.android.tv.TvSingletons;
+import com.android.tv.TvApplication;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.data.Channel;
import com.android.tv.data.Program;
@@ -149,7 +149,7 @@ public abstract class DvrConflictFragment extends DvrGuidedStepFragment {
private String getScheduleTitle(ScheduledRecording schedule) {
if (schedule.getType() == ScheduledRecording.TYPE_TIMED) {
Channel channel =
- TvSingletons.getSingletons(getContext())
+ TvApplication.getSingletons(getContext())
.getChannelDataManager()
.getChannel(schedule.getChannelId());
if (channel != null) {
@@ -179,7 +179,7 @@ public abstract class DvrConflictFragment extends DvrGuidedStepFragment {
List<ScheduledRecording> conflicts = null;
if (input != null) {
conflicts =
- TvSingletons.getSingletons(getContext())
+ TvApplication.getSingletons(getContext())
.getDvrManager()
.getConflictingSchedules(mProgram);
}
@@ -227,7 +227,7 @@ public abstract class DvrConflictFragment extends DvrGuidedStepFragment {
Bundle args = getArguments();
long channelId = args.getLong(DvrHalfSizedDialogFragment.KEY_CHANNEL_ID);
mChannel =
- TvSingletons.getSingletons(getContext())
+ TvApplication.getSingletons(getContext())
.getChannelDataManager()
.getChannel(channelId);
SoftPreconditions.checkArgument(mChannel != null);
@@ -238,7 +238,7 @@ public abstract class DvrConflictFragment extends DvrGuidedStepFragment {
mStartTimeMs = args.getLong(DvrHalfSizedDialogFragment.KEY_START_TIME_MS);
mEndTimeMs = args.getLong(DvrHalfSizedDialogFragment.KEY_END_TIME_MS);
conflicts =
- TvSingletons.getSingletons(getContext())
+ TvApplication.getSingletons(getContext())
.getDvrManager()
.getConflictingSchedules(
mChannel.getId(), mStartTimeMs, mEndTimeMs);
diff --git a/src/com/android/tv/dvr/ui/DvrGuidedStepFragment.java b/src/com/android/tv/dvr/ui/DvrGuidedStepFragment.java
index 793bd01b..8524e1ea 100644
--- a/src/com/android/tv/dvr/ui/DvrGuidedStepFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrGuidedStepFragment.java
@@ -26,13 +26,14 @@ 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.TvSingletons;
-import com.android.tv.common.recording.RecordingStorageStatusManager;
+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.DvrStorageStatusManager;
import java.util.List;
public abstract class DvrGuidedStepFragment extends TrackedGuidedStepFragment {
@@ -55,7 +56,7 @@ public abstract class DvrGuidedStepFragment extends TrackedGuidedStepFragment {
@Override
public void onAttach(Context context) {
super.onAttach(context);
- TvSingletons singletons = TvSingletons.getSingletons(context);
+ ApplicationSingletons singletons = TvApplication.getSingletons(context);
mDvrManager = singletons.getDvrManager();
}
@@ -114,8 +115,8 @@ public abstract class DvrGuidedStepFragment extends TrackedGuidedStepFragment {
}
/**
- * The inner guided step fragment for {@link
- * com.android.tv.dvr.ui.DvrHalfSizedDialogFragment .DvrNoFreeSpaceErrorDialogFragment}.
+ * The inner guided step fragment for {@link com.android.tv.dvr.ui.DvrHalfSizedDialogFragment
+ * .DvrNoFreeSpaceErrorDialogFragment}.
*/
public static class DvrNoFreeSpaceErrorFragment extends DvrGuidedStepFragment {
@Override
@@ -154,8 +155,7 @@ public abstract class DvrGuidedStepFragment extends TrackedGuidedStepFragment {
}
/**
- * The inner guided step fragment for {@link
- * com.android.tv.dvr.ui.DvrHalfSizedDialogFragment
+ * The inner guided step fragment for {@link com.android.tv.dvr.ui.DvrHalfSizedDialogFragment
* .DvrSmallSizedStorageErrorDialogFragment}.
*/
public static class DvrSmallSizedStorageErrorFragment extends DvrGuidedStepFragment {
@@ -166,7 +166,7 @@ public abstract class DvrGuidedStepFragment extends TrackedGuidedStepFragment {
getResources()
.getString(
R.string.dvr_error_small_sized_storage_description,
- RecordingStorageStatusManager.MIN_STORAGE_SIZE_FOR_DVR_IN_BYTES
+ DvrStorageStatusManager.MIN_STORAGE_SIZE_FOR_DVR_IN_BYTES
/ 1024
/ 1024
/ 1024);
diff --git a/src/com/android/tv/dvr/ui/DvrInsufficientSpaceErrorFragment.java b/src/com/android/tv/dvr/ui/DvrInsufficientSpaceErrorFragment.java
index 6fba4d98..ad26a5c2 100644
--- a/src/com/android/tv/dvr/ui/DvrInsufficientSpaceErrorFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrInsufficientSpaceErrorFragment.java
@@ -23,7 +23,7 @@ import android.os.Bundle;
import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
import android.support.v17.leanback.widget.GuidedAction;
import com.android.tv.R;
-import com.android.tv.TvSingletons;
+import com.android.tv.TvApplication;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.dvr.ui.browse.DvrBrowseActivity;
import java.util.ArrayList;
@@ -102,7 +102,7 @@ public class DvrInsufficientSpaceErrorFragment extends DvrGuidedStepFragment {
Activity activity = getActivity();
actions.add(
new GuidedAction.Builder(activity).clickAction(GuidedAction.ACTION_ID_OK).build());
- if (TvSingletons.getSingletons(getContext()).getDvrManager().hasValidItems()) {
+ if (TvApplication.getSingletons(getContext()).getDvrManager().hasValidItems()) {
actions.add(
new GuidedAction.Builder(activity)
.id(ACTION_VIEW_RECENT_RECORDINGS)
diff --git a/src/com/android/tv/dvr/ui/DvrPrioritySettingsFragment.java b/src/com/android/tv/dvr/ui/DvrPrioritySettingsFragment.java
index 5bb97e90..03124260 100644
--- a/src/com/android/tv/dvr/ui/DvrPrioritySettingsFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrPrioritySettingsFragment.java
@@ -16,11 +16,9 @@
package com.android.tv.dvr.ui;
-import android.annotation.TargetApi;
import android.app.FragmentManager;
import android.content.Context;
import android.graphics.Typeface;
-import android.os.Build;
import android.os.Bundle;
import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
import android.support.v17.leanback.widget.GuidedAction;
@@ -29,7 +27,7 @@ import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import com.android.tv.R;
-import com.android.tv.TvSingletons;
+import com.android.tv.TvApplication;
import com.android.tv.dvr.DvrDataManager;
import com.android.tv.dvr.DvrManager;
import com.android.tv.dvr.DvrScheduleManager;
@@ -38,8 +36,6 @@ import java.util.ArrayList;
import java.util.List;
/** Fragment for DVR series recording settings. */
-@TargetApi(Build.VERSION_CODES.N)
-@SuppressWarnings("AndroidApiChecker") // TODO(b/32513850) remove when error prone is updated
public class DvrPrioritySettingsFragment extends TrackedGuidedStepFragment {
/** Name of series recording id starting the fragment. Type: Long */
public static final String COME_FROM_SERIES_RECORDING_ID = "series_recording_id";
@@ -66,7 +62,7 @@ public class DvrPrioritySettingsFragment extends TrackedGuidedStepFragment {
.setPriority(Long.MAX_VALUE)
.setId(ONE_TIME_RECORDING_ID)
.build());
- DvrDataManager dvrDataManager = TvSingletons.getSingletons(context).getDvrDataManager();
+ DvrDataManager dvrDataManager = TvApplication.getSingletons(context).getDvrDataManager();
long comeFromSeriesRecordingId = getArguments().getLong(COME_FROM_SERIES_RECORDING_ID, -1);
for (SeriesRecording series : dvrDataManager.getSeriesRecordings()) {
if (series.getState() == SeriesRecording.STATE_SERIES_NORMAL
@@ -131,7 +127,7 @@ public class DvrPrioritySettingsFragment extends TrackedGuidedStepFragment {
public void onTrackedGuidedActionClicked(GuidedAction action) {
long actionId = action.getId();
if (actionId == ACTION_ID_SAVE) {
- DvrManager dvrManager = TvSingletons.getSingletons(getContext()).getDvrManager();
+ DvrManager dvrManager = TvApplication.getSingletons(getContext()).getDvrManager();
int size = mSeriesRecordings.size();
for (int i = 1; i < size; ++i) {
long priority = DvrScheduleManager.suggestSeriesPriority(size - i);
diff --git a/src/com/android/tv/dvr/ui/DvrScheduleFragment.java b/src/com/android/tv/dvr/ui/DvrScheduleFragment.java
index 5251e140..854fea56 100644
--- a/src/com/android/tv/dvr/ui/DvrScheduleFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrScheduleFragment.java
@@ -27,7 +27,7 @@ import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
import android.support.v17.leanback.widget.GuidedAction;
import android.text.format.DateUtils;
import com.android.tv.R;
-import com.android.tv.TvSingletons;
+import com.android.tv.TvApplication;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.data.Program;
import com.android.tv.dvr.DvrManager;
@@ -63,18 +63,16 @@ public class DvrScheduleFragment extends DvrGuidedStepFragment {
mProgram = args.getParcelable(DvrHalfSizedDialogFragment.KEY_PROGRAM);
mAddCurrentProgramToSeries = args.getBoolean(KEY_ADD_CURRENT_PROGRAM_TO_SERIES, false);
}
- DvrManager dvrManager = TvSingletons.getSingletons(getContext()).getDvrManager();
+ DvrManager dvrManager = TvApplication.getSingletons(getContext()).getDvrManager();
SoftPreconditions.checkArgument(
mProgram != null && mProgram.isEpisodic(),
TAG,
- "The program should be episodic: %s ",
- mProgram);
+ "The program should be episodic: " + mProgram);
SeriesRecording seriesRecording = dvrManager.getSeriesRecording(mProgram);
SoftPreconditions.checkArgument(
seriesRecording == null || seriesRecording.isStopped(),
TAG,
- "The series recording should be stopped or null: %s",
- seriesRecording);
+ "The series recording should be stopped or null: " + seriesRecording);
super.onCreate(savedInstanceState);
}
@@ -146,7 +144,7 @@ public class DvrScheduleFragment extends DvrGuidedStepFragment {
}
} else if (action.getId() == ACTION_RECORD_SERIES) {
SeriesRecording seriesRecording =
- TvSingletons.getSingletons(getContext())
+ TvApplication.getSingletons(getContext())
.getDvrDataManager()
.getSeriesRecording(mProgram.getSeriesId());
if (seriesRecording == null) {
@@ -161,7 +159,7 @@ public class DvrScheduleFragment extends DvrGuidedStepFragment {
seriesRecording =
SeriesRecording.buildFrom(seriesRecording)
.setPriority(
- TvSingletons.getSingletons(getContext())
+ TvApplication.getSingletons(getContext())
.getDvrScheduleManager()
.suggestNewSeriesPriority())
.build();
diff --git a/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java b/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java
index a2ae1f97..8b05cf1c 100644
--- a/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java
+++ b/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java
@@ -20,7 +20,7 @@ import android.app.Activity;
import android.os.Bundle;
import android.support.v17.leanback.app.GuidedStepFragment;
import com.android.tv.R;
-import com.android.tv.Starter;
+import com.android.tv.TvApplication;
/** Activity to show details view in DVR. */
public class DvrSeriesDeletionActivity extends Activity {
@@ -29,7 +29,7 @@ public class DvrSeriesDeletionActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
- Starter.start(this);
+ TvApplication.setCurrentRunningProcess(this, true);
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_dvr_series_settings);
// Check savedInstanceState to prevent that activity is being showed with animation.
diff --git a/src/com/android/tv/dvr/ui/DvrSeriesDeletionFragment.java b/src/com/android/tv/dvr/ui/DvrSeriesDeletionFragment.java
index 685f0a58..5f2c3582 100644
--- a/src/com/android/tv/dvr/ui/DvrSeriesDeletionFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrSeriesDeletionFragment.java
@@ -27,7 +27,7 @@ import android.text.TextUtils;
import android.view.ViewGroup.LayoutParams;
import android.widget.Toast;
import com.android.tv.R;
-import com.android.tv.TvSingletons;
+import com.android.tv.TvApplication;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.dvr.DvrDataManager;
import com.android.tv.dvr.DvrManager;
@@ -67,9 +67,9 @@ public class DvrSeriesDeletionFragment extends GuidedStepFragment {
mSeriesRecordingId =
getArguments().getLong(DvrSeriesDeletionActivity.SERIES_RECORDING_ID, -1);
SoftPreconditions.checkArgument(mSeriesRecordingId != -1);
- mDvrDataManager = TvSingletons.getSingletons(context).getDvrDataManager();
+ mDvrDataManager = TvApplication.getSingletons(context).getDvrDataManager();
mDvrWatchedPositionManager =
- TvSingletons.getSingletons(context).getDvrWatchedPositionManager();
+ TvApplication.getSingletons(context).getDvrWatchedPositionManager();
mRecordings = mDvrDataManager.getRecordedPrograms(mSeriesRecordingId);
mOneLineActionHeight =
getResources()
@@ -166,7 +166,7 @@ public class DvrSeriesDeletionFragment extends GuidedStepFragment {
}
}
if (!idsToDelete.isEmpty()) {
- DvrManager dvrManager = TvSingletons.getSingletons(getActivity()).getDvrManager();
+ DvrManager dvrManager = TvApplication.getSingletons(getActivity()).getDvrManager();
dvrManager.removeRecordedPrograms(idsToDelete);
}
Toast.makeText(
diff --git a/src/com/android/tv/dvr/ui/DvrSeriesScheduledFragment.java b/src/com/android/tv/dvr/ui/DvrSeriesScheduledFragment.java
index edb62c96..d600b54d 100644
--- a/src/com/android/tv/dvr/ui/DvrSeriesScheduledFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrSeriesScheduledFragment.java
@@ -23,7 +23,7 @@ import android.os.Bundle;
import android.support.v17.leanback.widget.GuidanceStylist;
import android.support.v17.leanback.widget.GuidedAction;
import com.android.tv.R;
-import com.android.tv.TvSingletons;
+import com.android.tv.TvApplication;
import com.android.tv.data.Program;
import com.android.tv.dvr.DvrScheduleManager;
import com.android.tv.dvr.data.ScheduledRecording;
@@ -68,7 +68,7 @@ public class DvrSeriesScheduledFragment extends DvrGuidedStepFragment {
getArguments()
.getBoolean(DvrSeriesScheduledDialogActivity.SHOW_VIEW_SCHEDULE_OPTION);
mSeriesRecording =
- TvSingletons.getSingletons(context)
+ TvApplication.getSingletons(context)
.getDvrDataManager()
.getSeriesRecording(seriesRecordingId);
if (mSeriesRecording == null) {
@@ -78,12 +78,12 @@ public class DvrSeriesScheduledFragment extends DvrGuidedStepFragment {
mPrograms = (List<Program>) BigArguments.getArgument(SERIES_SCHEDULED_KEY_PROGRAMS);
BigArguments.reset();
mSchedulesAddedCount =
- TvSingletons.getSingletons(getContext())
+ TvApplication.getSingletons(getContext())
.getDvrManager()
.getAvailableScheduledRecording(mSeriesRecording.getId())
.size();
DvrScheduleManager dvrScheduleManager =
- TvSingletons.getSingletons(context).getDvrScheduleManager();
+ TvApplication.getSingletons(context).getDvrScheduleManager();
List<ScheduledRecording> conflictingRecordings =
dvrScheduleManager.getConflictingSchedules(mSeriesRecording);
mHasConflict = !conflictingRecordings.isEmpty();
diff --git a/src/com/android/tv/dvr/ui/DvrSeriesSettingsActivity.java b/src/com/android/tv/dvr/ui/DvrSeriesSettingsActivity.java
index 1a51cf46..117f72d8 100644
--- a/src/com/android/tv/dvr/ui/DvrSeriesSettingsActivity.java
+++ b/src/com/android/tv/dvr/ui/DvrSeriesSettingsActivity.java
@@ -21,7 +21,7 @@ import android.graphics.drawable.ColorDrawable;
import android.os.Bundle;
import android.support.v17.leanback.app.GuidedStepFragment;
import com.android.tv.R;
-import com.android.tv.Starter;
+import com.android.tv.TvApplication;
import com.android.tv.common.SoftPreconditions;
/** Activity to show details view in DVR. */
@@ -60,7 +60,7 @@ public class DvrSeriesSettingsActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
- Starter.start(this);
+ TvApplication.setCurrentRunningProcess(this, true);
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_dvr_series_settings);
long seriesRecordingId = getIntent().getLongExtra(SERIES_RECORDING_ID, -1);
diff --git a/src/com/android/tv/dvr/ui/DvrSeriesSettingsFragment.java b/src/com/android/tv/dvr/ui/DvrSeriesSettingsFragment.java
index 9383058a..c44e44a3 100644
--- a/src/com/android/tv/dvr/ui/DvrSeriesSettingsFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrSeriesSettingsFragment.java
@@ -16,10 +16,8 @@
package com.android.tv.dvr.ui;
-import android.annotation.TargetApi;
import android.app.FragmentManager;
import android.content.Context;
-import android.os.Build;
import android.os.Bundle;
import android.support.v17.leanback.app.GuidedStepFragment;
import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
@@ -27,7 +25,7 @@ import android.support.v17.leanback.widget.GuidedAction;
import android.support.v17.leanback.widget.GuidedActionsStylist;
import android.util.LongSparseArray;
import com.android.tv.R;
-import com.android.tv.TvSingletons;
+import com.android.tv.TvApplication;
import com.android.tv.data.Channel;
import com.android.tv.data.ChannelDataManager;
import com.android.tv.data.Program;
@@ -45,8 +43,6 @@ import java.util.List;
import java.util.Set;
/** Fragment for DVR series recording settings. */
-@TargetApi(Build.VERSION_CODES.N)
-@SuppressWarnings("AndroidApiChecker") // TODO(b/32513850) remove when error prone is updated
public class DvrSeriesSettingsFragment extends GuidedStepFragment
implements DvrDataManager.SeriesRecordingListener {
private static final String TAG = "SeriesSettingsFragment";
@@ -85,7 +81,7 @@ public class DvrSeriesSettingsFragment extends GuidedStepFragment
public void onAttach(Context context) {
super.onAttach(context);
mBackStackCount = getFragmentManager().getBackStackEntryCount();
- mDvrDataManager = TvSingletons.getSingletons(context).getDvrDataManager();
+ mDvrDataManager = TvApplication.getSingletons(context).getDvrDataManager();
mSeriesRecordingId = getArguments().getLong(DvrSeriesSettingsActivity.SERIES_RECORDING_ID);
mSeriesRecording = mDvrDataManager.getSeriesRecording(mSeriesRecordingId);
if (mSeriesRecording == null) {
@@ -106,7 +102,7 @@ public class DvrSeriesSettingsFragment extends GuidedStepFragment
}
Set<Long> channelIds = new HashSet<>();
ChannelDataManager channelDataManager =
- TvSingletons.getSingletons(context).getChannelDataManager();
+ TvApplication.getSingletons(context).getChannelDataManager();
for (Program program : mPrograms) {
long channelId = program.getChannelId();
if (channelIds.add(channelId)) {
@@ -212,7 +208,7 @@ public class DvrSeriesSettingsFragment extends GuidedStepFragment
if (mSelectedChannelId != Channel.INVALID_ID) {
builder.setChannelId(mSelectedChannelId);
}
- DvrManager dvrManager = TvSingletons.getSingletons(getContext()).getDvrManager();
+ DvrManager dvrManager = TvApplication.getSingletons(getContext()).getDvrManager();
dvrManager.updateSeriesRecording(builder.build());
if (mCurrentProgram != null
&& (mChannelOption == SeriesRecording.OPTION_CHANNEL_ALL
@@ -332,7 +328,7 @@ public class DvrSeriesSettingsFragment extends GuidedStepFragment
recordingCandidates)
.get(mSeriesRecordingId);
if (!programsToSchedule.isEmpty()) {
- TvSingletons.getSingletons(getContext())
+ TvApplication.getSingletons(getContext())
.getDvrManager()
.addScheduleToSeriesRecording(mSeriesRecording, programsToSchedule);
}
diff --git a/src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java b/src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java
index e93387ab..6f34e8a0 100644
--- a/src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java
@@ -26,7 +26,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.R;
-import com.android.tv.TvSingletons;
+import com.android.tv.TvApplication;
import com.android.tv.dvr.DvrDataManager;
import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener;
import com.android.tv.dvr.data.ScheduledRecording;
@@ -100,7 +100,7 @@ public class DvrStopRecordingFragment extends DvrGuidedStepFragment {
dismissDialog();
return;
}
- mDvrDataManager = TvSingletons.getSingletons(context).getDvrDataManager();
+ mDvrDataManager = TvApplication.getSingletons(context).getDvrDataManager();
mDvrDataManager.addScheduledRecordingListener(mScheduledRecordingListener);
mStopReason = args.getInt(KEY_REASON);
}
diff --git a/src/com/android/tv/dvr/ui/DvrStopSeriesRecordingFragment.java b/src/com/android/tv/dvr/ui/DvrStopSeriesRecordingFragment.java
index 99211fdb..3d84f48f 100644
--- a/src/com/android/tv/dvr/ui/DvrStopSeriesRecordingFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrStopSeriesRecordingFragment.java
@@ -25,8 +25,9 @@ import android.support.v17.leanback.widget.GuidedAction;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import com.android.tv.ApplicationSingletons;
import com.android.tv.R;
-import com.android.tv.TvSingletons;
+import com.android.tv.TvApplication;
import com.android.tv.dvr.DvrDataManager;
import com.android.tv.dvr.DvrManager;
import com.android.tv.dvr.data.ScheduledRecording;
@@ -76,7 +77,7 @@ public class DvrStopSeriesRecordingFragment extends DvrGuidedStepFragment {
@Override
public void onTrackedGuidedActionClicked(GuidedAction action) {
if (action.getId() == ACTION_STOP_SERIES_RECORDING) {
- TvSingletons singletons = TvSingletons.getSingletons(getContext());
+ ApplicationSingletons singletons = TvApplication.getSingletons(getContext());
DvrManager dvrManager = singletons.getDvrManager();
DvrDataManager dataManager = singletons.getDvrDataManager();
List<ScheduledRecording> toDelete = new ArrayList<>();
diff --git a/src/com/android/tv/dvr/ui/DvrUiHelper.java b/src/com/android/tv/dvr/ui/DvrUiHelper.java
index 6373b30f..ae60f4a4 100644
--- a/src/com/android/tv/dvr/ui/DvrUiHelper.java
+++ b/src/com/android/tv/dvr/ui/DvrUiHelper.java
@@ -39,15 +39,14 @@ import android.widget.ImageView;
import android.widget.Toast;
import com.android.tv.MainActivity;
import com.android.tv.R;
-import com.android.tv.TvSingletons;
+import com.android.tv.TvApplication;
import com.android.tv.common.SoftPreconditions;
-import com.android.tv.common.recording.RecordingStorageStatusManager;
-import com.android.tv.common.util.CommonUtils;
import com.android.tv.data.BaseProgram;
import com.android.tv.data.Channel;
import com.android.tv.data.Program;
import com.android.tv.dialog.HalfSizedDialogFragment;
import com.android.tv.dvr.DvrManager;
+import com.android.tv.dvr.DvrStorageStatusManager;
import com.android.tv.dvr.data.RecordedProgram;
import com.android.tv.dvr.data.ScheduledRecording;
import com.android.tv.dvr.data.SeriesRecording;
@@ -92,17 +91,17 @@ public class DvrUiHelper {
*/
public static void checkStorageStatusAndShowErrorMessage(
Activity activity, String inputId, Runnable recordingRequestRunnable) {
- if (CommonUtils.isBundledInput(inputId)) {
- switch (TvSingletons.getSingletons(activity)
- .getRecordingStorageStatusManager()
+ if (Utils.isBundledInput(inputId)) {
+ switch (TvApplication.getSingletons(activity)
+ .getDvrStorageStatusManager()
.getDvrStorageStatus()) {
- case RecordingStorageStatusManager.STORAGE_STATUS_TOTAL_CAPACITY_TOO_SMALL:
+ case DvrStorageStatusManager.STORAGE_STATUS_TOTAL_CAPACITY_TOO_SMALL:
showDvrSmallSizedStorageErrorDialog(activity);
return;
- case RecordingStorageStatusManager.STORAGE_STATUS_MISSING:
+ case DvrStorageStatusManager.STORAGE_STATUS_MISSING:
showDvrMissingStorageErrorDialog(activity);
return;
- case RecordingStorageStatusManager.STORAGE_STATUS_FREE_SPACE_INSUFFICIENT:
+ case DvrStorageStatusManager.STORAGE_STATUS_FREE_SPACE_INSUFFICIENT:
showDvrNoFreeSpaceErrorDialog(activity, recordingRequestRunnable);
return;
}
@@ -282,7 +281,7 @@ public class DvrUiHelper {
if (program == null) {
return false;
}
- DvrManager dvrManager = TvSingletons.getSingletons(activity).getDvrManager();
+ DvrManager dvrManager = TvApplication.getSingletons(activity).getDvrManager();
if (!program.isEpisodic()) {
// One time recording.
dvrManager.addSchedule(program);
@@ -393,7 +392,7 @@ public class DvrUiHelper {
return;
}
List<ScheduledRecording> conflicts =
- TvSingletons.getSingletons(context)
+ TvApplication.getSingletons(context)
.getDvrManager()
.getConflictingSchedulesForTune(channel.getId());
startSchedulesActivity(context, getEarliestScheduledRecording(conflicts));
@@ -444,7 +443,7 @@ public class DvrUiHelper {
boolean showViewScheduleOptionInDialog,
Program currentProgram) {
SeriesRecording series =
- TvSingletons.getSingletons(context)
+ TvApplication.getSingletons(context)
.getDvrDataManager()
.getSeriesRecording(seriesRecordingId);
if (series == null) {
diff --git a/src/com/android/tv/dvr/ui/TrackedGuidedStepFragment.java b/src/com/android/tv/dvr/ui/TrackedGuidedStepFragment.java
index 0172f76f..0a24187a 100644
--- a/src/com/android/tv/dvr/ui/TrackedGuidedStepFragment.java
+++ b/src/com/android/tv/dvr/ui/TrackedGuidedStepFragment.java
@@ -19,7 +19,7 @@ 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.TvSingletons;
+import com.android.tv.TvApplication;
import com.android.tv.analytics.Tracker;
/** A {@link GuidedStepFragment} with {@link Tracker} for analytics. */
@@ -29,7 +29,7 @@ public abstract class TrackedGuidedStepFragment extends GuidedStepFragment {
@Override
public void onAttach(Context context) {
super.onAttach(context);
- mTracker = TvSingletons.getSingletons(context).getAnalytics().getDefaultTracker();
+ mTracker = TvApplication.getSingletons(context).getAnalytics().getDefaultTracker();
}
@Override
diff --git a/src/com/android/tv/dvr/ui/browse/CurrentRecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/CurrentRecordingDetailsFragment.java
index 7e7e1f75..22246e5a 100644
--- a/src/com/android/tv/dvr/ui/browse/CurrentRecordingDetailsFragment.java
+++ b/src/com/android/tv/dvr/ui/browse/CurrentRecordingDetailsFragment.java
@@ -22,7 +22,7 @@ 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.TvSingletons;
+import com.android.tv.TvApplication;
import com.android.tv.dialog.HalfSizedDialogFragment;
import com.android.tv.dvr.DvrDataManager;
import com.android.tv.dvr.DvrManager;
@@ -66,7 +66,7 @@ public class CurrentRecordingDetailsFragment extends RecordingDetailsFragment {
@Override
public void onAttach(Context context) {
super.onAttach(context);
- mDvrDataManger = TvSingletons.getSingletons(context).getDvrDataManager();
+ mDvrDataManger = TvApplication.getSingletons(context).getDvrDataManager();
mDvrDataManger.addScheduledRecordingListener(mScheduledRecordingListener);
}
@@ -100,7 +100,7 @@ public class CurrentRecordingDetailsFragment extends RecordingDetailsFragment {
public void onActionClick(long actionId) {
if (actionId == DvrStopRecordingFragment.ACTION_STOP) {
DvrManager dvrManager =
- TvSingletons.getSingletons(getContext())
+ TvApplication.getSingletons(getContext())
.getDvrManager();
dvrManager.stopRecording(getRecording());
getActivity().finish();
diff --git a/src/com/android/tv/dvr/ui/browse/DetailsContent.java b/src/com/android/tv/dvr/ui/browse/DetailsContent.java
index 70903373..9f588aa3 100644
--- a/src/com/android/tv/dvr/ui/browse/DetailsContent.java
+++ b/src/com/android/tv/dvr/ui/browse/DetailsContent.java
@@ -21,7 +21,7 @@ import android.media.tv.TvContract;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import com.android.tv.R;
-import com.android.tv.TvSingletons;
+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;
@@ -76,7 +76,7 @@ class DetailsContent {
static DetailsContent createFromScheduledRecording(
Context context, ScheduledRecording scheduledRecording) {
Channel channel =
- TvSingletons.getSingletons(context)
+ TvApplication.getSingletons(context)
.getChannelDataManager()
.getChannel(scheduledRecording.getChannelId());
String description =
@@ -278,7 +278,7 @@ class DetailsContent {
/** Builds details content. */
public DetailsContent build(Context context) {
Channel channel =
- TvSingletons.getSingletons(context)
+ TvApplication.getSingletons(context)
.getChannelDataManager()
.getChannel(mChannelId);
if (mDetailsContent.mTitle == null) {
diff --git a/src/com/android/tv/dvr/ui/browse/DetailsViewBackgroundHelper.java b/src/com/android/tv/dvr/ui/browse/DetailsViewBackgroundHelper.java
index 849360b8..5a058454 100644
--- a/src/com/android/tv/dvr/ui/browse/DetailsViewBackgroundHelper.java
+++ b/src/com/android/tv/dvr/ui/browse/DetailsViewBackgroundHelper.java
@@ -57,7 +57,6 @@ class DetailsViewBackgroundHelper {
public DetailsViewBackgroundHelper(Activity activity) {
mBackgroundManager = BackgroundManager.getInstance(activity);
mBackgroundManager.attach(activity.getWindow());
- mBackgroundManager.setAutoReleaseOnStop(false);
}
/** Sets the given image to background. */
diff --git a/src/com/android/tv/dvr/ui/browse/DvrBrowseActivity.java b/src/com/android/tv/dvr/ui/browse/DvrBrowseActivity.java
index 6cc1c7a1..f208b5e8 100644
--- a/src/com/android/tv/dvr/ui/browse/DvrBrowseActivity.java
+++ b/src/com/android/tv/dvr/ui/browse/DvrBrowseActivity.java
@@ -21,7 +21,7 @@ import android.content.Intent;
import android.media.tv.TvInputManager;
import android.os.Bundle;
import com.android.tv.R;
-import com.android.tv.Starter;
+import com.android.tv.TvApplication;
/** {@link android.app.Activity} for DVR UI. */
public class DvrBrowseActivity extends Activity {
@@ -29,7 +29,7 @@ public class DvrBrowseActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
- Starter.start(this);
+ TvApplication.setCurrentRunningProcess(this, true);
super.onCreate(savedInstanceState);
setContentView(R.layout.dvr_main);
mFragment = (DvrBrowseFragment) getFragmentManager().findFragmentById(R.id.dvr_frame);
diff --git a/src/com/android/tv/dvr/ui/browse/DvrBrowseFragment.java b/src/com/android/tv/dvr/ui/browse/DvrBrowseFragment.java
index 90326a8b..f8a54ef0 100644
--- a/src/com/android/tv/dvr/ui/browse/DvrBrowseFragment.java
+++ b/src/com/android/tv/dvr/ui/browse/DvrBrowseFragment.java
@@ -16,9 +16,7 @@
package com.android.tv.dvr.ui.browse;
-import android.annotation.TargetApi;
import android.content.Context;
-import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.support.v17.leanback.app.BrowseFragment;
@@ -31,8 +29,9 @@ import android.support.v17.leanback.widget.TitleViewAdapter;
import android.util.Log;
import android.view.View;
import android.view.ViewTreeObserver.OnGlobalFocusChangeListener;
+import com.android.tv.ApplicationSingletons;
import com.android.tv.R;
-import com.android.tv.TvSingletons;
+import com.android.tv.TvApplication;
import com.android.tv.data.GenreItems;
import com.android.tv.dvr.DvrDataManager;
import com.android.tv.dvr.DvrDataManager.OnDvrScheduleLoadFinishedListener;
@@ -52,8 +51,6 @@ import java.util.HashMap;
import java.util.List;
/** {@link BrowseFragment} for DVR functions. */
-@TargetApi(Build.VERSION_CODES.N)
-@SuppressWarnings("AndroidApiChecker") // TODO(b/32513850) remove when error prone is updated
public class DvrBrowseFragment extends BrowseFragment
implements RecordedProgramListener,
ScheduledRecordingListener,
@@ -171,7 +168,7 @@ public class DvrBrowseFragment extends BrowseFragment
if (DEBUG) Log.d(TAG, "onCreate");
super.onCreate(savedInstanceState);
Context context = getContext();
- TvSingletons singletons = TvSingletons.getSingletons(context);
+ ApplicationSingletons singletons = TvApplication.getSingletons(context);
mDvrDataManager = singletons.getDvrDataManager();
mDvrScheudleManager = singletons.getDvrScheduleManager();
mPresenterSelector =
diff --git a/src/com/android/tv/dvr/ui/browse/DvrDetailsActivity.java b/src/com/android/tv/dvr/ui/browse/DvrDetailsActivity.java
index 2659c3f3..a953f1d2 100644
--- a/src/com/android/tv/dvr/ui/browse/DvrDetailsActivity.java
+++ b/src/com/android/tv/dvr/ui/browse/DvrDetailsActivity.java
@@ -23,7 +23,7 @@ import android.transition.Transition;
import android.transition.Transition.TransitionListener;
import android.view.View;
import com.android.tv.R;
-import com.android.tv.Starter;
+import com.android.tv.TvApplication;
import com.android.tv.dialog.PinDialogFragment;
/** Activity to show details view in DVR. */
@@ -59,7 +59,7 @@ public class DvrDetailsActivity extends Activity implements PinDialogFragment.On
@Override
public void onCreate(Bundle savedInstanceState) {
- Starter.start(this);
+ TvApplication.setCurrentRunningProcess(this, true);
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_dvr_details);
long recordId = getIntent().getLongExtra(RECORDING_ID, -1);
diff --git a/src/com/android/tv/dvr/ui/browse/DvrDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/DvrDetailsFragment.java
index 209fc6e1..f03f3f58 100644
--- a/src/com/android/tv/dvr/ui/browse/DvrDetailsFragment.java
+++ b/src/com/android/tv/dvr/ui/browse/DvrDetailsFragment.java
@@ -37,9 +37,8 @@ import android.support.v17.leanback.widget.VerticalGridView;
import android.text.TextUtils;
import android.widget.Toast;
import com.android.tv.R;
-import com.android.tv.TvSingletons;
+import com.android.tv.TvApplication;
import com.android.tv.common.SoftPreconditions;
-import com.android.tv.common.util.CommonUtils;
import com.android.tv.data.Channel;
import com.android.tv.data.ChannelDataManager;
import com.android.tv.dialog.PinDialogFragment;
@@ -49,6 +48,7 @@ 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;
+import com.android.tv.util.Utils;
import java.io.File;
abstract class DvrDetailsFragment extends DetailsFragment {
@@ -195,7 +195,7 @@ abstract class DvrDetailsFragment extends DetailsFragment {
}
protected void startPlayback(RecordedProgram recordedProgram, long seekTimeMs) {
- if (CommonUtils.isInBundledPackageSet(recordedProgram.getPackageName())
+ if (Utils.isInBundledPackageSet(recordedProgram.getPackageName())
&& !isDataUriAccessible(recordedProgram.getDataUri())) {
// Since cleaning RecordedProgram from forgotten storage will take some time,
// ignore playback until cleaning is finished.
@@ -207,7 +207,7 @@ abstract class DvrDetailsFragment extends DetailsFragment {
}
long programId = recordedProgram.getId();
ParentalControlSettings parental =
- TvSingletons.getSingletons(getActivity())
+ TvApplication.getSingletons(getActivity())
.getTvInputManagerHelper()
.getParentalControlSettings();
if (!parental.isParentalControlsEnabled()) {
@@ -215,7 +215,7 @@ abstract class DvrDetailsFragment extends DetailsFragment {
return;
}
ChannelDataManager channelDataManager =
- TvSingletons.getSingletons(getActivity()).getChannelDataManager();
+ TvApplication.getSingletons(getActivity()).getChannelDataManager();
Channel channel = channelDataManager.getChannel(recordedProgram.getChannelId());
if (channel != null && channel.isLocked()) {
checkPinToPlay(recordedProgram, seekTimeMs);
diff --git a/src/com/android/tv/dvr/ui/browse/DvrItemPresenter.java b/src/com/android/tv/dvr/ui/browse/DvrItemPresenter.java
index 1e5f6935..4298d86a 100644
--- a/src/com/android/tv/dvr/ui/browse/DvrItemPresenter.java
+++ b/src/com/android/tv/dvr/ui/browse/DvrItemPresenter.java
@@ -31,9 +31,8 @@ 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}.
+ * 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;
diff --git a/src/com/android/tv/dvr/ui/browse/FullSchedulesCardPresenter.java b/src/com/android/tv/dvr/ui/browse/FullSchedulesCardPresenter.java
index af0f24c0..88133331 100644
--- a/src/com/android/tv/dvr/ui/browse/FullSchedulesCardPresenter.java
+++ b/src/com/android/tv/dvr/ui/browse/FullSchedulesCardPresenter.java
@@ -20,7 +20,7 @@ import android.content.Context;
import android.graphics.drawable.Drawable;
import android.view.View;
import com.android.tv.R;
-import com.android.tv.TvSingletons;
+import com.android.tv.TvApplication;
import com.android.tv.dvr.data.ScheduledRecording;
import com.android.tv.dvr.ui.DvrUiHelper;
import com.android.tv.util.Utils;
@@ -50,7 +50,7 @@ class FullSchedulesCardPresenter extends DvrItemPresenter<Object> {
cardView.setTitle(mCardTitleText);
cardView.setImage(mIconDrawable);
List<ScheduledRecording> scheduledRecordings =
- TvSingletons.getSingletons(mContext)
+ TvApplication.getSingletons(mContext)
.getDvrDataManager()
.getAvailableScheduledRecordings();
int fullDays = 0;
diff --git a/src/com/android/tv/dvr/ui/browse/RecordedProgramDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/RecordedProgramDetailsFragment.java
index 47b1a198..3b3401b2 100644
--- a/src/com/android/tv/dvr/ui/browse/RecordedProgramDetailsFragment.java
+++ b/src/com/android/tv/dvr/ui/browse/RecordedProgramDetailsFragment.java
@@ -23,7 +23,7 @@ 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.TvSingletons;
+import com.android.tv.TvApplication;
import com.android.tv.dvr.DvrDataManager;
import com.android.tv.dvr.DvrManager;
import com.android.tv.dvr.DvrWatchedPositionManager;
@@ -44,7 +44,7 @@ public class RecordedProgramDetailsFragment extends DvrDetailsFragment
@Override
public void onCreate(Bundle savedInstanceState) {
- mDvrDataManager = TvSingletons.getSingletons(getContext()).getDvrDataManager();
+ mDvrDataManager = TvApplication.getSingletons(getContext()).getDvrDataManager();
mDvrDataManager.addRecordedProgramListener(this);
super.onCreate(savedInstanceState);
}
@@ -52,7 +52,7 @@ public class RecordedProgramDetailsFragment extends DvrDetailsFragment
@Override
public void onCreateInternal() {
mDvrWatchedPositionManager =
- TvSingletons.getSingletons(getActivity()).getDvrWatchedPositionManager();
+ TvApplication.getSingletons(getActivity()).getDvrWatchedPositionManager();
setDetailsOverviewRow(
DetailsContent.createFromRecordedProgram(getContext(), mRecordedProgram));
}
@@ -139,7 +139,7 @@ public class RecordedProgramDetailsFragment extends DvrDetailsFragment
mRecordedProgram.getId()));
} else if (action.getId() == ACTION_DELETE_RECORDING) {
DvrManager dvrManager =
- TvSingletons.getSingletons(getActivity()).getDvrManager();
+ TvApplication.getSingletons(getActivity()).getDvrManager();
dvrManager.removeRecordedProgram(mRecordedProgram);
getActivity().finish();
}
diff --git a/src/com/android/tv/dvr/ui/browse/RecordedProgramPresenter.java b/src/com/android/tv/dvr/ui/browse/RecordedProgramPresenter.java
index e2db3ac4..aad1cc6a 100644
--- a/src/com/android/tv/dvr/ui/browse/RecordedProgramPresenter.java
+++ b/src/com/android/tv/dvr/ui/browse/RecordedProgramPresenter.java
@@ -19,7 +19,7 @@ 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.TvSingletons;
+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;
@@ -95,7 +95,7 @@ public class RecordedProgramPresenter extends DvrItemPresenter<RecordedProgram>
mTodayString = mContext.getString(R.string.dvr_date_today);
mYesterdayString = mContext.getString(R.string.dvr_date_yesterday);
mDvrWatchedPositionManager =
- TvSingletons.getSingletons(mContext).getDvrWatchedPositionManager();
+ TvApplication.getSingletons(mContext).getDvrWatchedPositionManager();
mProgressBarColor =
mContext.getResources().getColor(R.color.play_controls_progress_bar_watched);
mShowEpisodeTitle = showEpisodeTitle;
diff --git a/src/com/android/tv/dvr/ui/browse/RecordingCardView.java b/src/com/android/tv/dvr/ui/browse/RecordingCardView.java
index 0a204c14..edee5d3a 100644
--- a/src/com/android/tv/dvr/ui/browse/RecordingCardView.java
+++ b/src/com/android/tv/dvr/ui/browse/RecordingCardView.java
@@ -37,8 +37,8 @@ import com.android.tv.ui.ViewUtils;
import com.android.tv.util.ImageLoader;
/**
- * A CardView for displaying info about a {@link com.android.tv.dvr.data.ScheduledRecording}
- * or {@link RecordedProgram} or {@link com.android.tv.dvr.data.SeriesRecording}.
+ * A CardView for displaying info about a {@link com.android.tv.dvr.data.ScheduledRecording} or
+ * {@link RecordedProgram} or {@link com.android.tv.dvr.data.SeriesRecording}.
*/
public class RecordingCardView extends BaseCardView {
// This value should be the same with
diff --git a/src/com/android/tv/dvr/ui/browse/RecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/RecordingDetailsFragment.java
index e4d95630..c8f1c785 100644
--- a/src/com/android/tv/dvr/ui/browse/RecordingDetailsFragment.java
+++ b/src/com/android/tv/dvr/ui/browse/RecordingDetailsFragment.java
@@ -18,7 +18,7 @@ package com.android.tv.dvr.ui.browse;
import android.os.Bundle;
import android.support.v17.leanback.app.DetailsFragment;
-import com.android.tv.TvSingletons;
+import com.android.tv.TvApplication;
import com.android.tv.dvr.data.ScheduledRecording;
/** {@link DetailsFragment} for recordings in DVR. */
@@ -35,7 +35,7 @@ abstract class RecordingDetailsFragment extends DvrDetailsFragment {
protected boolean onLoadRecordingDetails(Bundle args) {
long scheduledRecordingId = args.getLong(DvrDetailsActivity.RECORDING_ID);
mRecording =
- TvSingletons.getSingletons(getContext())
+ TvApplication.getSingletons(getContext())
.getDvrDataManager()
.getScheduledRecording(scheduledRecordingId);
return mRecording != null;
diff --git a/src/com/android/tv/dvr/ui/browse/ScheduledRecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/ScheduledRecordingDetailsFragment.java
index 0765117d..b3e6ebb3 100644
--- a/src/com/android/tv/dvr/ui/browse/ScheduledRecordingDetailsFragment.java
+++ b/src/com/android/tv/dvr/ui/browse/ScheduledRecordingDetailsFragment.java
@@ -22,7 +22,7 @@ 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.TvSingletons;
+import com.android.tv.TvApplication;
import com.android.tv.dvr.DvrManager;
import com.android.tv.dvr.ui.DvrUiHelper;
@@ -37,7 +37,7 @@ public class ScheduledRecordingDetailsFragment extends RecordingDetailsFragment
@Override
public void onCreate(Bundle savedInstance) {
- mDvrManager = TvSingletons.getSingletons(getContext()).getDvrManager();
+ mDvrManager = TvApplication.getSingletons(getContext()).getDvrManager();
mHideViewSchedule = getArguments().getBoolean(DvrDetailsActivity.HIDE_VIEW_SCHEDULE);
super.onCreate(savedInstance);
}
diff --git a/src/com/android/tv/dvr/ui/browse/ScheduledRecordingPresenter.java b/src/com/android/tv/dvr/ui/browse/ScheduledRecordingPresenter.java
index f1ed52c8..fa948447 100644
--- a/src/com/android/tv/dvr/ui/browse/ScheduledRecordingPresenter.java
+++ b/src/com/android/tv/dvr/ui/browse/ScheduledRecordingPresenter.java
@@ -19,7 +19,7 @@ package com.android.tv.dvr.ui.browse;
import android.content.Context;
import android.os.Handler;
import com.android.tv.R;
-import com.android.tv.TvSingletons;
+import com.android.tv.TvApplication;
import com.android.tv.dvr.DvrManager;
import com.android.tv.dvr.data.ScheduledRecording;
import com.android.tv.util.Utils;
@@ -100,7 +100,7 @@ class ScheduledRecordingPresenter extends DvrItemPresenter<ScheduledRecording> {
public ScheduledRecordingPresenter(Context context) {
super(context);
- mDvrManager = TvSingletons.getSingletons(mContext).getDvrManager();
+ mDvrManager = TvApplication.getSingletons(mContext).getDvrManager();
mProgressBarColor =
mContext.getResources()
.getColor(R.color.play_controls_recording_icon_color_on_focus);
diff --git a/src/com/android/tv/dvr/ui/browse/SeriesRecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/SeriesRecordingDetailsFragment.java
index 2cd191a7..48bc9cbd 100644
--- a/src/com/android/tv/dvr/ui/browse/SeriesRecordingDetailsFragment.java
+++ b/src/com/android/tv/dvr/ui/browse/SeriesRecordingDetailsFragment.java
@@ -33,7 +33,7 @@ import android.support.v17.leanback.widget.PresenterSelector;
import android.support.v17.leanback.widget.SparseArrayObjectAdapter;
import android.text.TextUtils;
import com.android.tv.R;
-import com.android.tv.TvSingletons;
+import com.android.tv.TvApplication;
import com.android.tv.data.BaseProgram;
import com.android.tv.dvr.DvrDataManager;
import com.android.tv.dvr.DvrWatchedPositionManager;
@@ -73,7 +73,7 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment
@Override
public void onCreate(Bundle savedInstanceState) {
- mDvrDataManager = TvSingletons.getSingletons(getActivity()).getDvrDataManager();
+ mDvrDataManager = TvApplication.getSingletons(getActivity()).getDvrDataManager();
mWatchLabel = getString(R.string.dvr_detail_watch);
mResumeLabel = getString(R.string.dvr_detail_series_resume);
mWatchDrawable = getResources().getDrawable(R.drawable.lb_ic_play, null);
@@ -84,7 +84,7 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment
@Override
protected void onCreateInternal() {
mDvrWatchedPositionManager =
- TvSingletons.getSingletons(getActivity()).getDvrWatchedPositionManager();
+ TvApplication.getSingletons(getActivity()).getDvrWatchedPositionManager();
setDetailsOverviewRow(DetailsContent.createFromSeriesRecording(getContext(), mSeries));
setupRecordedProgramsRow();
mDvrDataManager.addSeriesRecordingListener(this);
@@ -137,7 +137,7 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment
protected boolean onLoadRecordingDetails(Bundle args) {
long recordId = args.getLong(DvrDetailsActivity.RECORDING_ID);
mSeries =
- TvSingletons.getSingletons(getActivity())
+ TvApplication.getSingletons(getActivity())
.getDvrDataManager()
.getSeriesRecording(recordId);
if (mSeries == null) {
diff --git a/src/com/android/tv/dvr/ui/browse/SeriesRecordingPresenter.java b/src/com/android/tv/dvr/ui/browse/SeriesRecordingPresenter.java
index 14f9dceb..02ce24ef 100644
--- a/src/com/android/tv/dvr/ui/browse/SeriesRecordingPresenter.java
+++ b/src/com/android/tv/dvr/ui/browse/SeriesRecordingPresenter.java
@@ -19,8 +19,9 @@ package com.android.tv.dvr.ui.browse;
import android.content.Context;
import android.media.tv.TvInputManager;
import android.text.TextUtils;
+import com.android.tv.ApplicationSingletons;
import com.android.tv.R;
-import com.android.tv.TvSingletons;
+import com.android.tv.TvApplication;
import com.android.tv.dvr.DvrDataManager;
import com.android.tv.dvr.DvrDataManager.RecordedProgramListener;
import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener;
@@ -185,7 +186,7 @@ class SeriesRecordingPresenter extends DvrItemPresenter<SeriesRecording> {
public SeriesRecordingPresenter(Context context) {
super(context);
- TvSingletons singletons = TvSingletons.getSingletons(context);
+ ApplicationSingletons singletons = TvApplication.getSingletons(context);
mDvrDataManager = singletons.getDvrDataManager();
mDvrManager = singletons.getDvrManager();
mWatchedPositionManager = singletons.getDvrWatchedPositionManager();
diff --git a/src/com/android/tv/dvr/ui/list/BaseDvrSchedulesFragment.java b/src/com/android/tv/dvr/ui/list/BaseDvrSchedulesFragment.java
index 84298bdf..42c7086a 100644
--- a/src/com/android/tv/dvr/ui/list/BaseDvrSchedulesFragment.java
+++ b/src/com/android/tv/dvr/ui/list/BaseDvrSchedulesFragment.java
@@ -23,8 +23,9 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
+import com.android.tv.ApplicationSingletons;
import com.android.tv.R;
-import com.android.tv.TvSingletons;
+import com.android.tv.TvApplication;
import com.android.tv.dvr.DvrDataManager;
import com.android.tv.dvr.DvrScheduleManager;
import com.android.tv.dvr.data.ScheduledRecording;
@@ -49,7 +50,7 @@ public abstract class BaseDvrSchedulesFragment extends DetailsFragment
mRowsAdapter = onCreateRowsAdapter(presenterSelector);
setAdapter(mRowsAdapter);
mRowsAdapter.start();
- TvSingletons singletons = TvSingletons.getSingletons(getContext());
+ ApplicationSingletons singletons = TvApplication.getSingletons(getContext());
singletons.getDvrDataManager().addScheduledRecordingListener(this);
singletons.getDvrScheduleManager().addOnConflictStateChangeListener(this);
mEmptyInfoScreenView = (TextView) getActivity().findViewById(R.id.empty_info_screen);
@@ -95,7 +96,7 @@ public abstract class BaseDvrSchedulesFragment extends DetailsFragment
@Override
public void onDestroy() {
- TvSingletons singletons = TvSingletons.getSingletons(getContext());
+ ApplicationSingletons singletons = TvApplication.getSingletons(getContext());
singletons.getDvrScheduleManager().removeOnConflictStateChangeListener(this);
singletons.getDvrDataManager().removeScheduledRecordingListener(this);
mRowsAdapter.stop();
diff --git a/src/com/android/tv/dvr/ui/list/DvrSchedulesActivity.java b/src/com/android/tv/dvr/ui/list/DvrSchedulesActivity.java
index 82b85630..11df780c 100644
--- a/src/com/android/tv/dvr/ui/list/DvrSchedulesActivity.java
+++ b/src/com/android/tv/dvr/ui/list/DvrSchedulesActivity.java
@@ -21,7 +21,7 @@ import android.app.ProgressDialog;
import android.os.Bundle;
import android.support.annotation.IntDef;
import com.android.tv.R;
-import com.android.tv.Starter;
+import com.android.tv.TvApplication;
import com.android.tv.data.Program;
import com.android.tv.dvr.data.SeriesRecording;
import com.android.tv.dvr.provider.EpisodicProgramLoadTask;
@@ -53,7 +53,7 @@ public class DvrSchedulesActivity extends Activity {
@Override
public void onCreate(final Bundle savedInstanceState) {
- Starter.start(this);
+ TvApplication.setCurrentRunningProcess(this, true);
// Pass null to prevent automatically re-creating fragments
super.onCreate(null);
setContentView(R.layout.activity_dvr_schedules);
diff --git a/src/com/android/tv/dvr/ui/list/DvrSeriesSchedulesFragment.java b/src/com/android/tv/dvr/ui/list/DvrSeriesSchedulesFragment.java
index d376e358..6ec2e152 100644
--- a/src/com/android/tv/dvr/ui/list/DvrSeriesSchedulesFragment.java
+++ b/src/com/android/tv/dvr/ui/list/DvrSeriesSchedulesFragment.java
@@ -30,8 +30,9 @@ import android.transition.Fade;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import com.android.tv.ApplicationSingletons;
import com.android.tv.R;
-import com.android.tv.TvSingletons;
+import com.android.tv.TvApplication;
import com.android.tv.data.ChannelDataManager;
import com.android.tv.data.Program;
import com.android.tv.dvr.DvrDataManager;
@@ -140,7 +141,7 @@ public class DvrSeriesSchedulesFragment extends BaseDvrSchedulesFragment {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- TvSingletons singletons = TvSingletons.getSingletons(getContext());
+ ApplicationSingletons singletons = TvApplication.getSingletons(getContext());
mChannelDataManager = singletons.getChannelDataManager();
mChannelDataManager.addListener(mChannelListener);
mDvrDataManager = singletons.getDvrDataManager();
diff --git a/src/com/android/tv/dvr/ui/list/ScheduleRowAdapter.java b/src/com/android/tv/dvr/ui/list/ScheduleRowAdapter.java
index 1215c19a..8dd6c322 100644
--- a/src/com/android/tv/dvr/ui/list/ScheduleRowAdapter.java
+++ b/src/com/android/tv/dvr/ui/list/ScheduleRowAdapter.java
@@ -26,7 +26,7 @@ import android.text.format.DateUtils;
import android.util.ArraySet;
import android.util.Log;
import com.android.tv.R;
-import com.android.tv.TvSingletons;
+import com.android.tv.TvApplication;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.dvr.DvrManager;
import com.android.tv.dvr.data.ScheduledRecording;
@@ -79,11 +79,11 @@ class ScheduleRowAdapter extends ArrayObjectAdapter {
public void start() {
clear();
List<ScheduledRecording> recordingList =
- TvSingletons.getSingletons(mContext)
+ TvApplication.getSingletons(mContext)
.getDvrDataManager()
.getNonStartedScheduledRecordings();
recordingList.addAll(
- TvSingletons.getSingletons(mContext).getDvrDataManager().getStartedRecordings());
+ TvApplication.getSingletons(mContext).getDvrDataManager().getStartedRecordings());
Collections.sort(
recordingList, ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR);
long deadLine = Utils.getLastMillisecondOfDay(System.currentTimeMillis());
@@ -136,7 +136,7 @@ class ScheduleRowAdapter extends ArrayObjectAdapter {
/** Stops schedules row adapter. */
public void stop() {
mHandler.removeCallbacksAndMessages(null);
- DvrManager dvrManager = TvSingletons.getSingletons(getContext()).getDvrManager();
+ DvrManager dvrManager = TvApplication.getSingletons(getContext()).getDvrManager();
for (int i = 0; i < size(); i++) {
if (get(i) instanceof ScheduleRow) {
ScheduleRow row = (ScheduleRow) get(i);
diff --git a/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java b/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java
index 5cab607a..67096e3b 100644
--- a/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java
+++ b/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java
@@ -38,7 +38,7 @@ import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;
import com.android.tv.R;
-import com.android.tv.TvSingletons;
+import com.android.tv.TvApplication;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.data.Channel;
import com.android.tv.dialog.HalfSizedDialogFragment;
@@ -344,8 +344,8 @@ class ScheduleRowPresenter extends RowPresenter {
setHeaderPresenter(null);
setSelectEffectEnabled(false);
mContext = context;
- mDvrManager = TvSingletons.getSingletons(context).getDvrManager();
- mDvrScheduleManager = TvSingletons.getSingletons(context).getDvrScheduleManager();
+ mDvrManager = TvApplication.getSingletons(context).getDvrManager();
+ mDvrScheduleManager = TvApplication.getSingletons(context).getDvrScheduleManager();
mTunerConflictWillNotBeRecordedInfo =
mContext.getString(R.string.dvr_schedules_tuner_conflict_will_not_be_recorded_info);
mTunerConflictWillBePartiallyRecordedInfo =
@@ -426,7 +426,7 @@ class ScheduleRowPresenter extends RowPresenter {
switch (actions.length) {
case 2:
viewHolder.mSecondActionView.setImageResource(getImageForAction(actions[1]));
- // fall through
+ // pass through
case 1:
viewHolder.mFirstActionView.setImageResource(getImageForAction(actions[0]));
break;
@@ -486,7 +486,7 @@ class ScheduleRowPresenter extends RowPresenter {
private String getChannelNameText(ScheduleRow row) {
Channel channel =
- TvSingletons.getSingletons(mContext)
+ TvApplication.getSingletons(mContext)
.getChannelDataManager()
.getChannel(row.getChannelId());
return channel == null
diff --git a/src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java b/src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java
index eb01aba2..03cc0a79 100644
--- a/src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java
+++ b/src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java
@@ -28,7 +28,7 @@ import android.view.ViewGroup;
import android.view.animation.DecelerateInterpolator;
import android.widget.TextView;
import com.android.tv.R;
-import com.android.tv.TvSingletons;
+import com.android.tv.TvApplication;
import com.android.tv.dvr.data.SeriesRecording;
import com.android.tv.dvr.ui.DvrUiHelper;
import com.android.tv.dvr.ui.list.SchedulesHeaderRow.SeriesRecordingHeaderRow;
@@ -158,11 +158,11 @@ abstract class SchedulesHeaderRowPresenter extends RowPresenter {
SeriesRecording seriesRecording =
SeriesRecording.buildFrom(header.getSeriesRecording())
.setPriority(
- TvSingletons.getSingletons(getContext())
+ TvApplication.getSingletons(getContext())
.getDvrScheduleManager()
.suggestNewSeriesPriority())
.build();
- TvSingletons.getSingletons(getContext())
+ TvApplication.getSingletons(getContext())
.getDvrManager()
.updateSeriesRecording(seriesRecording);
DvrUiHelper.startSeriesSettingsActivity(
diff --git a/src/com/android/tv/dvr/ui/list/SeriesScheduleRowAdapter.java b/src/com/android/tv/dvr/ui/list/SeriesScheduleRowAdapter.java
index 9a9c94ea..692c0f99 100644
--- a/src/com/android/tv/dvr/ui/list/SeriesScheduleRowAdapter.java
+++ b/src/com/android/tv/dvr/ui/list/SeriesScheduleRowAdapter.java
@@ -23,8 +23,9 @@ import android.os.Build;
import android.support.v17.leanback.widget.ClassPresenterSelector;
import android.util.ArrayMap;
import android.util.Log;
+import com.android.tv.ApplicationSingletons;
import com.android.tv.R;
-import com.android.tv.TvSingletons;
+import com.android.tv.TvApplication;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.data.Program;
import com.android.tv.dvr.DvrDataManager;
@@ -64,7 +65,7 @@ class SeriesScheduleRowAdapter extends ScheduleRowAdapter {
} else {
mInputId = null;
}
- TvSingletons singletons = TvSingletons.getSingletons(context);
+ ApplicationSingletons singletons = TvApplication.getSingletons(context);
mDvrManager = singletons.getDvrManager();
mDataManager = singletons.getDvrDataManager();
setHasStableIds(true);
diff --git a/src/com/android/tv/dvr/ui/playback/DvrPlaybackActivity.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackActivity.java
index b8b19adc..29f2734d 100644
--- a/src/com/android/tv/dvr/ui/playback/DvrPlaybackActivity.java
+++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackActivity.java
@@ -24,7 +24,7 @@ import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import com.android.tv.R;
-import com.android.tv.Starter;
+import com.android.tv.TvApplication;
import com.android.tv.dialog.PinDialogFragment.OnPinCheckedListener;
import com.android.tv.dvr.data.RecordedProgram;
import com.android.tv.util.Utils;
@@ -39,7 +39,7 @@ public class DvrPlaybackActivity extends Activity implements OnPinCheckedListene
@Override
public void onCreate(Bundle savedInstanceState) {
- Starter.start(this);
+ TvApplication.setCurrentRunningProcess(this, true);
if (DEBUG) Log.d(TAG, "onCreate");
super.onCreate(savedInstanceState);
setIntent(createProgramIntent(getIntent()));
diff --git a/src/com/android/tv/dvr/ui/playback/DvrPlaybackMediaSessionHelper.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackMediaSessionHelper.java
index dd17b22d..3ff90aa4 100644
--- a/src/com/android/tv/dvr/ui/playback/DvrPlaybackMediaSessionHelper.java
+++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackMediaSessionHelper.java
@@ -29,7 +29,7 @@ import android.os.AsyncTask;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import com.android.tv.R;
-import com.android.tv.TvSingletons;
+import com.android.tv.TvApplication;
import com.android.tv.data.Channel;
import com.android.tv.data.ChannelDataManager;
import com.android.tv.dvr.DvrWatchedPositionManager;
@@ -61,8 +61,8 @@ class DvrPlaybackMediaSessionHelper {
mActivity = activity;
mDvrPlayer = dvrPlayer;
mDvrWatchedPositionManager =
- TvSingletons.getSingletons(activity).getDvrWatchedPositionManager();
- mChannelDataManager = TvSingletons.getSingletons(activity).getChannelDataManager();
+ TvApplication.getSingletons(activity).getDvrWatchedPositionManager();
+ mChannelDataManager = TvApplication.getSingletons(activity).getChannelDataManager();
mDvrPlayer.setCallback(
new DvrPlayer.DvrPlayerCallback() {
@Override
diff --git a/src/com/android/tv/dvr/ui/playback/DvrPlaybackOverlayFragment.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackOverlayFragment.java
index d3374cfa..c5fccda2 100644
--- a/src/com/android/tv/dvr/ui/playback/DvrPlaybackOverlayFragment.java
+++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackOverlayFragment.java
@@ -43,7 +43,7 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import com.android.tv.R;
-import com.android.tv.TvSingletons;
+import com.android.tv.TvApplication;
import com.android.tv.data.BaseProgram;
import com.android.tv.dialog.PinDialogFragment;
import com.android.tv.dvr.DvrDataManager;
@@ -116,9 +116,9 @@ public class DvrPlaybackOverlayFragment extends PlaybackFragment {
.getResources()
.getDimensionPixelOffset(
R.dimen.dvr_playback_overlay_padding_top_no_secondary_row);
- mDvrDataManager = TvSingletons.getSingletons(getActivity()).getDvrDataManager();
+ mDvrDataManager = TvApplication.getSingletons(getActivity()).getDvrDataManager();
mContentRatingsManager =
- TvSingletons.getSingletons(getContext())
+ TvApplication.getSingletons(getContext())
.getTvInputManagerHelper()
.getContentRatingsManager();
if (!mDvrDataManager.isRecordedProgramLoadFinished()) {
diff --git a/src/com/android/tv/experiments/ExperimentFlag.java b/src/com/android/tv/experiments/ExperimentFlag.java
new file mode 100644
index 00000000..2963482e
--- /dev/null
+++ b/src/com/android/tv/experiments/ExperimentFlag.java
@@ -0,0 +1,62 @@
+/*
+ * 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.experiments;
+
+import android.support.annotation.VisibleForTesting;
+
+/** Experiments return values based on user, device and other criteria. */
+public final class ExperimentFlag<T> {
+
+ private static boolean sAllowOverrides = false;
+
+ @VisibleForTesting
+ public static void initForTest() {
+ sAllowOverrides = true;
+ }
+
+ /** Returns a boolean experiment */
+ public static ExperimentFlag<Boolean> createFlag(boolean defaultValue) {
+ return new ExperimentFlag<>(defaultValue);
+ }
+
+ private final T mDefaultValue;
+
+ private T mOverrideValue = null;
+ private boolean mOverridden = false;
+
+ private ExperimentFlag(T defaultValue) {
+ mDefaultValue = defaultValue;
+ }
+
+ /** Returns value for this experiment */
+ public T get() {
+ 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
new file mode 100644
index 00000000..0bff384e
--- /dev/null
+++ b/src/com/android/tv/experiments/Experiments.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.experiments;
+
+import static com.android.tv.experiments.ExperimentFlag.createFlag;
+
+import com.android.tv.common.BuildConfig;
+
+/**
+ * Set of experiments visible in AOSP.
+ *
+ * <p>This file is maintained by hand.
+ */
+public final class Experiments {
+ public static final ExperimentFlag<Boolean> CLOUD_EPG = createFlag(true);
+
+ public static final ExperimentFlag<Boolean> ENABLE_UNRATED_CONTENT_SETTINGS = createFlag(false);
+
+ /**
+ * Allow developer features such as the dev menu and other aids.
+ *
+ * <p>These features are available to select users(aka fishfooders) on production builds.
+ */
+ public static final ExperimentFlag<Boolean> ENABLE_DEVELOPER_FEATURES =
+ createFlag(BuildConfig.ENG);
+
+ private Experiments() {}
+}
diff --git a/src/com/android/tv/guide/ProgramGuide.java b/src/com/android/tv/guide/ProgramGuide.java
index 33ab9ad7..20c3ccdf 100644
--- a/src/com/android/tv/guide/ProgramGuide.java
+++ b/src/com/android/tv/guide/ProgramGuide.java
@@ -44,12 +44,11 @@ import android.view.ViewGroup.LayoutParams;
import android.view.ViewTreeObserver;
import android.view.accessibility.AccessibilityManager;
import com.android.tv.ChannelTuner;
+import com.android.tv.Features;
import com.android.tv.MainActivity;
import com.android.tv.R;
-import com.android.tv.TvFeatures;
import com.android.tv.analytics.Tracker;
import com.android.tv.common.WeakHandler;
-import com.android.tv.common.util.DurationTimer;
import com.android.tv.data.ChannelDataManager;
import com.android.tv.data.GenreItems;
import com.android.tv.data.ProgramDataManager;
@@ -57,6 +56,7 @@ import com.android.tv.dvr.DvrDataManager;
import com.android.tv.dvr.DvrScheduleManager;
import com.android.tv.ui.HardwareLayerAnimatorListenerAdapter;
import com.android.tv.ui.ViewUtils;
+import com.android.tv.util.DurationTimer;
import com.android.tv.util.TvInputManagerHelper;
import com.android.tv.util.Utils;
import java.util.ArrayList;
@@ -165,7 +165,6 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener {
}
};
- @SuppressWarnings("RestrictTo")
public ProgramGuide(
MainActivity activity,
ChannelTuner channelTuner,
@@ -237,7 +236,7 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener {
mSidePanelGridView.setWindowAlignmentOffsetPercent(
VerticalGridView.WINDOW_ALIGN_OFFSET_PERCENT_DISABLED);
- if (TvFeatures.EPG_SEARCH.isEnabled(mActivity)) {
+ if (Features.EPG_SEARCH.isEnabled(mActivity)) {
mSearchOrb =
(SearchOrbView)
mContainer.findViewById(R.id.program_guide_side_panel_search_orb);
diff --git a/src/com/android/tv/guide/ProgramItemView.java b/src/com/android/tv/guide/ProgramItemView.java
index 9daa9f2f..c8c7ff2d 100644
--- a/src/com/android/tv/guide/ProgramItemView.java
+++ b/src/com/android/tv/guide/ProgramItemView.java
@@ -35,9 +35,10 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.Toast;
+import com.android.tv.ApplicationSingletons;
import com.android.tv.MainActivity;
import com.android.tv.R;
-import com.android.tv.TvSingletons;
+import com.android.tv.TvApplication;
import com.android.tv.analytics.Tracker;
import com.android.tv.common.feature.CommonFeatures;
import com.android.tv.data.Channel;
@@ -90,7 +91,8 @@ public class ProgramItemView extends TextView {
// do nothing
return;
}
- TvSingletons singletons = TvSingletons.getSingletons(view.getContext());
+ ApplicationSingletons singletons =
+ TvApplication.getSingletons(view.getContext());
Tracker tracker = singletons.getTracker();
tracker.sendEpgItemClicked();
final MainActivity tvActivity = (MainActivity) view.getContext();
@@ -206,7 +208,7 @@ public class ProgramItemView extends TextView {
super(context, attrs, defStyle);
setOnClickListener(ON_CLICKED);
setOnFocusChangeListener(ON_FOCUS_CHANGED);
- mDvrManager = TvSingletons.getSingletons(getContext()).getDvrManager();
+ mDvrManager = TvApplication.getSingletons(getContext()).getDvrManager();
}
private void initIfNeeded() {
diff --git a/src/com/android/tv/guide/ProgramTableAdapter.java b/src/com/android/tv/guide/ProgramTableAdapter.java
index c77673a2..8dd14ca3 100644
--- a/src/com/android/tv/guide/ProgramTableAdapter.java
+++ b/src/com/android/tv/guide/ProgramTableAdapter.java
@@ -16,6 +16,8 @@
package com.android.tv.guide;
+import static com.android.tv.util.ImageLoader.ImageLoaderCallback;
+
import android.animation.Animator;
import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
@@ -40,7 +42,6 @@ import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
-import android.view.ViewParent;
import android.view.ViewTreeObserver;
import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener;
@@ -48,9 +49,9 @@ import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.android.tv.R;
-import com.android.tv.TvSingletons;
+import com.android.tv.TvApplication;
+import com.android.tv.common.TvCommonUtils;
import com.android.tv.common.feature.CommonFeatures;
-import com.android.tv.common.util.CommonUtils;
import com.android.tv.data.Channel;
import com.android.tv.data.Program;
import com.android.tv.data.Program.CriticScore;
@@ -58,12 +59,10 @@ import com.android.tv.dvr.DvrDataManager;
import com.android.tv.dvr.DvrManager;
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;
import com.android.tv.util.ImageCache;
import com.android.tv.util.ImageLoader;
-import com.android.tv.util.ImageLoader.ImageLoaderCallback;
import com.android.tv.util.ImageLoader.LoadTvInputLogoTask;
import com.android.tv.util.TvInputManagerHelper;
import com.android.tv.util.Utils;
@@ -114,10 +113,10 @@ class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapter.Progr
mContext = context;
mAccessibilityManager =
(AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
- mTvInputManagerHelper = TvSingletons.getSingletons(context).getTvInputManagerHelper();
+ mTvInputManagerHelper = TvApplication.getSingletons(context).getTvInputManagerHelper();
if (CommonFeatures.DVR.isEnabled(context)) {
- mDvrManager = TvSingletons.getSingletons(context).getDvrManager();
- mDvrDataManager = TvSingletons.getSingletons(context).getDvrDataManager();
+ mDvrManager = TvApplication.getSingletons(context).getDvrManager();
+ mDvrDataManager = TvApplication.getSingletons(context).getDvrDataManager();
} else {
mDvrManager = null;
mDvrDataManager = null;
@@ -315,7 +314,7 @@ class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapter.Progr
new AccessibilityManager.AccessibilityStateChangeListener() {
@Override
public void onAccessibilityStateChanged(boolean enable) {
- enable &= !CommonUtils.isRunningInTest();
+ enable &= !TvCommonUtils.isRunningInTest();
mDetailView.setFocusable(enable);
mChannelHeaderView.setFocusable(enable);
}
@@ -369,7 +368,7 @@ class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapter.Progr
mInputLogoView = (ImageView) mContainer.findViewById(R.id.input_logo);
boolean accessibilityEnabled =
- mAccessibilityManager.isEnabled() && !CommonUtils.isRunningInTest();
+ mAccessibilityManager.isEnabled() && !TvCommonUtils.isRunningInTest();
mDetailView.setFocusable(accessibilityEnabled);
mChannelHeaderView.setFocusable(accessibilityEnabled);
}
@@ -449,7 +448,7 @@ class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapter.Progr
if (newFocus == null) {
return;
} // When the accessibility service is enabled, focus might be put on channel's header
- // or
+ // or
// detail view, besides program items.
if (newFocus == mChannelHeaderView) {
mSelectedEntry = ((ProgramItemView) mProgramRow.getChildAt(0)).getTableEntry();
diff --git a/src/com/android/tv/license/LicenseUtils.java b/src/com/android/tv/license/LicenseUtils.java
index ea774caa..1bae0c6a 100644
--- a/src/com/android/tv/license/LicenseUtils.java
+++ b/src/com/android/tv/license/LicenseUtils.java
@@ -21,7 +21,6 @@ import java.io.IOException;
import java.io.InputStream;
/** Utilities for showing open source licenses. */
-@SuppressWarnings("TryWithResources") // TODO(b/62143348): remove when error prone check fixed
public final class LicenseUtils {
public static final String RATING_SOURCE_FILE = "file:///android_asset/rating_sources.html";
diff --git a/src/com/android/tv/menu/ChannelsRowAdapter.java b/src/com/android/tv/menu/ChannelsRowAdapter.java
index 2d86ec75..73ff6f3b 100644
--- a/src/com/android/tv/menu/ChannelsRowAdapter.java
+++ b/src/com/android/tv/menu/ChannelsRowAdapter.java
@@ -20,13 +20,15 @@ import android.content.Context;
import android.content.Intent;
import android.media.tv.TvInputInfo;
import android.view.View;
+import com.android.tv.ApplicationSingletons;
import com.android.tv.R;
-import com.android.tv.TvSingletons;
+import com.android.tv.TvApplication;
import com.android.tv.analytics.Tracker;
import com.android.tv.common.feature.CommonFeatures;
import com.android.tv.data.Channel;
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 java.util.ArrayDeque;
import java.util.ArrayList;
@@ -100,10 +102,10 @@ public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter<Channels
Context context, Recommender recommender, int minCount, int maxCount) {
super(context);
mContext = context;
- TvSingletons tvSingletons = TvSingletons.getSingletons(context);
- mTracker = tvSingletons.getTracker();
+ ApplicationSingletons appSingletons = TvApplication.getSingletons(context);
+ mTracker = appSingletons.getTracker();
if (CommonFeatures.DVR.isEnabled(context)) {
- mDvrDataManager = tvSingletons.getDvrDataManager();
+ mDvrDataManager = appSingletons.getDvrDataManager();
} else {
mDvrDataManager = null;
}
@@ -229,14 +231,14 @@ public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter<Channels
}
private boolean needToShowSetupItem() {
- TvSingletons singletons = TvSingletons.getSingletons(mContext);
- TvInputManagerHelper inputManager = singletons.getTvInputManagerHelper();
- return singletons.getSetupUtils().hasNewInput(inputManager);
+ TvInputManagerHelper inputManager =
+ TvApplication.getSingletons(mContext).getTvInputManagerHelper();
+ return SetupUtils.getInstance(mContext).hasNewInput(inputManager);
}
private boolean needToShowDvrItem() {
TvInputManagerHelper inputManager =
- TvSingletons.getSingletons(mContext).getTvInputManagerHelper();
+ TvApplication.getSingletons(mContext).getTvInputManagerHelper();
if (mDvrDataManager != null) {
for (TvInputInfo info : inputManager.getTvInputInfos(true, true)) {
if (info.canRecord()) {
@@ -249,7 +251,7 @@ public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter<Channels
private boolean needToShowAppLinkItem() {
TvInputManagerHelper inputManager =
- TvSingletons.getSingletons(mContext).getTvInputManagerHelper();
+ TvApplication.getSingletons(mContext).getTvInputManagerHelper();
Channel currentChannel = getMainActivity().getCurrentChannel();
return currentChannel != null
&& currentChannel.getAppLinkType(mContext) != Channel.APP_LINK_TYPE_NONE
diff --git a/src/com/android/tv/menu/CustomizableOptionsRowAdapter.java b/src/com/android/tv/menu/CustomizableOptionsRowAdapter.java
index 7da26916..9ec70d09 100644
--- a/src/com/android/tv/menu/CustomizableOptionsRowAdapter.java
+++ b/src/com/android/tv/menu/CustomizableOptionsRowAdapter.java
@@ -17,7 +17,7 @@
package com.android.tv.menu;
import android.content.Context;
-import com.android.tv.common.customization.CustomAction;
+import com.android.tv.customization.CustomAction;
import java.util.ArrayList;
import java.util.List;
diff --git a/src/com/android/tv/menu/Menu.java b/src/com/android/tv/menu/Menu.java
index 0e081ba8..2b8a1fa6 100644
--- a/src/com/android/tv/menu/Menu.java
+++ b/src/com/android/tv/menu/Menu.java
@@ -30,15 +30,15 @@ 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.TvOptionsManager;
-import com.android.tv.TvSingletons;
import com.android.tv.analytics.Tracker;
+import com.android.tv.common.TvCommonUtils;
import com.android.tv.common.WeakHandler;
-import com.android.tv.common.util.CommonUtils;
-import com.android.tv.common.util.DurationTimer;
import com.android.tv.menu.MenuRowFactory.PartnerRow;
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;
@@ -139,7 +139,7 @@ public class Menu {
OnMenuVisibilityChangeListener onMenuVisibilityChangeListener) {
mContext = context;
mMenuView = menuView;
- mTracker = TvSingletons.getSingletons(context).getTracker();
+ mTracker = TvApplication.getSingletons(context).getTracker();
mMenuUpdater = new MenuUpdater(this, tvView, optionsManager);
Resources res = context.getResources();
mShowDurationMillis = res.getInteger(R.integer.menu_show_duration);
@@ -328,7 +328,7 @@ public class Menu {
@VisibleForTesting
void disableAnimationForTest() {
- if (!CommonUtils.isRunningInTest()) {
+ if (!TvCommonUtils.isRunningInTest()) {
throw new RuntimeException("Animation may only be enabled/disabled during tests.");
}
mAnimationDisabledForTest = true;
diff --git a/src/com/android/tv/menu/MenuLayoutManager.java b/src/com/android/tv/menu/MenuLayoutManager.java
index a600f704..e652463e 100644
--- a/src/com/android/tv/menu/MenuLayoutManager.java
+++ b/src/com/android/tv/menu/MenuLayoutManager.java
@@ -366,7 +366,7 @@ public class MenuLayoutManager {
return;
}
boolean indexValid = Utils.isIndexValid(mMenuRowViews, position);
- SoftPreconditions.checkArgument(indexValid, TAG, "position %s ", position);
+ SoftPreconditions.checkArgument(indexValid, TAG, "position " + position);
if (!indexValid) {
return;
}
@@ -419,7 +419,7 @@ public class MenuLayoutManager {
return;
}
boolean newIndexValid = Utils.isIndexValid(mMenuRowViews, position);
- SoftPreconditions.checkArgument(newIndexValid, TAG, "position %s", position);
+ SoftPreconditions.checkArgument(newIndexValid, TAG, "position " + position);
if (!newIndexValid) {
return;
}
diff --git a/src/com/android/tv/menu/MenuRowFactory.java b/src/com/android/tv/menu/MenuRowFactory.java
index 048d725d..5424e6f6 100644
--- a/src/com/android/tv/menu/MenuRowFactory.java
+++ b/src/com/android/tv/menu/MenuRowFactory.java
@@ -21,8 +21,8 @@ import android.support.annotation.Nullable;
import android.text.TextUtils;
import com.android.tv.MainActivity;
import com.android.tv.R;
-import com.android.tv.common.customization.CustomAction;
-import com.android.tv.common.customization.CustomizationManager;
+import com.android.tv.customization.CustomAction;
+import com.android.tv.customization.TvCustomizationManager;
import com.android.tv.ui.TunableTvView;
import java.util.List;
@@ -30,14 +30,14 @@ import java.util.List;
public class MenuRowFactory {
private final MainActivity mMainActivity;
private final TunableTvView mTvView;
- private final CustomizationManager mCustomizationManager;
+ private final TvCustomizationManager mTvCustomizationManager;
/** A constructor. */
public MenuRowFactory(MainActivity mainActivity, TunableTvView tvView) {
mMainActivity = mainActivity;
mTvView = tvView;
- mCustomizationManager = new CustomizationManager(mainActivity);
- mCustomizationManager.initialize();
+ mTvCustomizationManager = new TvCustomizationManager(mainActivity);
+ mTvCustomizationManager.initialize();
}
/** Creates an object corresponding to the given {@code key}. */
@@ -50,8 +50,8 @@ public class MenuRowFactory {
return new ChannelsRow(mMainActivity, menu, mMainActivity.getProgramDataManager());
} else if (PartnerRow.class.equals(key)) {
List<CustomAction> customActions =
- mCustomizationManager.getCustomActions(CustomizationManager.ID_PARTNER_ROW);
- String title = mCustomizationManager.getPartnerRowTitle();
+ mTvCustomizationManager.getCustomActions(TvCustomizationManager.ID_PARTNER_ROW);
+ String title = mTvCustomizationManager.getPartnerRowTitle();
if (customActions != null && !TextUtils.isEmpty(title)) {
return new PartnerRow(mMainActivity, menu, title, customActions);
}
@@ -60,7 +60,8 @@ public class MenuRowFactory {
return new TvOptionsRow(
mMainActivity,
menu,
- mCustomizationManager.getCustomActions(CustomizationManager.ID_OPTIONS_ROW));
+ mTvCustomizationManager.getCustomActions(
+ TvCustomizationManager.ID_OPTIONS_ROW));
}
return null;
}
diff --git a/src/com/android/tv/menu/OptionsRowAdapter.java b/src/com/android/tv/menu/OptionsRowAdapter.java
index ceffe861..593bede7 100644
--- a/src/com/android/tv/menu/OptionsRowAdapter.java
+++ b/src/com/android/tv/menu/OptionsRowAdapter.java
@@ -19,7 +19,7 @@ package com.android.tv.menu;
import android.content.Context;
import android.view.View;
import com.android.tv.R;
-import com.android.tv.TvSingletons;
+import com.android.tv.TvApplication;
import com.android.tv.analytics.Tracker;
import java.util.List;
@@ -54,7 +54,7 @@ public abstract class OptionsRowAdapter extends ItemListRowView.ItemListAdapter<
public OptionsRowAdapter(Context context) {
super(context);
- mTracker = TvSingletons.getSingletons(context).getTracker();
+ mTracker = TvApplication.getSingletons(context).getTracker();
}
/** Update action list and its content. */
diff --git a/src/com/android/tv/menu/PartnerOptionsRowAdapter.java b/src/com/android/tv/menu/PartnerOptionsRowAdapter.java
index 9676fe4d..1a962640 100644
--- a/src/com/android/tv/menu/PartnerOptionsRowAdapter.java
+++ b/src/com/android/tv/menu/PartnerOptionsRowAdapter.java
@@ -17,7 +17,7 @@
package com.android.tv.menu;
import android.content.Context;
-import com.android.tv.common.customization.CustomAction;
+import com.android.tv.customization.CustomAction;
import java.util.Collections;
import java.util.List;
diff --git a/src/com/android/tv/menu/PlayControlsRowView.java b/src/com/android/tv/menu/PlayControlsRowView.java
index 35f1d503..d9879e18 100644
--- a/src/com/android/tv/menu/PlayControlsRowView.java
+++ b/src/com/android/tv/menu/PlayControlsRowView.java
@@ -28,7 +28,7 @@ import com.android.tv.MainActivity;
import com.android.tv.R;
import com.android.tv.TimeShiftManager;
import com.android.tv.TimeShiftManager.TimeShiftActionId;
-import com.android.tv.TvSingletons;
+import com.android.tv.TvApplication;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.common.feature.CommonFeatures;
import com.android.tv.data.Channel;
@@ -127,8 +127,8 @@ public class PlayControlsRowView extends MenuRowView {
mCompactButtonMargin =
res.getDimensionPixelSize(R.dimen.play_controls_button_compact_margin);
if (CommonFeatures.DVR.isEnabled(context)) {
- mDvrDataManager = TvSingletons.getSingletons(context).getDvrDataManager();
- mDvrManager = TvSingletons.getSingletons(context).getDvrManager();
+ mDvrDataManager = TvApplication.getSingletons(context).getDvrDataManager();
+ mDvrManager = TvApplication.getSingletons(context).getDvrManager();
} else {
mDvrDataManager = null;
mDvrManager = null;
@@ -275,7 +275,7 @@ public class PlayControlsRowView extends MenuRowView {
private void onRecordButtonClicked() {
boolean isRecording = isCurrentChannelRecording();
Channel currentChannel = mMainActivity.getCurrentChannel();
- TvSingletons.getSingletons(getContext())
+ TvApplication.getSingletons(getContext())
.getTracker()
.sendMenuClicked(
isRecording
@@ -290,7 +290,7 @@ public class PlayControlsRowView extends MenuRowView {
.show();
} else {
Program program =
- TvSingletons.getSingletons(mMainActivity)
+ TvApplication.getSingletons(mMainActivity)
.getProgramDataManager()
.getCurrentProgram(currentChannel.getId());
DvrUiHelper.checkStorageStatusAndShowErrorMessage(
diff --git a/src/com/android/tv/menu/TvOptionsRowAdapter.java b/src/com/android/tv/menu/TvOptionsRowAdapter.java
index 55affb59..d340d309 100644
--- a/src/com/android/tv/menu/TvOptionsRowAdapter.java
+++ b/src/com/android/tv/menu/TvOptionsRowAdapter.java
@@ -19,16 +19,16 @@ package com.android.tv.menu;
import android.content.Context;
import android.media.tv.TvTrackInfo;
import android.support.annotation.VisibleForTesting;
-import com.android.tv.TvFeatures;
+import com.android.tv.Features;
import com.android.tv.TvOptionsManager;
-import com.android.tv.common.customization.CustomAction;
-import com.android.tv.common.util.CommonUtils;
+import com.android.tv.customization.CustomAction;
import com.android.tv.data.DisplayMode;
import com.android.tv.ui.TvViewUiManager;
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.Utils;
import java.util.ArrayList;
import java.util.List;
@@ -45,12 +45,12 @@ public class TvOptionsRowAdapter extends CustomizableOptionsRowAdapter {
List<MenuAction> actionList = new ArrayList<>();
actionList.add(MenuAction.SELECT_CLOSED_CAPTION_ACTION);
actionList.add(MenuAction.SELECT_DISPLAY_MODE_ACTION);
- if (TvFeatures.PICTURE_IN_PICTURE.isEnabled(getMainActivity())) {
+ if (Features.PICTURE_IN_PICTURE.isEnabled(getMainActivity())) {
actionList.add(MenuAction.SYSTEMWIDE_PIP_ACTION);
}
actionList.add(MenuAction.SELECT_AUDIO_LANGUAGE_ACTION);
actionList.add(MenuAction.MORE_CHANNELS_ACTION);
- if (CommonUtils.isDeveloper()) {
+ if (Utils.isDeveloper()) {
actionList.add(MenuAction.DEV_ACTION);
}
actionList.add(MenuAction.SETTINGS_ACTION);
diff --git a/src/com/android/tv/onboarding/NewSourcesFragment.java b/src/com/android/tv/onboarding/NewSourcesFragment.java
index e6b247a0..eaf06990 100644
--- a/src/com/android/tv/onboarding/NewSourcesFragment.java
+++ b/src/com/android/tv/onboarding/NewSourcesFragment.java
@@ -24,14 +24,14 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.android.tv.R;
-import com.android.tv.TvSingletons;
+import com.android.tv.TvApplication;
import com.android.tv.common.ui.setup.SetupActionHelper;
+import com.android.tv.util.SetupUtils;
/** A fragment for new channel source info/setup. */
public class NewSourcesFragment extends Fragment {
/** The action category. */
- public static final String ACTION_CATEOGRY =
- "com.android.tv.onboarding.NewSourcesFragment";
+ public static final String ACTION_CATEOGRY = "com.android.tv.onboarding.NewSourcesFragment";
/** An action to show the setup screen. */
public static final int ACTION_SETUP = 1;
/** An action to close this fragment. */
@@ -52,8 +52,9 @@ public class NewSourcesFragment extends Fragment {
View view = inflater.inflate(R.layout.fragment_new_sources, container, false);
initializeButton(view.findViewById(R.id.setup), ACTION_SETUP);
initializeButton(view.findViewById(R.id.skip), ACTION_SKIP);
- TvSingletons singletons = TvSingletons.getSingletons(getActivity());
- singletons.getSetupUtils().markAllInputsRecognized(singletons.getTvInputManagerHelper());
+ SetupUtils.getInstance(getActivity())
+ .markAllInputsRecognized(
+ TvApplication.getSingletons(getActivity()).getTvInputManagerHelper());
view.requestFocus();
return view;
}
diff --git a/src/com/android/tv/onboarding/OnboardingActivity.java b/src/com/android/tv/onboarding/OnboardingActivity.java
index a1cf9de1..8d429a1d 100644
--- a/src/com/android/tv/onboarding/OnboardingActivity.java
+++ b/src/com/android/tv/onboarding/OnboardingActivity.java
@@ -26,15 +26,16 @@ import android.media.tv.TvInputInfo;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.widget.Toast;
+import com.android.tv.ApplicationSingletons;
import com.android.tv.R;
import com.android.tv.SetupPassthroughActivity;
-import com.android.tv.TvSingletons;
+import com.android.tv.TvApplication;
+import com.android.tv.common.TvCommonUtils;
import com.android.tv.common.ui.setup.SetupActivity;
import com.android.tv.common.ui.setup.SetupMultiPaneFragment;
-import com.android.tv.common.util.CommonUtils;
-import com.android.tv.common.util.PermissionUtils;
import com.android.tv.data.ChannelDataManager;
import com.android.tv.util.OnboardingUtils;
+import com.android.tv.util.PermissionUtils;
import com.android.tv.util.SetupUtils;
import com.android.tv.util.TvInputManagerHelper;
@@ -49,13 +50,12 @@ public class OnboardingActivity extends SetupActivity {
private ChannelDataManager mChannelDataManager;
private TvInputManagerHelper mInputManager;
- private SetupUtils mSetupUtils;
private final ChannelDataManager.Listener mChannelListener =
new ChannelDataManager.Listener() {
@Override
public void onLoadFinished() {
mChannelDataManager.removeListener(this);
- mSetupUtils.markNewChannelsBrowsable();
+ SetupUtils.getInstance(OnboardingActivity.this).markNewChannelsBrowsable();
}
@Override
@@ -81,15 +81,14 @@ public class OnboardingActivity extends SetupActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- TvSingletons singletons = TvSingletons.getSingletons(this);
+ ApplicationSingletons singletons = TvApplication.getSingletons(this);
mInputManager = singletons.getTvInputManagerHelper();
- mSetupUtils = singletons.getSetupUtils();
if (PermissionUtils.hasAccessAllEpg(this) || PermissionUtils.hasReadTvListings(this)) {
mChannelDataManager = singletons.getChannelDataManager();
// Make the channels of the new inputs which have been setup outside Live TV
// browsable.
if (mChannelDataManager.isDbLoadFinished()) {
- mSetupUtils.markNewChannelsBrowsable();
+ SetupUtils.getInstance(this).markNewChannelsBrowsable();
} else {
mChannelDataManager.addListener(mChannelListener);
}
@@ -180,7 +179,7 @@ public class OnboardingActivity extends SetupActivity {
params.getString(
SetupSourcesFragment.ACTION_PARAM_KEY_INPUT_ID);
TvInputInfo input = mInputManager.getTvInputInfo(inputId);
- Intent intent = CommonUtils.createSetupIntent(input);
+ Intent intent = TvCommonUtils.createSetupIntent(input);
if (intent == null) {
Toast.makeText(
this,
@@ -214,7 +213,7 @@ public class OnboardingActivity extends SetupActivity {
case SetupMultiPaneFragment.ACTION_DONE:
{
ChannelDataManager manager =
- TvSingletons.getSingletons(OnboardingActivity.this)
+ TvApplication.getSingletons(OnboardingActivity.this)
.getChannelDataManager();
if (manager.getChannelCount() == 0) {
finish();
diff --git a/src/com/android/tv/onboarding/SetupSourcesFragment.java b/src/com/android/tv/onboarding/SetupSourcesFragment.java
index 3025538b..1ac6b27e 100644
--- a/src/com/android/tv/onboarding/SetupSourcesFragment.java
+++ b/src/com/android/tv/onboarding/SetupSourcesFragment.java
@@ -30,12 +30,14 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
+import com.android.tv.ApplicationSingletons;
import com.android.tv.R;
-import com.android.tv.TvSingletons;
+import com.android.tv.TvApplication;
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;
@@ -46,8 +48,7 @@ import java.util.List;
/** A fragment for channel source info/setup. */
public class SetupSourcesFragment extends SetupMultiPaneFragment {
/** The action category for the actions which is fired from this fragment. */
- public static final String ACTION_CATEGORY =
- "com.android.tv.onboarding.SetupSourcesFragment";
+ public static final String ACTION_CATEGORY = "com.android.tv.onboarding.SetupSourcesFragment";
/** An action to open the merchant collection. */
public static final int ACTION_ONLINE_STORE = 1;
/**
@@ -70,7 +71,7 @@ public class SetupSourcesFragment extends SetupMultiPaneFragment {
public View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = super.onCreateView(inflater, container, savedInstanceState);
- TvSingletons.getSingletons(getActivity()).getTracker().sendScreenView(SETUP_TRACKER_LABEL);
+ TvApplication.getSingletons(getActivity()).getTracker().sendScreenView(SETUP_TRACKER_LABEL);
return view;
}
@@ -189,18 +190,16 @@ public class SetupSourcesFragment extends SetupMultiPaneFragment {
@Override
public void onCreate(Bundle savedInstanceState) {
Context context = getActivity();
- TvSingletons singletons = TvSingletons.getSingletons(context);
- mInputManager = singletons.getTvInputManagerHelper();
- mChannelDataManager = singletons.getChannelDataManager();
- mSetupUtils = singletons.getSetupUtils();
+ ApplicationSingletons app = TvApplication.getSingletons(context);
+ mInputManager = app.getTvInputManagerHelper();
+ mChannelDataManager = app.getChannelDataManager();
+ mSetupUtils = SetupUtils.getInstance(context);
buildInputs();
mInputManager.addCallback(mInputCallback);
mChannelDataManager.addListener(mChannelDataManagerListener);
super.onCreate(savedInstanceState);
mParentFragment = (SetupSourcesFragment) getParentFragment();
- singletons
- .getTunerInputController()
- .executeNetworkTunerDiscoveryAsyncTask(getContext());
+ TunerInputController.executeNetworkTunerDiscoveryAsyncTask(getContext());
}
@Override
diff --git a/src/com/android/tv/onboarding/WelcomeFragment.java b/src/com/android/tv/onboarding/WelcomeFragment.java
index 92b56e8d..d139314e 100644
--- a/src/com/android/tv/onboarding/WelcomeFragment.java
+++ b/src/com/android/tv/onboarding/WelcomeFragment.java
@@ -16,8 +16,6 @@
package com.android.tv.onboarding;
-import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS;
-
import android.animation.Animator;
import android.animation.AnimatorInflater;
import android.animation.AnimatorListenerAdapter;
@@ -26,18 +24,11 @@ import android.content.Context;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v17.leanback.app.OnboardingFragment;
-import android.text.Editable;
-import android.text.TextWatcher;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
-import android.view.View.AccessibilityDelegate;
import android.view.ViewGroup;
-import android.view.accessibility.AccessibilityEvent;
-import android.view.accessibility.AccessibilityNodeInfo;
-import android.widget.Button;
import android.widget.ImageView;
-import android.widget.TextView;
import com.android.tv.R;
import com.android.tv.common.ui.setup.SetupActionHelper;
import com.android.tv.common.ui.setup.animation.SetupAnimationHelper;
@@ -46,7 +37,7 @@ import java.util.List;
/** A fragment for the onboarding welcome screen. */
public class WelcomeFragment extends OnboardingFragment {
- public static final String ACTION_CATEGORY = "com.android.tv.onboarding.WelcomeFragment";
+ public static final String ACTION_CATEGORY = "comgoogle.android.tv.onboarding.WelcomeFragment";
public static final int ACTION_NEXT = 1;
private static final long START_DELAY_CLOUD_MS = 33;
@@ -585,15 +576,8 @@ public class WelcomeFragment extends OnboardingFragment {
private ImageView mTvContentView;
private ImageView mArrowView;
- private TextView mTitleView;
- private Button mStartButton;
- private View mPagingIndicator;
-
private Animator mAnimator;
- private boolean mLogoAnimationFinished;
- private boolean mTitleChanged;
-
public WelcomeFragment() {
setExitTransition(
new SetupAnimationHelper.TransitionBuilder()
@@ -614,91 +598,14 @@ public class WelcomeFragment extends OnboardingFragment {
mPageDescriptions = getResources().getStringArray(R.array.welcome_page_descriptions);
}
}
+
@Nullable
@Override
public View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = super.onCreateView(inflater, container, savedInstanceState);
setLogoResourceId(R.drawable.splash_logo);
- mTitleView = view.findViewById(android.support.v17.leanback.R.id.title);
- mPagingIndicator = view.findViewById(android.support.v17.leanback.R.id.page_indicator);
- mStartButton = view.findViewById(android.support.v17.leanback.R.id.button_start);
-
- mStartButton.setAccessibilityDelegate(new AccessibilityDelegate() {
- @Override
- public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) {
- int type = event.getEventType();
- if (type == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED
- || type == AccessibilityEvent.TYPE_VIEW_FOCUSED) {
- if (!mTitleChanged || mTitleView.isAccessibilityFocused()) {
- // Skip the event before the title is accessibility focused to avoid race
- // conditions
- return;
- }
- }
- super.onInitializeAccessibilityEvent(host, event);
- }
- });
-
- mPagingIndicator.setAccessibilityDelegate(new AccessibilityDelegate() {
- @Override
- public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) {
- int type = event.getEventType();
- if (type == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED
- || type == AccessibilityEvent.TYPE_VIEW_FOCUSED) {
- if (!mTitleChanged || mTitleView.isAccessibilityFocused()) {
- // Skip the event before the title is accessibility focused to avoid race
- // conditions
- return;
- }
- }
- super.onInitializeAccessibilityEvent(host, event);
- }
- });
-
- mTitleView.setAccessibilityDelegate(new AccessibilityDelegate() {
- @Override
- public boolean performAccessibilityAction(View host, int action, Bundle args) {
- if (action == ACTION_CLEAR_ACCESSIBILITY_FOCUS) {
- if (!mTitleChanged || mTitleView.isAccessibilityFocused()) {
- // Skip the event before the title is accessibility focused to avoid race
- // conditions
- return false;
- }
- }
- return super.performAccessibilityAction(host, action, args);
- }
- });
-
- mTitleView.addTextChangedListener(new TextWatcher() {
- @Override
- public void beforeTextChanged(CharSequence s, int start, int count, int after) {
- mTitleChanged = false;
- }
-
- @Override
- public void onTextChanged(CharSequence s, int start, int before, int count) {
- }
-
- @Override
- public void afterTextChanged(Editable s) {
- if (!mTitleView.isAccessibilityFocused()) {
- mTitleView.performAccessibilityAction(
- AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null);
- } else {
- mTitleView.sendAccessibilityEvent(
- AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
- }
- mTitleChanged = true;
- }
- });
- return view;
- }
-
- @Override
- public void onViewCreated(View view, Bundle savedInstanceState) {
- super.onViewCreated(view, savedInstanceState);
- if (savedInstanceState != null && mLogoAnimationFinished) {
+ if (savedInstanceState != null) {
switch (getCurrentPageIndex()) {
case 0:
mTvContentView.setImageResource(
@@ -720,6 +627,7 @@ public class WelcomeFragment extends OnboardingFragment {
break;
}
}
+ return view;
}
@Override
@@ -728,12 +636,6 @@ public class WelcomeFragment extends OnboardingFragment {
}
@Override
- protected void onLogoAnimationFinished() {
- super.onLogoAnimationFinished();
- mLogoAnimationFinished = true;
- }
-
- @Override
protected Animator onCreateEnterAnimation() {
List<Animator> animators = new ArrayList<>();
// Cloud 1
@@ -831,7 +733,6 @@ public class WelcomeFragment extends OnboardingFragment {
if (mAnimator != null) {
mAnimator.cancel();
}
- mTitleChanged = false;
mArrowView.setVisibility(View.GONE);
// TV screen hiding animator.
Animator hideAnimator =
diff --git a/src/com/android/tv/parental/ContentRatingsManager.java b/src/com/android/tv/parental/ContentRatingsManager.java
index 32a1325b..a9c947c6 100644
--- a/src/com/android/tv/parental/ContentRatingsManager.java
+++ b/src/com/android/tv/parental/ContentRatingsManager.java
@@ -19,12 +19,12 @@ package com.android.tv.parental;
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;
-import com.android.tv.util.TvInputManagerHelper;
import java.util.ArrayList;
import java.util.List;
@@ -32,19 +32,19 @@ public class ContentRatingsManager {
private final List<ContentRatingSystem> mContentRatingSystems = new ArrayList<>();
private final Context mContext;
- private final TvInputManagerHelper.TvInputManagerInterface mTvInputManager;
- public ContentRatingsManager(
- Context context, TvInputManagerHelper.TvInputManagerInterface tvInputManager) {
+ public ContentRatingsManager(Context context) {
mContext = context;
- this.mTvInputManager = tvInputManager;
}
public void update() {
mContentRatingSystems.clear();
+
+ TvInputManager manager =
+ (TvInputManager) mContext.getSystemService(Context.TV_INPUT_SERVICE);
ContentRatingsParser parser = new ContentRatingsParser(mContext);
- List<TvContentRatingSystemInfo> infos = mTvInputManager.getTvContentRatingSystemList();
+ List<TvContentRatingSystemInfo> infos = manager.getTvContentRatingSystemList();
for (TvContentRatingSystemInfo info : infos) {
List<ContentRatingSystem> list = parser.parse(info);
if (list != null) {
diff --git a/src/com/android/tv/parental/ContentRatingsParser.java b/src/com/android/tv/parental/ContentRatingsParser.java
index 294e9463..3e645ce9 100644
--- a/src/com/android/tv/parental/ContentRatingsParser.java
+++ b/src/com/android/tv/parental/ContentRatingsParser.java
@@ -33,7 +33,6 @@ import java.util.List;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
-@SuppressWarnings("TryWithResources") // TODO(b/62143348): remove when error prone check fixed
public class ContentRatingsParser {
private static final String TAG = "ContentRatingsParser";
private static final boolean DEBUG = false;
diff --git a/src/com/android/tv/parental/ParentalControlSettings.java b/src/com/android/tv/parental/ParentalControlSettings.java
index db1f0a4d..b9cf45b2 100644
--- a/src/com/android/tv/parental/ParentalControlSettings.java
+++ b/src/com/android/tv/parental/ParentalControlSettings.java
@@ -19,7 +19,7 @@ package com.android.tv.parental;
import android.content.Context;
import android.media.tv.TvContentRating;
import android.media.tv.TvInputManager;
-import com.android.tv.common.experiments.Experiments;
+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;
diff --git a/src/com/android/tv/perf/EventNames.java b/src/com/android/tv/perf/EventNames.java
index 54745f3b..12488ddb 100644
--- a/src/com/android/tv/perf/EventNames.java
+++ b/src/com/android/tv/perf/EventNames.java
@@ -25,7 +25,6 @@ import java.lang.annotation.Retention;
* Constants for performance event names.
*
* <p>Only constants are used to insure no PII is sent.
- *
*/
public final class EventNames {
diff --git a/src/com/android/tv/receiver/AudioCapabilitiesReceiver.java b/src/com/android/tv/receiver/AudioCapabilitiesReceiver.java
index 3fb66245..90e087f0 100644
--- a/src/com/android/tv/receiver/AudioCapabilitiesReceiver.java
+++ b/src/com/android/tv/receiver/AudioCapabilitiesReceiver.java
@@ -24,10 +24,11 @@ import android.media.AudioFormat;
import android.media.AudioManager;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
-import com.android.tv.TvSingletons;
+import com.android.tv.ApplicationSingletons;
+import com.android.tv.TvApplication;
import com.android.tv.analytics.Analytics;
import com.android.tv.analytics.Tracker;
-import com.android.tv.common.util.SharedPreferencesUtils;
+import com.android.tv.common.SharedPreferencesUtils;
/**
* Creates HDMI plug broadcast receiver, and reports AC3 passthrough capabilities to Google
@@ -60,9 +61,9 @@ public final class AudioCapabilitiesReceiver {
public AudioCapabilitiesReceiver(
@NonNull Context context, @Nullable OnAc3PassthroughCapabilityChangeListener listener) {
mContext = context;
- TvSingletons tvSingletons = TvSingletons.getSingletons(context);
- mAnalytics = tvSingletons.getAnalytics();
- mTracker = tvSingletons.getTracker();
+ ApplicationSingletons appSingletons = TvApplication.getSingletons(context);
+ mAnalytics = appSingletons.getAnalytics();
+ mTracker = appSingletons.getTracker();
mListener = listener;
}
diff --git a/src/com/android/tv/receiver/BootCompletedReceiver.java b/src/com/android/tv/receiver/BootCompletedReceiver.java
index d8528bb5..b3ecb8e6 100644
--- a/src/com/android/tv/receiver/BootCompletedReceiver.java
+++ b/src/com/android/tv/receiver/BootCompletedReceiver.java
@@ -23,10 +23,9 @@ import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Build;
import android.util.Log;
-import com.android.tv.Starter;
+import com.android.tv.Features;
import com.android.tv.TvActivity;
-import com.android.tv.TvFeatures;
-import com.android.tv.TvSingletons;
+import com.android.tv.TvApplication;
import com.android.tv.dvr.recorder.DvrRecordingService;
import com.android.tv.dvr.recorder.RecordingScheduler;
import com.android.tv.recommendation.ChannelPreviewUpdater;
@@ -52,12 +51,12 @@ public class BootCompletedReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
- if (!TvSingletons.getSingletons(context).getTvInputManagerHelper().hasTvInputManager()) {
+ 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);
- Starter.start(context);
+ TvApplication.setCurrentRunningProcess(context, true);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
ChannelPreviewUpdater.getInstance(context).updatePreviewDataForChannelsImmediately();
@@ -70,7 +69,7 @@ public class BootCompletedReceiver extends BroadcastReceiver {
// Grant permission to already set up packages after the system has finished booting.
SetupUtils.grantEpgPermissionToSetUpPackages(context);
- if (TvFeatures.UNHIDE.isEnabled(context)) {
+ if (Features.UNHIDE.isEnabled(context)) {
if (OnboardingUtils.isFirstBoot(context)) {
// Enable the application if this is the first "unhide" feature is enabled just in
// case when the app has been disabled before.
@@ -85,7 +84,7 @@ public class BootCompletedReceiver extends BroadcastReceiver {
}
}
- RecordingScheduler scheduler = TvSingletons.getSingletons(context).getRecordingScheduler();
+ 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 0133d8ee..7c4117d4 100644
--- a/src/com/android/tv/receiver/GlobalKeyReceiver.java
+++ b/src/com/android/tv/receiver/GlobalKeyReceiver.java
@@ -23,9 +23,7 @@ import android.os.AsyncTask;
import android.provider.Settings;
import android.util.Log;
import android.view.KeyEvent;
-import com.android.tv.Starter;
import com.android.tv.TvApplication;
-import com.android.tv.TvSingletons;
/** Handles global keys. */
public class GlobalKeyReceiver extends BroadcastReceiver {
@@ -41,11 +39,11 @@ public class GlobalKeyReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
- if (!TvSingletons.getSingletons(context).getTvInputManagerHelper().hasTvInputManager()) {
+ if (!TvApplication.getSingletons(context).getTvInputManagerHelper().hasTvInputManager()) {
Log.wtf(TAG, "Stopping because device does not have a TvInputManager");
return;
}
- Starter.start(context);
+ TvApplication.setCurrentRunningProcess(context, true);
Context appContext = context.getApplicationContext();
if (DEBUG) Log.d(TAG, "onReceive: " + intent);
if (sUserSetupComplete) {
diff --git a/src/com/android/tv/receiver/PackageIntentsReceiver.java b/src/com/android/tv/receiver/PackageIntentsReceiver.java
index 3958f6bf..bd26c7b3 100644
--- a/src/com/android/tv/receiver/PackageIntentsReceiver.java
+++ b/src/com/android/tv/receiver/PackageIntentsReceiver.java
@@ -21,10 +21,7 @@ import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.util.Log;
-import com.android.tv.Starter;
-import com.android.tv.TvFeatures;
-import com.android.tv.TvSingletons;
-
+import com.android.tv.TvApplication;
import com.android.tv.util.Partner;
/** A class for handling the broadcast intents from PackageManager. */
@@ -33,12 +30,12 @@ public class PackageIntentsReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
- if (!TvSingletons.getSingletons(context).getTvInputManagerHelper().hasTvInputManager()) {
+ if (!TvApplication.getSingletons(context).getTvInputManagerHelper().hasTvInputManager()) {
Log.wtf(TAG, "Stopping because device does not have a TvInputManager");
return;
}
- Starter.start(context);
- ((TvSingletons) context.getApplicationContext()).handleInputCountChanged();
+ TvApplication.setCurrentRunningProcess(context, true);
+ ((TvApplication) context.getApplicationContext()).handleInputCountChanged();
Uri uri = intent.getData();
final String packageName = (uri != null ? uri.getSchemeSpecificPart() : null);
diff --git a/src/com/android/tv/recommendation/ChannelPreviewUpdater.java b/src/com/android/tv/recommendation/ChannelPreviewUpdater.java
index 5da802c4..d332e18a 100644
--- a/src/com/android/tv/recommendation/ChannelPreviewUpdater.java
+++ b/src/com/android/tv/recommendation/ChannelPreviewUpdater.java
@@ -28,8 +28,8 @@ import android.support.annotation.RequiresApi;
import android.support.media.tv.TvContractCompat;
import android.text.TextUtils;
import android.util.Log;
-import com.android.tv.Starter;
-import com.android.tv.TvSingletons;
+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;
@@ -46,7 +46,8 @@ import java.util.concurrent.TimeUnit;
@RequiresApi(Build.VERSION_CODES.O)
public class ChannelPreviewUpdater {
private static final String TAG = "ChannelPreviewUpdater";
- private static final boolean DEBUG = false;
+ // 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);
@@ -99,10 +100,10 @@ public class ChannelPreviewUpdater {
mRecommender.registerEvaluator(new RandomEvaluator(), 0.1, 0.1);
mRecommender.registerEvaluator(new FavoriteChannelEvaluator(), 0.5, 0.5);
mRecommender.registerEvaluator(new RoutineWatchEvaluator(), 1.0, 1.0);
- TvSingletons tvSingleton = TvSingletons.getSingletons(context);
- mPreviewDataManager = tvSingleton.getPreviewDataManager();
+ ApplicationSingletons appSingleton = TvApplication.getSingletons(context);
+ mPreviewDataManager = appSingleton.getPreviewDataManager();
mParentalControlSettings =
- tvSingleton.getTvInputManagerHelper().getParentalControlSettings();
+ appSingleton.getTvInputManagerHelper().getParentalControlSettings();
}
/** Starts the routine service for updating the preview programs. */
@@ -293,7 +294,7 @@ public class ChannelPreviewUpdater {
@Override
public void onCreate() {
- Starter.start(this);
+ TvApplication.setCurrentRunningProcess(this, true);
if (DEBUG) Log.d(TAG, "ChannelPreviewUpdateService.onCreate");
mChannelPreviewUpdater = ChannelPreviewUpdater.getInstance(this);
}
diff --git a/src/com/android/tv/recommendation/ChannelRecord.java b/src/com/android/tv/recommendation/ChannelRecord.java
index 34679452..812b9d3f 100644
--- a/src/com/android/tv/recommendation/ChannelRecord.java
+++ b/src/com/android/tv/recommendation/ChannelRecord.java
@@ -17,9 +17,8 @@
package com.android.tv.recommendation;
import android.content.Context;
-import android.support.annotation.GuardedBy;
import android.support.annotation.VisibleForTesting;
-import com.android.tv.TvSingletons;
+import com.android.tv.TvApplication;
import com.android.tv.data.Channel;
import com.android.tv.data.Program;
import com.android.tv.data.ProgramDataManager;
@@ -30,10 +29,7 @@ public class ChannelRecord {
// TODO: decide the value for max history size.
@VisibleForTesting static final int MAX_HISTORY_SIZE = 100;
private final Context mContext;
-
- @GuardedBy("this")
private final Deque<WatchedProgram> mWatchHistory;
-
private Program mCurrentProgram;
private Channel mChannel;
private long mTotalWatchDurationMs;
@@ -63,7 +59,7 @@ public class ChannelRecord {
mInputRemoved = removed;
}
- public synchronized long getLastWatchEndTimeMs() {
+ public long getLastWatchEndTimeMs() {
WatchedProgram p = mWatchHistory.peekLast();
return (p == null) ? 0 : p.getWatchEndTimeMs();
}
@@ -72,7 +68,7 @@ public class ChannelRecord {
long time = System.currentTimeMillis();
if (mCurrentProgram == null || mCurrentProgram.getEndTimeUtcMillis() < time) {
ProgramDataManager manager =
- TvSingletons.getSingletons(mContext).getProgramDataManager();
+ TvApplication.getSingletons(mContext).getProgramDataManager();
mCurrentProgram = manager.getCurrentProgram(mChannel.getId());
}
return mCurrentProgram;
@@ -82,11 +78,11 @@ public class ChannelRecord {
return mTotalWatchDurationMs;
}
- public final synchronized WatchedProgram[] getWatchHistory() {
+ public final WatchedProgram[] getWatchHistory() {
return mWatchHistory.toArray(new WatchedProgram[mWatchHistory.size()]);
}
- public synchronized void logWatchHistory(WatchedProgram p) {
+ public void logWatchHistory(WatchedProgram p) {
mWatchHistory.offer(p);
mTotalWatchDurationMs += p.getWatchedDurationMs();
if (mWatchHistory.size() > MAX_HISTORY_SIZE) {
diff --git a/src/com/android/tv/recommendation/NotificationService.java b/src/com/android/tv/recommendation/NotificationService.java
index 201bb103..c4b321e1 100644
--- a/src/com/android/tv/recommendation/NotificationService.java
+++ b/src/com/android/tv/recommendation/NotificationService.java
@@ -40,10 +40,10 @@ import android.text.TextUtils;
import android.util.Log;
import android.util.SparseLongArray;
import android.view.View;
+import com.android.tv.ApplicationSingletons;
import com.android.tv.MainActivityWrapper.OnCurrentChannelChangeListener;
import com.android.tv.R;
-import com.android.tv.Starter;
-import com.android.tv.TvSingletons;
+import com.android.tv.TvApplication;
import com.android.tv.common.WeakHandler;
import com.android.tv.data.Channel;
import com.android.tv.data.Program;
@@ -122,7 +122,7 @@ public class NotificationService extends Service
@Override
public void onCreate() {
if (DEBUG) Log.d(TAG, "onCreate");
- Starter.start(this);
+ TvApplication.setCurrentRunningProcess(this, true);
super.onCreate();
mCurrentNotificationCount = 0;
mNotificationChannels = new long[NOTIFICATION_COUNT];
@@ -146,17 +146,17 @@ public class NotificationService extends Service
getResources().getDimensionPixelOffset(R.dimen.notif_ch_logo_padding_bottom);
mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
- TvSingletons tvSingletons = TvSingletons.getSingletons(this);
- mTvInputManagerHelper = tvSingletons.getTvInputManagerHelper();
+ ApplicationSingletons appSingletons = TvApplication.getSingletons(this);
+ mTvInputManagerHelper = appSingletons.getTvInputManagerHelper();
mHandlerThread = new HandlerThread("tv notification");
mHandlerThread.start();
mHandler = new NotificationHandler(mHandlerThread.getLooper(), this);
mHandler.sendEmptyMessage(MSG_INITIALIZE_RECOMMENDER);
// Just called for early initialization.
- tvSingletons.getChannelDataManager();
- tvSingletons.getProgramDataManager();
- tvSingletons.getMainActivityWrapper().addOnCurrentChannelChangeListener(this);
+ appSingletons.getChannelDataManager();
+ appSingletons.getProgramDataManager();
+ appSingletons.getMainActivityWrapper().addOnCurrentChannelChangeListener(this);
}
@UiThread
@@ -209,7 +209,7 @@ public class NotificationService extends Service
@Override
public void onDestroy() {
- TvSingletons.getSingletons(this)
+ TvApplication.getSingletons(this)
.getMainActivityWrapper()
.removeOnCurrentChannelChangeListener(this);
if (mRecommender != null) {
diff --git a/src/com/android/tv/recommendation/RecommendationDataManager.java b/src/com/android/tv/recommendation/RecommendationDataManager.java
index 8e8455fa..794ca7e2 100644
--- a/src/com/android/tv/recommendation/RecommendationDataManager.java
+++ b/src/com/android/tv/recommendation/RecommendationDataManager.java
@@ -33,13 +33,13 @@ import android.support.annotation.MainThread;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.WorkerThread;
-import com.android.tv.TvSingletons;
+import com.android.tv.TvApplication;
import com.android.tv.common.WeakHandler;
-import com.android.tv.common.util.PermissionUtils;
import com.android.tv.data.Channel;
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.TvUriMatcher;
import java.util.ArrayList;
import java.util.Collection;
@@ -50,7 +50,6 @@ import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
-@SuppressWarnings("TryWithResources") // TODO(b/62143348): remove when error prone check fixed
public class RecommendationDataManager implements WatchedHistoryManager.Listener {
private static final int MSG_START = 1000;
private static final int MSG_STOP = 1001;
@@ -186,7 +185,7 @@ public class RecommendationDataManager implements WatchedHistoryManager.Listener
mHandler = new RecommendationHandler(mHandlerThread.getLooper(), this);
mMainHandler = new RecommendationMainHandler(Looper.getMainLooper(), this);
mContentObserver = new RecommendationContentObserver(mHandler);
- mChannelDataManager = TvSingletons.getSingletons(mContext).getChannelDataManager();
+ mChannelDataManager = TvApplication.getSingletons(mContext).getChannelDataManager();
runOnMainThread(
new Runnable() {
@Override
diff --git a/src/com/android/tv/recommendation/RecordedProgramPreviewUpdater.java b/src/com/android/tv/recommendation/RecordedProgramPreviewUpdater.java
index edc23c53..2b17c510 100644
--- a/src/com/android/tv/recommendation/RecordedProgramPreviewUpdater.java
+++ b/src/com/android/tv/recommendation/RecordedProgramPreviewUpdater.java
@@ -21,7 +21,8 @@ import android.os.Build;
import android.support.annotation.RequiresApi;
import android.text.TextUtils;
import android.util.Log;
-import com.android.tv.TvSingletons;
+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;
@@ -33,10 +34,10 @@ import java.util.Set;
/** Class to update the preview data for {@link RecordedProgram} */
@RequiresApi(Build.VERSION_CODES.O)
-@SuppressWarnings("AndroidApiChecker") // TODO(b/32513850) remove when error prone is updated
public class RecordedProgramPreviewUpdater {
private static final String TAG = "RecordedProgramPreviewUpdater";
- private static final boolean DEBUG = false;
+ // STOPSHIP: set it to false.
+ private static final boolean DEBUG = true;
private static final int RECOMMENDATION_COUNT = 6;
@@ -57,9 +58,9 @@ public class RecordedProgramPreviewUpdater {
private RecordedProgramPreviewUpdater(Context context) {
mContext = context.getApplicationContext();
- TvSingletons tvSingletons = TvSingletons.getSingletons(mContext);
- mPreviewDataManager = tvSingletons.getPreviewDataManager();
- mDvrDataManager = tvSingletons.getDvrDataManager();
+ ApplicationSingletons applicationSingletons = TvApplication.getSingletons(mContext);
+ mPreviewDataManager = applicationSingletons.getPreviewDataManager();
+ mDvrDataManager = applicationSingletons.getDvrDataManager();
mDvrDataManager.addRecordedProgramListener(
new DvrDataManager.RecordedProgramListener() {
@Override
diff --git a/src/com/android/tv/search/DataManagerSearch.java b/src/com/android/tv/search/DataManagerSearch.java
index 0d4a9dbc..a60355f4 100644
--- a/src/com/android/tv/search/DataManagerSearch.java
+++ b/src/com/android/tv/search/DataManagerSearch.java
@@ -26,7 +26,8 @@ import android.os.SystemClock;
import android.support.annotation.MainThread;
import android.text.TextUtils;
import android.util.Log;
-import com.android.tv.TvSingletons;
+import com.android.tv.ApplicationSingletons;
+import com.android.tv.TvApplication;
import com.android.tv.data.Channel;
import com.android.tv.data.ChannelDataManager;
import com.android.tv.data.Program;
@@ -59,9 +60,9 @@ public class DataManagerSearch implements SearchInterface {
DataManagerSearch(Context context) {
mContext = context;
mTvInputManager = (TvInputManager) context.getSystemService(Context.TV_INPUT_SERVICE);
- TvSingletons tvSingletons = TvSingletons.getSingletons(context);
- mChannelDataManager = tvSingletons.getChannelDataManager();
- mProgramDataManager = tvSingletons.getProgramDataManager();
+ ApplicationSingletons appSingletons = TvApplication.getSingletons(context);
+ mChannelDataManager = appSingletons.getChannelDataManager();
+ mProgramDataManager = appSingletons.getProgramDataManager();
}
@Override
diff --git a/src/com/android/tv/search/LocalSearchProvider.java b/src/com/android/tv/search/LocalSearchProvider.java
index 579811e7..dfd585c3 100644
--- a/src/com/android/tv/search/LocalSearchProvider.java
+++ b/src/com/android/tv/search/LocalSearchProvider.java
@@ -27,13 +27,13 @@ import android.support.annotation.NonNull;
import android.support.annotation.VisibleForTesting;
import android.text.TextUtils;
import android.util.Log;
-import com.android.tv.TvSingletons;
+import com.android.tv.TvApplication;
import com.android.tv.common.SoftPreconditions;
-import com.android.tv.common.util.CommonUtils;
-import com.android.tv.common.util.PermissionUtils;
+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;
@@ -84,13 +84,13 @@ public class LocalSearchProvider extends ContentProvider {
@Override
public boolean onCreate() {
- mPerformanceMonitor = TvSingletons.getSingletons(getContext()).getPerformanceMonitor();
+ mPerformanceMonitor = TvApplication.getSingletons(getContext()).getPerformanceMonitor();
return true;
}
@VisibleForTesting
void setSearchInterface(SearchInterface searchInterface) {
- SoftPreconditions.checkState(CommonUtils.isRunningInTest());
+ SoftPreconditions.checkState(TvCommonUtils.isRunningInTest());
mSearchInterface = searchInterface;
}
diff --git a/src/com/android/tv/search/ProgramGuideSearchFragment.java b/src/com/android/tv/search/ProgramGuideSearchFragment.java
index d3fe1554..1ca86d22 100644
--- a/src/com/android/tv/search/ProgramGuideSearchFragment.java
+++ b/src/com/android/tv/search/ProgramGuideSearchFragment.java
@@ -40,8 +40,8 @@ import android.view.View;
import android.view.ViewGroup;
import com.android.tv.MainActivity;
import com.android.tv.R;
-import com.android.tv.common.util.PermissionUtils;
import com.android.tv.util.ImageLoader;
+import com.android.tv.util.PermissionUtils;
import java.util.List;
public class ProgramGuideSearchFragment extends SearchFragment {
@@ -83,8 +83,7 @@ public class ProgramGuideSearchFragment extends SearchFragment {
mMainCardHeight,
createImageLoaderCallback(cardView));
} else {
- cardView.setMainImage(
- mMainActivity.getDrawable(R.drawable.ic_live_channels_96x96));
+ cardView.setMainImage(mMainActivity.getDrawable(R.drawable.ic_launcher));
}
}
@@ -169,7 +168,7 @@ public class ProgramGuideSearchFragment extends SearchFragment {
View v = super.onCreateView(inflater, container, savedInstanceState);
v.setBackgroundResource(R.color.program_guide_scrim);
- setBadgeDrawable(mMainActivity.getDrawable(R.drawable.ic_live_channels_96x96));
+ setBadgeDrawable(mMainActivity.getDrawable(R.drawable.ic_launcher));
setSearchResultProvider(mSearchResultProvider);
setOnItemViewClickedListener(mItemClickedListener);
return v;
diff --git a/src/com/android/tv/search/TvProviderSearch.java b/src/com/android/tv/search/TvProviderSearch.java
index a8640538..d6cf9cc9 100644
--- a/src/com/android/tv/search/TvProviderSearch.java
+++ b/src/com/android/tv/search/TvProviderSearch.java
@@ -33,8 +33,8 @@ import android.support.annotation.WorkerThread;
import android.text.TextUtils;
import android.util.Log;
import com.android.tv.common.TvContentRatingCache;
-import com.android.tv.common.util.PermissionUtils;
import com.android.tv.search.LocalSearchProvider.SearchResult;
+import com.android.tv.util.PermissionUtils;
import com.android.tv.util.Utils;
import java.util.ArrayList;
import java.util.Collections;
@@ -48,7 +48,6 @@ import java.util.Objects;
import java.util.Set;
/** An implementation of {@link SearchInterface} to search query from TvProvider directly. */
-@SuppressWarnings("TryWithResources") // TODO(b/62143348): remove when error prone check fixed
public class TvProviderSearch implements SearchInterface {
private static final String TAG = "TvProviderSearch";
private static final boolean DEBUG = false;
diff --git a/src/com/android/tv/setup/SystemSetupActivity.java b/src/com/android/tv/setup/SystemSetupActivity.java
index 3fefc0c8..df25afa6 100644
--- a/src/com/android/tv/setup/SystemSetupActivity.java
+++ b/src/com/android/tv/setup/SystemSetupActivity.java
@@ -24,12 +24,13 @@ import android.content.Intent;
import android.media.tv.TvInputInfo;
import android.os.Bundle;
import android.widget.Toast;
+import com.android.tv.ApplicationSingletons;
import com.android.tv.R;
import com.android.tv.SetupPassthroughActivity;
-import com.android.tv.TvSingletons;
+import com.android.tv.TvApplication;
+import com.android.tv.common.TvCommonUtils;
import com.android.tv.common.ui.setup.SetupActivity;
import com.android.tv.common.ui.setup.SetupMultiPaneFragment;
-import com.android.tv.common.util.CommonUtils;
import com.android.tv.onboarding.SetupSourcesFragment;
import com.android.tv.util.OnboardingUtils;
import com.android.tv.util.SetupUtils;
@@ -51,7 +52,7 @@ public class SystemSetupActivity extends SetupActivity {
finish();
return;
}
- TvSingletons singletons = TvSingletons.getSingletons(this);
+ ApplicationSingletons singletons = TvApplication.getSingletons(this);
mInputManager = singletons.getTvInputManagerHelper();
}
@@ -85,7 +86,7 @@ public class SystemSetupActivity extends SetupActivity {
params.getString(
SetupSourcesFragment.ACTION_PARAM_KEY_INPUT_ID);
TvInputInfo input = mInputManager.getTvInputInfo(inputId);
- Intent intent = CommonUtils.createSetupIntent(input);
+ Intent intent = TvCommonUtils.createSetupIntent(input);
if (intent == null) {
Toast.makeText(
this,
diff --git a/src/com/android/tv/tuner/ChannelScanFileParser.java b/src/com/android/tv/tuner/ChannelScanFileParser.java
new file mode 100644
index 00000000..50153f89
--- /dev/null
+++ b/src/com/android/tv/tuner/ChannelScanFileParser.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner;
+
+import android.util.Log;
+import com.android.tv.tuner.data.nano.Channel;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Parses plain text formatted scan files, which contain the list of channels. */
+public class ChannelScanFileParser {
+ private static final String TAG = "ChannelScanFileParser";
+
+ public static final class ScanChannel {
+ public final int type;
+ public final int frequency;
+ public final String modulation;
+ public final String filename;
+ /**
+ * Radio frequency (channel) number specified at
+ * https://en.wikipedia.org/wiki/North_American_television_frequencies This can be {@code
+ * null} for cases like cable signal.
+ */
+ public final Integer radioFrequencyNumber;
+
+ public static ScanChannel forTuner(
+ int frequency, String modulation, Integer radioFrequencyNumber) {
+ return new ScanChannel(
+ Channel.TYPE_TUNER, frequency, modulation, null, radioFrequencyNumber);
+ }
+
+ public static ScanChannel forFile(int frequency, String filename) {
+ return new ScanChannel(Channel.TYPE_FILE, frequency, "file:", filename, null);
+ }
+
+ private ScanChannel(
+ int type,
+ int frequency,
+ String modulation,
+ String filename,
+ Integer radioFrequencyNumber) {
+ this.type = type;
+ this.frequency = frequency;
+ this.modulation = modulation;
+ this.filename = filename;
+ this.radioFrequencyNumber = radioFrequencyNumber;
+ }
+ }
+
+ /**
+ * Parses a given scan file and returns the list of {@link ScanChannel} objects.
+ *
+ * @param is {@link InputStream} of a scan file. Each line matches one channel. The line format
+ * of the scan file is as follows:<br>
+ * "A &lt;frequency&gt; &lt;modulation&gt;".
+ * @return a list of {@link ScanChannel} objects parsed
+ */
+ public static List<ScanChannel> parseScanFile(InputStream is) {
+ BufferedReader in = new BufferedReader(new InputStreamReader(is));
+ String line;
+ List<ScanChannel> scanChannelList = new ArrayList<>();
+ try {
+ while ((line = in.readLine()) != null) {
+ if (line.isEmpty()) {
+ continue;
+ }
+ if (line.charAt(0) == '#') {
+ // Skip comment line
+ continue;
+ }
+ String[] tokens = line.split("\\s+");
+ if (tokens.length != 3 && tokens.length != 4) {
+ continue;
+ }
+ scanChannelList.add(
+ ScanChannel.forTuner(
+ Integer.parseInt(tokens[1]),
+ tokens[2],
+ tokens.length == 4 ? Integer.parseInt(tokens[3]) : null));
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "error on parseScanFile()", e);
+ }
+ return scanChannelList;
+ }
+}
diff --git a/src/com/android/tv/tuner/DvbDeviceAccessor.java b/src/com/android/tv/tuner/DvbDeviceAccessor.java
new file mode 100644
index 00000000..217433d2
--- /dev/null
+++ b/src/com/android/tv/tuner/DvbDeviceAccessor.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner;
+
+import android.content.Context;
+import android.media.tv.TvInputManager;
+import android.os.ParcelFileDescriptor;
+import android.support.annotation.IntDef;
+import android.support.annotation.NonNull;
+import android.util.Log;
+import com.android.tv.common.recording.RecordingCapability;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+
+/** Provides with the file descriptors to access DVB device. */
+public class DvbDeviceAccessor {
+ private static final String TAG = "DvbDeviceAccessor";
+
+ @IntDef({DVB_DEVICE_DEMUX, DVB_DEVICE_DVR, DVB_DEVICE_FRONTEND})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface DvbDevice {}
+
+ public static final int DVB_DEVICE_DEMUX = 0; // TvInputManager.DVB_DEVICE_DEMUX;
+ public static final int DVB_DEVICE_DVR = 1; // TvInputManager.DVB_DEVICE_DVR;
+ public static final int DVB_DEVICE_FRONTEND = 2; // TvInputManager.DVB_DEVICE_FRONTEND;
+
+ private static Method sGetDvbDeviceListMethod;
+ private static Method sOpenDvbDeviceMethod;
+
+ private final TvInputManager mTvInputManager;
+
+ static {
+ try {
+ Class tvInputManagerClass = Class.forName("android.media.tv.TvInputManager");
+ Class dvbDeviceInfoClass = Class.forName("android.media.tv.DvbDeviceInfo");
+ sGetDvbDeviceListMethod = tvInputManagerClass.getDeclaredMethod("getDvbDeviceList");
+ sGetDvbDeviceListMethod.setAccessible(true);
+ sOpenDvbDeviceMethod =
+ tvInputManagerClass.getDeclaredMethod(
+ "openDvbDevice", dvbDeviceInfoClass, Integer.TYPE);
+ sOpenDvbDeviceMethod.setAccessible(true);
+ } catch (ClassNotFoundException e) {
+ Log.e(TAG, "Couldn't find class", e);
+ } catch (NoSuchMethodException e) {
+ Log.e(TAG, "Couldn't find method", e);
+ }
+ }
+
+ public DvbDeviceAccessor(Context context) {
+ mTvInputManager = (TvInputManager) context.getSystemService(Context.TV_INPUT_SERVICE);
+ }
+
+ public List<DvbDeviceInfoWrapper> getDvbDeviceList() {
+ try {
+ List<DvbDeviceInfoWrapper> wrapperList = new ArrayList<>();
+ List dvbDeviceInfoList = (List) sGetDvbDeviceListMethod.invoke(mTvInputManager);
+ for (Object dvbDeviceInfo : dvbDeviceInfoList) {
+ wrapperList.add(new DvbDeviceInfoWrapper(dvbDeviceInfo));
+ }
+ Collections.sort(wrapperList);
+ return wrapperList;
+ } catch (IllegalAccessException e) {
+ Log.e(TAG, "Couldn't access", e);
+ } catch (InvocationTargetException e) {
+ Log.e(TAG, "Couldn't invoke", e);
+ }
+ return null;
+ }
+
+ /** Returns the number of currently connected DVB devices. */
+ public int getNumOfDvbDevices() {
+ List<DvbDeviceInfoWrapper> dvbDeviceList = getDvbDeviceList();
+ return dvbDeviceList == null ? 0 : dvbDeviceList.size();
+ }
+
+ public boolean isDvbDeviceAvailable() {
+ try {
+ List dvbDeviceInfoList = (List) sGetDvbDeviceListMethod.invoke(mTvInputManager);
+ return (!dvbDeviceInfoList.isEmpty());
+ } catch (IllegalAccessException e) {
+ Log.e(TAG, "Couldn't access", e);
+ } catch (InvocationTargetException e) {
+ Log.e(TAG, "Couldn't invoke", e);
+ }
+ return false;
+ }
+
+ public ParcelFileDescriptor openDvbDevice(
+ DvbDeviceInfoWrapper deviceInfo, @DvbDevice int device) {
+ try {
+ return (ParcelFileDescriptor)
+ sOpenDvbDeviceMethod.invoke(
+ mTvInputManager, deviceInfo.getDvbDeviceInfo(), device);
+ } catch (IllegalAccessException e) {
+ Log.e(TAG, "Couldn't access", e);
+ } catch (InvocationTargetException e) {
+ Log.e(TAG, "Couldn't invoke", e);
+ }
+ return null;
+ }
+
+ /**
+ * Returns the current recording capability for USB tuner.
+ *
+ * @param inputId the input id to use.
+ */
+ public RecordingCapability getRecordingCapability(String inputId) {
+ List<DvbDeviceInfoWrapper> deviceList = getDvbDeviceList();
+ // TODO(DVR) implement accurate capabilities and updating values when needed.
+ return RecordingCapability.builder()
+ .setInputId(inputId)
+ .setMaxConcurrentPlayingSessions(1)
+ .setMaxConcurrentTunedSessions(deviceList.size())
+ .setMaxConcurrentSessionsOfAllTypes(deviceList.size() + 1)
+ .build();
+ }
+
+ public static class DvbDeviceInfoWrapper implements Comparable<DvbDeviceInfoWrapper> {
+ private static Method sGetAdapterIdMethod;
+ private static Method sGetDeviceIdMethod;
+ private final Object mDvbDeviceInfo;
+ private final int mAdapterId;
+ private final int mDeviceId;
+ private final long mId;
+
+ static {
+ try {
+ Class dvbDeviceInfoClass = Class.forName("android.media.tv.DvbDeviceInfo");
+ sGetAdapterIdMethod = dvbDeviceInfoClass.getDeclaredMethod("getAdapterId");
+ sGetAdapterIdMethod.setAccessible(true);
+ sGetDeviceIdMethod = dvbDeviceInfoClass.getDeclaredMethod("getDeviceId");
+ sGetDeviceIdMethod.setAccessible(true);
+ } catch (ClassNotFoundException e) {
+ Log.e(TAG, "Couldn't find class", e);
+ } catch (NoSuchMethodException e) {
+ Log.e(TAG, "Couldn't find method", e);
+ }
+ }
+
+ public DvbDeviceInfoWrapper(Object dvbDeviceInfo) {
+ mDvbDeviceInfo = dvbDeviceInfo;
+ mAdapterId = initAdapterId();
+ mDeviceId = initDeviceId();
+ mId = (((long) getAdapterId()) << 32) | (getDeviceId() & 0xffffffffL);
+ }
+
+ public long getId() {
+ return mId;
+ }
+
+ public int getAdapterId() {
+ return mAdapterId;
+ }
+
+ private int initAdapterId() {
+ try {
+ return (int) sGetAdapterIdMethod.invoke(mDvbDeviceInfo);
+ } catch (InvocationTargetException e) {
+ Log.e(TAG, "Couldn't invoke", e);
+ } catch (IllegalAccessException e) {
+ Log.e(TAG, "Couldn't access", e);
+ }
+ return -1;
+ }
+
+ public int getDeviceId() {
+ return mDeviceId;
+ }
+
+ private int initDeviceId() {
+ try {
+ return (int) sGetDeviceIdMethod.invoke(mDvbDeviceInfo);
+ } catch (InvocationTargetException e) {
+ Log.e(TAG, "Couldn't invoke", e);
+ } catch (IllegalAccessException e) {
+ Log.e(TAG, "Couldn't access", e);
+ }
+ return -1;
+ }
+
+ public Object getDvbDeviceInfo() {
+ return mDvbDeviceInfo;
+ }
+
+ @Override
+ public int compareTo(@NonNull DvbDeviceInfoWrapper another) {
+ if (getAdapterId() != another.getAdapterId()) {
+ return getAdapterId() - another.getAdapterId();
+ }
+ return getDeviceId() - another.getDeviceId();
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ Locale.US,
+ "DvbDeviceInfo {adapterId: %d, deviceId: %d}",
+ getAdapterId(),
+ getDeviceId());
+ }
+ }
+}
diff --git a/src/com/android/tv/tuner/DvbTunerHal.java b/src/com/android/tv/tuner/DvbTunerHal.java
new file mode 100644
index 00000000..8397400f
--- /dev/null
+++ b/src/com/android/tv/tuner/DvbTunerHal.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner;
+
+import android.content.Context;
+import android.os.ParcelFileDescriptor;
+import android.util.Log;
+import com.android.tv.tuner.DvbDeviceAccessor.DvbDeviceInfoWrapper;
+import java.util.List;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+/** A class to handle a hardware Linux DVB API supported tuner device. */
+public class DvbTunerHal extends TunerHal {
+
+ private static final Object sLock = new Object();
+ // @GuardedBy("sLock")
+ private static final SortedSet<DvbDeviceInfoWrapper> sUsedDvbDevices = new TreeSet<>();
+
+ private final DvbDeviceAccessor mDvbDeviceAccessor;
+ private DvbDeviceInfoWrapper mDvbDeviceInfo;
+
+ protected DvbTunerHal(Context context) {
+ super(context);
+ mDvbDeviceAccessor = new DvbDeviceAccessor(context);
+ }
+
+ @Override
+ protected boolean openFirstAvailable() {
+ List<DvbDeviceInfoWrapper> deviceInfoList = mDvbDeviceAccessor.getDvbDeviceList();
+ if (deviceInfoList == null || deviceInfoList.isEmpty()) {
+ Log.e(TAG, "There's no dvb device attached");
+ return false;
+ }
+ synchronized (sLock) {
+ for (DvbDeviceInfoWrapper deviceInfo : deviceInfoList) {
+ if (!sUsedDvbDevices.contains(deviceInfo)) {
+ if (DEBUG) Log.d(TAG, "Available device info: " + deviceInfo);
+ mDvbDeviceInfo = deviceInfo;
+ sUsedDvbDevices.add(deviceInfo);
+ getDeliverySystemTypeFromDevice();
+ return true;
+ }
+ }
+ }
+ Log.e(TAG, "There's no available dvb devices");
+ return false;
+ }
+
+ /**
+ * Acquires the tuner device. The requested device will be locked to the current instance if
+ * it's not acquired by others.
+ *
+ * @param deviceInfo a tuner device to open
+ * @return {@code true} if the operation was successful, {@code false} otherwise
+ */
+ protected boolean open(DvbDeviceInfoWrapper deviceInfo) {
+ if (deviceInfo == null) {
+ Log.e(TAG, "Device info should not be null");
+ return false;
+ }
+ if (mDvbDeviceInfo != null) {
+ Log.e(TAG, "Already acquired");
+ return false;
+ }
+ List<DvbDeviceInfoWrapper> deviceInfoList = mDvbDeviceAccessor.getDvbDeviceList();
+ if (deviceInfoList == null || deviceInfoList.isEmpty()) {
+ Log.e(TAG, "There's no dvb device attached");
+ return false;
+ }
+ for (DvbDeviceInfoWrapper deviceInfoWrapper : deviceInfoList) {
+ if (deviceInfoWrapper.compareTo(deviceInfo) == 0) {
+ synchronized (sLock) {
+ if (sUsedDvbDevices.contains(deviceInfo)) {
+ Log.e(TAG, deviceInfo + " is already taken");
+ return false;
+ }
+ sUsedDvbDevices.add(deviceInfo);
+ }
+ if (DEBUG) Log.d(TAG, "Available device info: " + deviceInfo);
+ mDvbDeviceInfo = deviceInfo;
+ return true;
+ }
+ }
+ Log.e(TAG, "There's no such dvb device attached");
+ return false;
+ }
+
+ @Override
+ public void close() {
+ if (mDvbDeviceInfo != null) {
+ if (isStreaming()) {
+ stopTune();
+ }
+ nativeFinalize(mDvbDeviceInfo.getId());
+ synchronized (sLock) {
+ sUsedDvbDevices.remove(mDvbDeviceInfo);
+ }
+ mDvbDeviceInfo = null;
+ }
+ }
+
+ @Override
+ protected boolean isDeviceOpen() {
+ return (mDvbDeviceInfo != null);
+ }
+
+ @Override
+ protected long getDeviceId() {
+ if (mDvbDeviceInfo != null) {
+ return mDvbDeviceInfo.getId();
+ }
+ return -1;
+ }
+
+ @Override
+ protected int openDvbFrontEndFd() {
+ if (mDvbDeviceInfo != null) {
+ ParcelFileDescriptor descriptor =
+ mDvbDeviceAccessor.openDvbDevice(
+ mDvbDeviceInfo, DvbDeviceAccessor.DVB_DEVICE_FRONTEND);
+ if (descriptor != null) {
+ return descriptor.detachFd();
+ }
+ }
+ return -1;
+ }
+
+ @Override
+ protected int openDvbDemuxFd() {
+ if (mDvbDeviceInfo != null) {
+ ParcelFileDescriptor descriptor =
+ mDvbDeviceAccessor.openDvbDevice(
+ mDvbDeviceInfo, DvbDeviceAccessor.DVB_DEVICE_DEMUX);
+ if (descriptor != null) {
+ return descriptor.detachFd();
+ }
+ }
+ return -1;
+ }
+
+ @Override
+ protected int openDvbDvrFd() {
+ if (mDvbDeviceInfo != null) {
+ ParcelFileDescriptor descriptor =
+ mDvbDeviceAccessor.openDvbDevice(
+ mDvbDeviceInfo, DvbDeviceAccessor.DVB_DEVICE_DVR);
+ if (descriptor != null) {
+ return descriptor.detachFd();
+ }
+ }
+ return -1;
+ }
+
+ /** Gets the number of USB tuner devices currently present. */
+ public static int getNumberOfDevices(Context context) {
+ 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
new file mode 100644
index 00000000..c8db73c3
--- /dev/null
+++ b/src/com/android/tv/tuner/TunerHal.java
@@ -0,0 +1,358 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner;
+
+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.customization.TvCustomizationManager;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/** A base class to handle a hardware tuner device. */
+public abstract class TunerHal implements AutoCloseable {
+ protected static final String TAG = "TunerHal";
+ protected static final boolean DEBUG = false;
+
+ @IntDef({FILTER_TYPE_OTHER, FILTER_TYPE_AUDIO, FILTER_TYPE_VIDEO, FILTER_TYPE_PCR})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface FilterType {}
+
+ public static final int FILTER_TYPE_OTHER = 0;
+ public static final int FILTER_TYPE_AUDIO = 1;
+ public static final int FILTER_TYPE_VIDEO = 2;
+ public static final int FILTER_TYPE_PCR = 3;
+
+ @StringDef({MODULATION_8VSB, MODULATION_QAM256})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ModulationType {}
+
+ 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;
+
+ static {
+ System.loadLibrary("tunertvinput_jni");
+ }
+
+ /**
+ * Creates a TunerHal instance.
+ *
+ * @param context context for creating the TunerHal instance
+ * @return the TunerHal instance
+ */
+ @WorkerThread
+ public static synchronized TunerHal createInstance(Context context) {
+ TunerHal tunerHal = null;
+ if (DvbTunerHal.getNumberOfDevices(context) > 0) {
+ if (DEBUG) Log.d(TAG, "Use DvbTunerHal");
+ tunerHal = new DvbTunerHal(context);
+ }
+ return tunerHal != null && tunerHal.openFirstAvailable() ? tunerHal : null;
+ }
+
+ /** Gets the number of tuner devices currently present. */
+ @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 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;
+ }
+
+ /**
+ * Returns if tuner input service would use built-in tuners instead of USB tuners or network
+ * tuners.
+ */
+ 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) {
+ mIsStreaming = false;
+ mFrequency = -1;
+ mModulation = null;
+ }
+
+ protected boolean isStreaming() {
+ 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();
+ close();
+ }
+
+ protected native void nativeFinalize(long deviceId);
+
+ /**
+ * Acquires the first available tuner device. If there is a tuner device that is available, the
+ * tuner device will be locked to the current instance.
+ *
+ * @return {@code true} if the operation was successful, {@code false} otherwise
+ */
+ protected abstract boolean openFirstAvailable();
+
+ protected abstract boolean isDeviceOpen();
+
+ protected abstract long getDeviceId();
+
+ /**
+ * Sets the tuner channel. This should be called after acquiring a tuner device.
+ *
+ * @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, String channelNumber) {
+ if (!isDeviceOpen()) {
+ Log.e(TAG, "There's no available device");
+ return false;
+ }
+ if (mIsStreaming) {
+ nativeCloseAllPidFilters(getDeviceId());
+ mIsStreaming = false;
+ }
+
+ // When tuning to a new channel in the same frequency, there's no need to stop current tuner
+ // device completely and the only thing necessary for tuning is reopening pid filters.
+ 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;
+ }
+ int timeout_ms =
+ modulation.equals(MODULATION_8VSB)
+ ? DEFAULT_VSB_TUNE_TIMEOUT_MS
+ : DEFAULT_QAM_TUNE_TIMEOUT_MS;
+ 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;
+ return true;
+ }
+ return false;
+ }
+
+ protected native boolean nativeTune(
+ long deviceId, int frequency, @ModulationType String modulation, int timeout_ms);
+
+ /**
+ * Sets a pid filter. This should be set after setting a channel.
+ *
+ * @param pid a pid number to be added to filter list
+ * @param filterType a type of pid. Must be one of (FILTER_TYPE_XXX)
+ * @return {@code true} if the operation was successful, {@code false} otherwise
+ */
+ public synchronized boolean addPidFilter(int pid, @FilterType int filterType) {
+ if (!isDeviceOpen()) {
+ Log.e(TAG, "There's no available device");
+ return false;
+ }
+ if (pid >= 0 && pid <= 0x1fff) {
+ nativeAddPidFilter(getDeviceId(), pid, filterType);
+ return true;
+ }
+ return false;
+ }
+
+ 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
+ * the tuner ready to accept another tune request.
+ */
+ public synchronized void stopTune() {
+ if (isDeviceOpen()) {
+ if (mIsStreaming) {
+ nativeCloseAllPidFilters(getDeviceId());
+ }
+ nativeStopTune(getDeviceId());
+ }
+ mIsStreaming = false;
+ mFrequency = -1;
+ mModulation = null;
+ }
+
+ public void setHasPendingTune(boolean hasPendingTune) {
+ nativeSetHasPendingTune(getDeviceId(), hasPendingTune);
+ }
+
+ public int getDeliverySystemType() {
+ return mDeliverySystemType;
+ }
+
+ protected native void nativeStopTune(long deviceId);
+
+ /**
+ * This method must be called after {@link TunerHal#tune} and before {@link TunerHal#stopTune}.
+ * Writes at most maxSize TS frames in a buffer provided by the user. The frames employ MPEG
+ * encoding.
+ *
+ * @param javaBuffer a buffer to write the video data in
+ * @param javaBufferSize the max amount of bytes to write in this buffer. Usually this number
+ * should be equal to the length of the buffer.
+ * @return the amount of bytes written in the buffer. Note that this value could be 0 if no new
+ * frames have been obtained since the last call.
+ */
+ public synchronized int readTsStream(byte[] javaBuffer, int javaBufferSize) {
+ if (isDeviceOpen()) {
+ return nativeWriteInBuffer(getDeviceId(), javaBuffer, javaBufferSize);
+ } else {
+ return 0;
+ }
+ }
+
+ protected native int nativeWriteInBuffer(long deviceId, byte[] javaBuffer, int javaBufferSize);
+
+ /**
+ * Opens Linux DVB frontend device. This method is called from native JNI and used only for
+ * DvbTunerHal.
+ */
+ protected int openDvbFrontEndFd() {
+ return -1;
+ }
+
+ /**
+ * Opens Linux DVB demux device. This method is called from native JNI and used only for
+ * DvbTunerHal.
+ */
+ protected int openDvbDemuxFd() {
+ return -1;
+ }
+
+ /**
+ * Opens Linux DVB dvr device. This method is called from native JNI and used only for
+ * 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 02611bbf..829bec1c 100644
--- a/src/com/android/tv/tuner/TunerInputController.java
+++ b/src/com/android/tv/tuner/TunerInputController.java
@@ -17,9 +17,6 @@
package com.android.tv.tuner;
import android.app.AlarmManager;
-import android.app.Notification;
-import android.app.NotificationChannel;
-import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
@@ -27,14 +24,10 @@ import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
-import android.content.pm.PackageManager.NameNotFoundException;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbManager;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
-import android.net.Uri;
import android.os.AsyncTask;
import android.os.Handler;
import android.os.Looper;
@@ -45,94 +38,72 @@ 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.Starter;
import com.android.tv.TvApplication;
-import com.android.tv.TvSingletons;
-import com.android.tv.common.BuildConfig;
-import com.android.tv.common.util.SystemPropertiesProxy;
-
-
-import com.android.tv.tuner.setup.BaseTunerSetupActivity;
+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.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
import java.util.Map;
-import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
- * Controls the package visibility of {@link BaseTunerTvInputService}.
+ * Controls the package visibility of {@link TunerTvInputService}.
*
* <p>Listens to broadcast intent for {@link Intent#ACTION_BOOT_COMPLETED}, {@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 {
- private static final boolean DEBUG = false;
+ 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";
- private static final String PLAY_STORE_LINK_TEMPLATE = "market://details?id=%s";
/** Action of {@link Intent} to check network connection repeatedly when it is necessary. */
- private static final String CHECKING_NETWORK_TUNER_STATUS =
- "com.android.tv.action.CHECKING_NETWORK_TUNER_STATUS";
+ 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 String EXTRA_DEVICE_IP = "com.android.tv.action.extra.DEVICE_IP";
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 String NOTIFICATION_CHANNEL_ID = "tuner_discovery_notification";
- // TODO: Load settings from XML file
private static final TunerDevice[] TUNER_DEVICES = {
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 final ComponentName usbTunerComponent;
- private final ComponentName networkTunerComponent;
- private final ComponentName builtInTunerComponent;
- private final Map<TunerDevice, ComponentName> mTunerServiceMapping = new HashMap<>();
-
- private final Map<ComponentName, String> mTunerApplicationNames = new HashMap<>();
- private final Map<ComponentName, String> mNotificationMessages = new HashMap<>();
- private final Map<ComponentName, Bitmap> mNotificationLargeIcons = new HashMap<>();
-
- private final CheckDvbDeviceHandler mHandler = new CheckDvbDeviceHandler(this);
-
- public TunerInputController(ComponentName embeddedTuner) {
- usbTunerComponent = embeddedTuner;
- networkTunerComponent = usbTunerComponent;
- builtInTunerComponent = usbTunerComponent;
- for (TunerDevice device : TUNER_DEVICES) {
- mTunerServiceMapping.put(device, usbTunerComponent);
- }
- }
-
/** Checks status of USB devices to see if there are available USB tuners connected. */
- public void onCheckingUsbTunerStatus(Context context, String action) {
- onCheckingUsbTunerStatus(context, action, mHandler);
+ public static void onCheckingUsbTunerStatus(Context context, String action) {
+ onCheckingUsbTunerStatus(context, action, new CheckDvbDeviceHandler());
}
- private void onCheckingUsbTunerStatus(
+ private static void onCheckingUsbTunerStatus(
Context context, String action, @NonNull CheckDvbDeviceHandler handler) {
- Set<TunerDevice> connectedUsbTuners = getConnectedUsbTuners(context);
+ 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 (!connectedUsbTuners.isEmpty()) {
+ 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.
@@ -140,37 +111,45 @@ public class TunerInputController {
handler.obtainMessage(MSG_ENABLE_INPUT_SERVICE, context),
DVB_DRIVER_CHECK_DELAY_MS);
} else {
- handleTunerStatusChanged(
+ 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,
- connectedUsbTuners,
+ false,
TextUtils.equals(action, UsbManager.ACTION_USB_DEVICE_DETACHED)
? TunerHal.TUNER_TYPE_USB
: null);
}
}
- private void onNetworkTunerChanged(Context context, boolean enabled) {
+ private static void onNetworkTunerChanged(Context context, boolean enabled) {
SharedPreferences sharedPreferences =
PreferenceManager.getDefaultSharedPreferences(context);
- if (sharedPreferences.contains(PREFERENCE_IS_NETWORK_TUNER_ATTACHED)
- && sharedPreferences.getBoolean(PREFERENCE_IS_NETWORK_TUNER_ATTACHED, false)
- == enabled) {
- // the status is not changed
- return;
- }
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);
+ }
}
- // Network tuner detection is initiated by UI. So the app should not
- // be killed.
- handleTunerStatusChanged(
- context, true, getConnectedUsbTuners(context), TunerHal.TUNER_TYPE_NETWORK);
}
/**
@@ -179,131 +158,75 @@ public class TunerInputController {
* @param context {@link Context} instance
* @return {@code true} if any tuner device we support is plugged in
*/
- private Set<TunerDevice> getConnectedUsbTuners(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);
- Set<TunerDevice> devices = new HashSet<>();
for (UsbDevice device : deviceList.values()) {
if (DEBUG) {
Log.d(TAG, "Device: " + device);
}
for (TunerDevice tuner : TUNER_DEVICES) {
- if (tuner.equalsTo(device) && tuner.isSupported(currentSecurityLevel)) {
+ if (tuner.equals(device) && tuner.isSupported(currentSecurityLevel)) {
Log.i(TAG, "Tuner found");
- devices.add(tuner);
+ return true;
}
}
}
- return devices;
- }
-
- private void handleTunerStatusChanged(
- Context context,
- boolean forceDontKillApp,
- Set<TunerDevice> connectedUsbTuners,
- Integer triggerType) {
- Map<ComponentName, Integer> serviceToEnable = new HashMap<>();
- Set<ComponentName> serviceToDisable = new HashSet<>();
- serviceToDisable.add(builtInTunerComponent);
- serviceToDisable.add(networkTunerComponent);
- if (TunerFeatures.TUNER.isEnabled(context)) {
- // TODO: support both built-in tuner and other tuners at the same time?
- if (TunerHal.useBuiltInTuner(context)) {
- enableTunerTvInputService(
- context, true, false, TunerHal.TUNER_TYPE_BUILT_IN, builtInTunerComponent);
- return;
- }
- SharedPreferences sharedPreferences =
- PreferenceManager.getDefaultSharedPreferences(context);
- if (sharedPreferences.getBoolean(PREFERENCE_IS_NETWORK_TUNER_ATTACHED, false)) {
- serviceToEnable.put(networkTunerComponent, TunerHal.TUNER_TYPE_NETWORK);
- }
- }
- for (TunerDevice device : TUNER_DEVICES) {
- if (TunerFeatures.TUNER.isEnabled(context) && connectedUsbTuners.contains(device)) {
- serviceToEnable.put(mTunerServiceMapping.get(device), TunerHal.TUNER_TYPE_USB);
- } else {
- serviceToDisable.add(mTunerServiceMapping.get(device));
- }
- }
- serviceToDisable.removeAll(serviceToEnable.keySet());
- for (ComponentName serviceComponent : serviceToEnable.keySet()) {
- if (isTunerPackageInstalled(context, serviceComponent)) {
- enableTunerTvInputService(
- context,
- true,
- forceDontKillApp,
- serviceToEnable.get(serviceComponent),
- serviceComponent);
- } else {
- sendNotificationToInstallPackage(context, serviceComponent);
- }
- }
- for (ComponentName serviceComponent : serviceToDisable) {
- if (isTunerPackageInstalled(context, serviceComponent)) {
- enableTunerTvInputService(
- context, false, forceDontKillApp, triggerType, serviceComponent);
- } else {
- cancelNotificationToInstallPackage(context, serviceComponent);
- }
- }
+ return false;
}
/**
- * Enable/disable the component {@link BaseTunerTvInputService}.
+ * Enable/disable the component {@link TunerTvInputService}.
*
* @param context {@link Context} instance
* @param enabled {@code true} to enable the service; otherwise {@code false}
*/
private static void enableTunerTvInputService(
- Context context,
- boolean enabled,
- boolean forceDontKillApp,
- Integer tunerType,
- ComponentName serviceComponent) {
+ 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);
+
+ // Don't kill app by enabling/disabling TvActivity. If LC is killed by enabling/disabling
+ // TvActivity, the following pm.setComponentEnabledSetting doesn't work.
+ ((TvApplication) context.getApplicationContext())
+ .handleInputCountChanged(true, enabled, true);
+ // 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 =
+ 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(serviceComponent)) {
- int flags = forceDontKillApp ? PackageManager.DONT_KILL_APP : 0;
- if (serviceComponent.getPackageName().equals(context.getPackageName())) {
- // Don't kill APP when handling input count changing. Or the following
- // setComponentEnabledSetting() call won't work.
- ((TvApplication) context.getApplicationContext())
- .handleInputCountChanged(true, enabled, true);
- // Bundled input. Don't kill app if LiveChannels app is active since we don't want
- // to kill the running app.
- if (TvSingletons.getSingletons(context).getMainActivityWrapper().isCreated()) {
- flags |= PackageManager.DONT_KILL_APP;
- }
- // Send/cancel the USB tuner TV input setup notification.
- BaseTunerSetupActivity.onTvInputEnabled(context, enabled, tunerType);
- 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 (newState != pm.getComponentEnabledSetting(componentName)) {
+ // 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 && 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();
}
}
- // Enable/disable the USB tuner TV input.
- pm.setComponentEnabledSetting(serviceComponent, newState, flags);
if (DEBUG) Log.d(TAG, "Status updated:" + enabled);
- } else if (enabled && serviceComponent.getPackageName().equals(context.getPackageName())) {
+ } else if (enabled) {
// 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);
@@ -313,50 +236,62 @@ public class TunerInputController {
/**
* Discovers a network tuner. If the network connection is down, it won't repeatedly checking.
*/
- public void executeNetworkTunerDiscoveryAsyncTask(final Context context) {
- executeNetworkTunerDiscoveryAsyncTask(context, 0, 0);
+ 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
+ * @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.
- * @param deviceIp The previous discovered device IP, 0 if none.
*/
- private void executeNetworkTunerDiscoveryAsyncTask(
- final Context context, final long repeatedDurationMs, final int deviceIp) {
- if (!TunerFeatures.NETWORK_TUNER.isEnabled(context)) {
+ private static void executeNetworkTunerDiscoveryAsyncTask(
+ final Context context, final long repeatedDurationMs) {
+ if (!Features.NETWORK_TUNER.isEnabled(context)) {
return;
}
- final Intent networkCheckingIntent = new Intent(context, IntentReceiver.class);
- networkCheckingIntent.setAction(CHECKING_NETWORK_TUNER_STATUS);
- if (!isNetworkConnected(context) && repeatedDurationMs > 0) {
- sendCheckingAlarm(context, networkCheckingIntent, repeatedDurationMs);
- } else {
- new AsyncTask<Void, Void, Boolean>() {
- @Override
- protected Boolean doInBackground(Void... params) {
- Boolean result = null;
+ new AsyncTask<Void, Void, Boolean>() {
+ @Override
+ protected Boolean doInBackground(Void... params) {
+ if (isNetworkConnected(context)) {
// Implement and execute network tuner discovery AsyncTask here.
- return result;
+ } 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 foundNetworkTuner) {
- if (foundNetworkTuner == null) {
- return;
- }
- sendCheckingAlarm(
- context,
- networkCheckingIntent,
- foundNetworkTuner ? INITIAL_CHECKING_DURATION_MS : repeatedDurationMs);
- onNetworkTunerChanged(context, foundNetworkTuner);
+ @Override
+ protected void onPostExecute(Boolean result) {
+ if (result == null) {
+ return;
}
- }.execute();
- }
+ onNetworkTunerChanged(context, result);
+ }
+ }.execute();
}
private static boolean isNetworkConnected(Context context) {
@@ -366,119 +301,33 @@ public class TunerInputController {
return networkInfo != null && networkInfo.isConnected();
}
- private static void sendCheckingAlarm(Context context, Intent intent, long delayMs) {
- AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
- intent.putExtra(EXTRA_CHECKING_DURATION, delayMs);
- PendingIntent alarmIntent =
- PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
- alarmManager.set(
- AlarmManager.ELAPSED_REALTIME,
- SystemClock.elapsedRealtime() + delayMs,
- alarmIntent);
- }
-
- private static boolean isTunerPackageInstalled(
- Context context, ComponentName serviceComponent) {
- try {
- context.getPackageManager().getPackageInfo(serviceComponent.getPackageName(), 0);
- return true;
- } catch (NameNotFoundException e) {
- return false;
- }
- }
-
- private void sendNotificationToInstallPackage(Context context, ComponentName serviceComponent) {
- if (!BuildConfig.ENG) {
- return;
- }
- String applicationName = mTunerApplicationNames.get(serviceComponent);
- if (applicationName == null) {
- applicationName = context.getString(R.string.tuner_install_default_application_name);
- }
- String contentTitle =
- context.getString(
- R.string.tuner_install_notification_content_title, applicationName);
- String contentText = mNotificationMessages.get(serviceComponent);
- if (contentText == null) {
- contentText = context.getString(R.string.tuner_install_notification_content_text);
- }
- Bitmap largeIcon = mNotificationLargeIcons.get(serviceComponent);
- if (largeIcon == null) {
- // TODO: Make a better default image.
- largeIcon = BitmapFactory.decodeResource(context.getResources(), R.drawable.ic_store);
- }
- NotificationManager notificationManager =
- (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
- if (notificationManager.getNotificationChannel(NOTIFICATION_CHANNEL_ID) == null) {
- createNotificationChannel(context, notificationManager);
- }
- Intent intent = new Intent(Intent.ACTION_VIEW);
- intent.setData(
- Uri.parse(
- String.format(
- PLAY_STORE_LINK_TEMPLATE, serviceComponent.getPackageName())));
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- Notification.Builder builder = new Notification.Builder(context, NOTIFICATION_CHANNEL_ID);
- builder.setAutoCancel(true)
- .setSmallIcon(R.drawable.ic_launcher_s)
- .setLargeIcon(largeIcon)
- .setContentTitle(contentTitle)
- .setContentText(contentText)
- .setCategory(Notification.CATEGORY_RECOMMENDATION)
- .setContentIntent(PendingIntent.getActivity(context, 0, intent, 0));
- notificationManager.notify(serviceComponent.getPackageName(), 0, builder.build());
- }
-
- private static void cancelNotificationToInstallPackage(
- Context context, ComponentName serviceComponent) {
- NotificationManager notificationManager =
- (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
- notificationManager.cancel(serviceComponent.getPackageName(), 0);
- }
-
- private static void createNotificationChannel(
- Context context, NotificationManager notificationManager) {
- notificationManager.createNotificationChannel(
- new NotificationChannel(
- NOTIFICATION_CHANNEL_ID,
- context.getResources()
- .getString(R.string.ut_setup_notification_channel_name),
- NotificationManager.IMPORTANCE_HIGH));
- }
-
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);
- Starter.start(context);
- TunerInputController tunerInputController =
- TvSingletons.getSingletons(context).getTunerInputController();
- if (!TunerFeatures.TUNER.isEnabled(context)) {
- tunerInputController.handleTunerStatusChanged(
- context, false, Collections.emptySet(), null);
+ TvApplication.setCurrentRunningProcess(context, true);
+ if (!Features.TUNER.isEnabled(context)) {
+ enableTunerTvInputService(context, false, false, null);
return;
}
switch (intent.getAction()) {
case Intent.ACTION_BOOT_COMPLETED:
- tunerInputController.executeNetworkTunerDiscoveryAsyncTask(
- context, INITIAL_CHECKING_DURATION_MS, 0);
- // fall through
+ executeNetworkTunerDiscoveryAsyncTask(context, INITIAL_CHECKING_DURATION_MS);
case TvApplication.ACTION_APPLICATION_FIRST_LAUNCHED:
case UsbManager.ACTION_USB_DEVICE_ATTACHED:
case UsbManager.ACTION_USB_DEVICE_DETACHED:
- tunerInputController.onCheckingUsbTunerStatus(context, intent.getAction());
+ onCheckingUsbTunerStatus(context, intent.getAction(), mHandler);
break;
- case CHECKING_NETWORK_TUNER_STATUS:
+ case CHECKING_NETWORK_CONNECTION:
long repeatedDurationMs =
intent.getLongExtra(
EXTRA_CHECKING_DURATION, INITIAL_CHECKING_DURATION_MS);
- tunerInputController.executeNetworkTunerDiscoveryAsyncTask(
+ executeNetworkTunerDiscoveryAsyncTask(
context,
- Math.min(repeatedDurationMs * 2, MAXIMUM_CHECKING_DURATION_MS),
- intent.getIntExtra(EXTRA_DEVICE_IP, 0));
+ Math.min(repeatedDurationMs * 2, MAXIMUM_CHECKING_DURATION_MS));
break;
- default: // fall out
}
}
}
@@ -500,7 +349,7 @@ public class TunerInputController {
this.minSecurityLevel = minSecurityLevel;
}
- private boolean equalsTo(UsbDevice device) {
+ private boolean equals(UsbDevice device) {
return device.getVendorId() == vendorId && device.getProductId() == productId;
}
@@ -523,13 +372,10 @@ public class TunerInputController {
}
private static class CheckDvbDeviceHandler extends Handler {
-
- private final TunerInputController mTunerInputController;
private DvbDeviceAccessor mDvbDeviceAccessor;
- CheckDvbDeviceHandler(TunerInputController tunerInputController) {
+ CheckDvbDeviceHandler() {
super(Looper.getMainLooper());
- this.mTunerInputController = tunerInputController;
}
@Override
@@ -541,15 +387,9 @@ public class TunerInputController {
mDvbDeviceAccessor = new DvbDeviceAccessor(context);
}
boolean enabled = mDvbDeviceAccessor.isDvbDeviceAvailable();
- mTunerInputController.handleTunerStatusChanged(
- context,
- false,
- enabled
- ? mTunerInputController.getConnectedUsbTuners(context)
- : Collections.emptySet(),
- TunerHal.TUNER_TYPE_USB);
+ enableTunerTvInputService(
+ context, enabled, false, enabled ? TunerHal.TUNER_TYPE_USB : null);
break;
- default: // fall out
}
}
}
diff --git a/src/com/android/tv/tuner/TunerPreferenceProvider.java b/src/com/android/tv/tuner/TunerPreferenceProvider.java
new file mode 100644
index 00000000..425c30ac
--- /dev/null
+++ b/src/com/android/tv/tuner/TunerPreferenceProvider.java
@@ -0,0 +1,214 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.UriMatcher;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteException;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.net.Uri;
+
+/**
+ * A content provider for storing the preferences. It's used across TV app and USB tuner TV input.
+ */
+public class TunerPreferenceProvider extends ContentProvider {
+ /** The authority of the provider */
+ public static final String AUTHORITY = "com.android.tv.tuner.preferences";
+
+ private static final String PATH_PREFERENCES = "preferences";
+
+ private static final int DATABASE_VERSION = 1;
+ private static final String DATABASE_NAME = "usbtuner_preferences.db";
+ private static final String PREFERENCES_TABLE = "preferences";
+
+ private static final int MATCH_PREFERENCE = 1;
+ private static final int MATCH_PREFERENCE_KEY = 2;
+
+ private static final UriMatcher sUriMatcher;
+
+ private DatabaseOpenHelper mDatabaseOpenHelper;
+
+ static {
+ sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
+ sUriMatcher.addURI(AUTHORITY, "preferences", MATCH_PREFERENCE);
+ sUriMatcher.addURI(AUTHORITY, "preferences/*", MATCH_PREFERENCE_KEY);
+ }
+
+ /**
+ * Builds a Uri that points to a specific preference.
+ *
+ * @param key a key of the preference to point to
+ */
+ public static Uri buildPreferenceUri(String key) {
+ return Preferences.CONTENT_URI.buildUpon().appendPath(key).build();
+ }
+
+ /** Columns definitions for the preferences table. */
+ public interface Preferences {
+
+ /** The content:// style for the preferences table. */
+ Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/" + PATH_PREFERENCES);
+
+ /** The MIME type of a directory of preferences. */
+ String CONTENT_TYPE = "vnd.android.cursor.dir/preferences";
+
+ /** The MIME type of a single preference. */
+ String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/preferences";
+
+ /**
+ * The ID of this preference.
+ *
+ * <p>This is auto-incremented.
+ *
+ * <p>Type: INTEGER
+ */
+ String _ID = "_id";
+
+ /**
+ * The key of this preference.
+ *
+ * <p>Should be unique.
+ *
+ * <p>Type: TEXT
+ */
+ String COLUMN_KEY = "key";
+
+ /**
+ * The value of this preference.
+ *
+ * <p>Type: TEXT
+ */
+ String COLUMN_VALUE = "value";
+ }
+
+ private static class DatabaseOpenHelper extends SQLiteOpenHelper {
+ public DatabaseOpenHelper(Context context) {
+ super(context, DATABASE_NAME, null, DATABASE_VERSION);
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ db.execSQL(
+ "CREATE TABLE "
+ + PREFERENCES_TABLE
+ + " ("
+ + Preferences._ID
+ + " INTEGER PRIMARY KEY AUTOINCREMENT,"
+ + Preferences.COLUMN_KEY
+ + " TEXT NOT NULL,"
+ + Preferences.COLUMN_VALUE
+ + " TEXT);");
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ // No-op
+ }
+ }
+
+ @Override
+ public boolean onCreate() {
+ mDatabaseOpenHelper = new DatabaseOpenHelper(getContext());
+ return true;
+ }
+
+ @Override
+ public Cursor query(
+ Uri uri,
+ String[] projection,
+ String selection,
+ String[] selectionArgs,
+ String sortOrder) {
+ int match = sUriMatcher.match(uri);
+ if (match != MATCH_PREFERENCE && match != MATCH_PREFERENCE_KEY) {
+ throw new UnsupportedOperationException();
+ }
+ SQLiteDatabase db = mDatabaseOpenHelper.getReadableDatabase();
+ Cursor cursor =
+ db.query(
+ PREFERENCES_TABLE,
+ projection,
+ selection,
+ selectionArgs,
+ null,
+ null,
+ sortOrder);
+ cursor.setNotificationUri(getContext().getContentResolver(), uri);
+ return cursor;
+ }
+
+ @Override
+ public String getType(Uri uri) {
+ switch (sUriMatcher.match(uri)) {
+ case MATCH_PREFERENCE:
+ return Preferences.CONTENT_TYPE;
+ case MATCH_PREFERENCE_KEY:
+ return Preferences.CONTENT_ITEM_TYPE;
+ }
+ throw new IllegalArgumentException("Unknown URI " + uri);
+ }
+
+ /**
+ * Inserts a preference row into the preference table.
+ *
+ * <p>If a key is already exists in the table, it removes the old row and inserts a new row.
+ *
+ * @param uri the URL of the table to insert into
+ * @param values the initial values for the newly inserted row
+ * @return the URL of the newly created row
+ */
+ @Override
+ public Uri insert(Uri uri, ContentValues values) {
+ if (sUriMatcher.match(uri) != MATCH_PREFERENCE) {
+ throw new UnsupportedOperationException();
+ }
+ return insertRow(uri, values);
+ }
+
+ private Uri insertRow(Uri uri, ContentValues values) {
+ SQLiteDatabase db = mDatabaseOpenHelper.getWritableDatabase();
+
+ // Remove the old row.
+ db.delete(
+ PREFERENCES_TABLE,
+ Preferences.COLUMN_KEY + " like ?",
+ new String[] {values.getAsString(Preferences.COLUMN_KEY)});
+
+ long rowId = db.insert(PREFERENCES_TABLE, null, values);
+ if (rowId > 0) {
+ Uri rowUri = buildPreferenceUri(values.getAsString(Preferences.COLUMN_KEY));
+ getContext().getContentResolver().notifyChange(rowUri, null);
+ return rowUri;
+ }
+
+ throw new SQLiteException("Failed to insert row into " + uri);
+ }
+
+ @Override
+ public int delete(Uri uri, String selection, String[] selectionArgs) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/src/com/android/tv/tuner/TunerPreferences.java b/src/com/android/tv/tuner/TunerPreferences.java
new file mode 100644
index 00000000..62a4ce99
--- /dev/null
+++ b/src/com/android/tv/tuner/TunerPreferences.java
@@ -0,0 +1,428 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.database.ContentObserver;
+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. */
+public class TunerPreferences {
+ private static final String TAG = "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
+ public static void initialize(final Context context) {
+ if (sInitialized) {
+ return;
+ }
+ sInitialized = true;
+ if (useContentProvider(context)) {
+ loadPreferences(context);
+ sContentObserver =
+ new ContentObserver(new Handler()) {
+ @Override
+ public void onChange(boolean selfChange) {
+ loadPreferences(context);
+ }
+ };
+ context.getContentResolver()
+ .registerContentObserver(
+ TunerPreferenceProvider.Preferences.CONTENT_URI,
+ true,
+ sContentObserver);
+ } else {
+ new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ getSharedPreferences(context);
+ return null;
+ }
+ }.execute();
+ }
+ }
+
+ /** Releases the resources. */
+ 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;
+ }
+
+ /**
+ * Loads the preferences from database.
+ *
+ * <p>This preferences is used across processes, so the preferences should be loaded again when
+ * the databases changes.
+ */
+ @MainThread
+ public static void loadPreferences(Context context) {
+ if (sLoadPreferencesTask != null
+ && sLoadPreferencesTask.getStatus() != AsyncTask.Status.FINISHED) {
+ sLoadPreferencesTask.cancel(true);
+ }
+ sLoadPreferencesTask = new LoadPreferencesTask(context);
+ sLoadPreferencesTask.execute();
+ }
+
+ private static boolean useContentProvider(Context context) {
+ // If TIS is a part of LC, it should use ContentProvider to resolve multiple process access.
+ return TisConfiguration.isPackagedWithLiveChannels(context);
+ }
+
+ public static synchronized int getChannelDataVersion(Context context) {
+ SoftPreconditions.checkState(sInitialized);
+ if (useContentProvider(context)) {
+ return sPreferenceValues.getInt(
+ PREFS_KEY_CHANNEL_DATA_VERSION, CHANNEL_DATA_VERSION_NOT_SET);
+ } else {
+ return getSharedPreferences(context)
+ .getInt(
+ TunerPreferences.PREFS_KEY_CHANNEL_DATA_VERSION,
+ CHANNEL_DATA_VERSION_NOT_SET);
+ }
+ }
+
+ public static synchronized void setChannelDataVersion(Context context, int version) {
+ if (useContentProvider(context)) {
+ setPreference(context, PREFS_KEY_CHANNEL_DATA_VERSION, version);
+ } else {
+ getSharedPreferences(context)
+ .edit()
+ .putInt(TunerPreferences.PREFS_KEY_CHANNEL_DATA_VERSION, version)
+ .apply();
+ }
+ }
+
+ public static synchronized int getScannedChannelCount(Context context) {
+ SoftPreconditions.checkState(sInitialized);
+ if (useContentProvider(context)) {
+ return sPreferenceValues.getInt(PREFS_KEY_SCANNED_CHANNEL_COUNT);
+ } else {
+ return getSharedPreferences(context)
+ .getInt(TunerPreferences.PREFS_KEY_SCANNED_CHANNEL_COUNT, 0);
+ }
+ }
+
+ public static synchronized void setScannedChannelCount(Context context, int channelCount) {
+ if (useContentProvider(context)) {
+ setPreference(context, PREFS_KEY_SCANNED_CHANNEL_COUNT, channelCount);
+ } else {
+ getSharedPreferences(context)
+ .edit()
+ .putInt(TunerPreferences.PREFS_KEY_SCANNED_CHANNEL_COUNT, channelCount)
+ .apply();
+ }
+ }
+
+ 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);
+ } else {
+ return getSharedPreferences(context)
+ .getBoolean(TunerPreferences.PREFS_KEY_SCAN_DONE, false);
+ }
+ }
+
+ public static synchronized void setScanDone(Context context) {
+ if (useContentProvider(context)) {
+ setPreference(context, PREFS_KEY_SCAN_DONE, true);
+ } else {
+ getSharedPreferences(context)
+ .edit()
+ .putBoolean(TunerPreferences.PREFS_KEY_SCAN_DONE, true)
+ .apply();
+ }
+ }
+
+ public static synchronized boolean shouldShowSetupActivity(Context context) {
+ SoftPreconditions.checkState(sInitialized);
+ if (useContentProvider(context)) {
+ return sPreferenceValues.getBoolean(PREFS_KEY_LAUNCH_SETUP);
+ } else {
+ return getSharedPreferences(context)
+ .getBoolean(TunerPreferences.PREFS_KEY_LAUNCH_SETUP, false);
+ }
+ }
+
+ public static synchronized void setShouldShowSetupActivity(Context context, boolean need) {
+ if (useContentProvider(context)) {
+ setPreference(context, PREFS_KEY_LAUNCH_SETUP, need);
+ } else {
+ getSharedPreferences(context)
+ .edit()
+ .putBoolean(TunerPreferences.PREFS_KEY_LAUNCH_SETUP, need)
+ .apply();
+ }
+ }
+
+ 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);
+ } else {
+ return getSharedPreferences(context)
+ .getBoolean(TunerPreferences.PREFS_KEY_STORE_TS_STREAM, false);
+ }
+ }
+
+ public static synchronized void setStoreTsStream(Context context, boolean shouldStore) {
+ if (useContentProvider(context)) {
+ setPreference(context, PREFS_KEY_STORE_TS_STREAM, shouldStore);
+ } else {
+ getSharedPreferences(context)
+ .edit()
+ .putBoolean(TunerPreferences.PREFS_KEY_STORE_TS_STREAM, shouldStore)
+ .apply();
+ }
+ }
+
+ private static SharedPreferences getSharedPreferences(Context context) {
+ return context.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE);
+ }
+
+ 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) {
+ ContentResolver resolver = context.getContentResolver();
+ ContentValues values = new ContentValues();
+ values.put(Preferences.COLUMN_KEY, key);
+ values.put(Preferences.COLUMN_VALUE, value);
+ try {
+ resolver.insert(Preferences.CONTENT_URI, values);
+ } catch (Exception e) {
+ SoftPreconditions.warn(
+ TAG, "setPreference", "Error writing preference values", e);
+ }
+ return null;
+ }
+ }.execute();
+ }
+
+ private static class LoadPreferencesTask extends AsyncTask<Void, Void, Bundle> {
+ private final Context mContext;
+
+ private LoadPreferencesTask(Context context) {
+ mContext = context;
+ }
+
+ @Override
+ protected Bundle doInBackground(Void... params) {
+ Bundle bundle = new Bundle();
+ ContentResolver resolver = mContext.getContentResolver();
+ String[] projection = new String[] {Preferences.COLUMN_KEY, Preferences.COLUMN_VALUE};
+ try (Cursor cursor =
+ resolver.query(Preferences.CONTENT_URI, projection, null, null, null)) {
+ if (cursor != null) {
+ while (!isCancelled() && cursor.moveToNext()) {
+ 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) {
+ // Does nothing.
+ }
+ break;
+ case PREFS_KEY_SCAN_DONE:
+ case PREFS_KEY_LAUNCH_SETUP:
+ case PREFS_KEY_STORE_TS_STREAM:
+ bundle.putBoolean(key, Boolean.parseBoolean(value));
+ break;
+ case PREFS_KEY_LAST_POSTAL_CODE:
+ bundle.putString(key, value);
+ break;
+ }
+ }
+ }
+ } catch (Exception e) {
+ SoftPreconditions.warn(TAG, "getPreference", "Error querying preference values", e);
+ return null;
+ }
+ return bundle;
+ }
+
+ @Override
+ protected void onPostExecute(Bundle bundle) {
+ synchronized (TunerPreferences.class) {
+ if (bundle != null) {
+ sPreferenceValues.putAll(bundle);
+ }
+ }
+ if (sPreferencesChangedListener != null) {
+ sPreferencesChangedListener.onTunerPreferencesChanged();
+ }
+ }
+ }
+}
diff --git a/src/com/android/tv/tuner/cc/CaptionLayout.java b/src/com/android/tv/tuner/cc/CaptionLayout.java
new file mode 100644
index 00000000..eb9ad463
--- /dev/null
+++ b/src/com/android/tv/tuner/cc/CaptionLayout.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.cc;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack;
+import com.android.tv.tuner.layout.ScaledLayout;
+
+/**
+ * Layout containing the safe title area that helps the closed captions look more prominent. This is
+ * required by CEA-708B.
+ */
+public class CaptionLayout extends ScaledLayout {
+ // The safe title area has 10% margins of the screen.
+ private static final float SAFE_TITLE_AREA_SCALE_START_X = 0.1f;
+ private static final float SAFE_TITLE_AREA_SCALE_END_X = 0.9f;
+ private static final float SAFE_TITLE_AREA_SCALE_START_Y = 0.1f;
+ private static final float SAFE_TITLE_AREA_SCALE_END_Y = 0.9f;
+
+ private final ScaledLayout mSafeTitleAreaLayout;
+ private AtscCaptionTrack mCaptionTrack;
+
+ public CaptionLayout(Context context) {
+ this(context, null);
+ }
+
+ public CaptionLayout(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public CaptionLayout(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ mSafeTitleAreaLayout = new ScaledLayout(context);
+ addView(
+ mSafeTitleAreaLayout,
+ new ScaledLayoutParams(
+ SAFE_TITLE_AREA_SCALE_START_X, SAFE_TITLE_AREA_SCALE_END_X,
+ SAFE_TITLE_AREA_SCALE_START_Y, SAFE_TITLE_AREA_SCALE_END_Y));
+ }
+
+ public void addOrUpdateViewToSafeTitleArea(
+ CaptionWindowLayout captionWindowLayout, ScaledLayoutParams scaledLayoutParams) {
+ int index = mSafeTitleAreaLayout.indexOfChild(captionWindowLayout);
+ if (index < 0) {
+ mSafeTitleAreaLayout.addView(captionWindowLayout, scaledLayoutParams);
+ return;
+ }
+ mSafeTitleAreaLayout.updateViewLayout(captionWindowLayout, scaledLayoutParams);
+ }
+
+ public void removeViewFromSafeTitleArea(CaptionWindowLayout captionWindowLayout) {
+ mSafeTitleAreaLayout.removeView(captionWindowLayout);
+ }
+
+ public void setCaptionTrack(AtscCaptionTrack captionTrack) {
+ mCaptionTrack = captionTrack;
+ }
+
+ public AtscCaptionTrack getCaptionTrack() {
+ return mCaptionTrack;
+ }
+}
diff --git a/src/com/android/tv/tuner/cc/CaptionTrackRenderer.java b/src/com/android/tv/tuner/cc/CaptionTrackRenderer.java
new file mode 100644
index 00000000..bcb8e1c0
--- /dev/null
+++ b/src/com/android/tv/tuner/cc/CaptionTrackRenderer.java
@@ -0,0 +1,340 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.cc;
+
+import android.os.Handler;
+import android.os.Message;
+import android.util.Log;
+import android.view.View;
+import com.android.tv.tuner.data.Cea708Data.CaptionEvent;
+import com.android.tv.tuner.data.Cea708Data.CaptionPenAttr;
+import com.android.tv.tuner.data.Cea708Data.CaptionPenColor;
+import com.android.tv.tuner.data.Cea708Data.CaptionPenLocation;
+import com.android.tv.tuner.data.Cea708Data.CaptionWindow;
+import com.android.tv.tuner.data.Cea708Data.CaptionWindowAttr;
+import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack;
+import java.util.ArrayList;
+import java.util.concurrent.TimeUnit;
+
+/** Decodes and renders CEA-708. */
+public class CaptionTrackRenderer implements Handler.Callback {
+ // TODO: Remaining works
+ // CaptionTrackRenderer does not support the full spec of CEA-708. The remaining works are
+ // described in the follows.
+ // C0 Table: Backspace, FF, and HCR are not supported. The rule for P16 is not standardized but
+ // it is handled as EUC-KR charset for korea broadcasting.
+ // C1 Table: All styles of windows and pens except underline, italic, pen size, and pen offset
+ // specified in CEA-708 are ignored and this follows system wide cc preferences for
+ // look and feel. SetPenLocation is not implemented.
+ // G2 Table: TSP, NBTSP and BLK are not supported.
+ // Text/commands: Word wrapping, fonts, row and column locking are not supported.
+
+ private static final String TAG = "CaptionTrackRenderer";
+ private static final boolean DEBUG = false;
+
+ private static final long DELAY_IN_MILLIS = TimeUnit.MILLISECONDS.toMillis(100);
+
+ // According to CEA-708B, there can exist up to 8 caption windows.
+ private static final int CAPTION_WINDOWS_MAX = 8;
+ private static final int CAPTION_ALL_WINDOWS_BITMAP = 255;
+
+ private static final int MSG_DELAY_CANCEL = 1;
+ private static final int MSG_CAPTION_CLEAR = 2;
+
+ private static final long CAPTION_CLEAR_INTERVAL_MS = 60000;
+
+ private final CaptionLayout mCaptionLayout;
+ private boolean mIsDelayed = false;
+ private CaptionWindowLayout mCurrentWindowLayout;
+ private final CaptionWindowLayout[] mCaptionWindowLayouts =
+ new CaptionWindowLayout[CAPTION_WINDOWS_MAX];
+ private final ArrayList<CaptionEvent> mPendingCaptionEvents = new ArrayList<>();
+ private final Handler mHandler;
+
+ public CaptionTrackRenderer(CaptionLayout captionLayout) {
+ mCaptionLayout = captionLayout;
+ mHandler = new Handler(this);
+ }
+
+ @Override
+ public boolean handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_DELAY_CANCEL:
+ delayCancel();
+ return true;
+ case MSG_CAPTION_CLEAR:
+ clearWindows(CAPTION_ALL_WINDOWS_BITMAP);
+ return true;
+ }
+ return false;
+ }
+
+ public void start(AtscCaptionTrack captionTrack) {
+ if (captionTrack == null) {
+ stop();
+ return;
+ }
+ if (DEBUG) {
+ Log.d(TAG, "Start captionTrack " + captionTrack.language);
+ }
+ reset();
+ mCaptionLayout.setCaptionTrack(captionTrack);
+ mCaptionLayout.setVisibility(View.VISIBLE);
+ }
+
+ public void stop() {
+ if (DEBUG) {
+ Log.d(TAG, "Stop captionTrack");
+ }
+ mCaptionLayout.setVisibility(View.INVISIBLE);
+ mHandler.removeMessages(MSG_CAPTION_CLEAR);
+ }
+
+ public void processCaptionEvent(CaptionEvent event) {
+ if (mIsDelayed) {
+ mPendingCaptionEvents.add(event);
+ return;
+ }
+ switch (event.type) {
+ case Cea708Parser.CAPTION_EMIT_TYPE_BUFFER:
+ sendBufferToCurrentWindow((String) event.obj);
+ break;
+ case Cea708Parser.CAPTION_EMIT_TYPE_CONTROL:
+ sendControlToCurrentWindow((char) event.obj);
+ break;
+ case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_CWX:
+ setCurrentWindowLayout((int) event.obj);
+ break;
+ case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_CLW:
+ clearWindows((int) event.obj);
+ break;
+ case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_DSW:
+ displayWindows((int) event.obj);
+ break;
+ case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_HDW:
+ hideWindows((int) event.obj);
+ break;
+ case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_TGW:
+ toggleWindows((int) event.obj);
+ break;
+ case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_DLW:
+ deleteWindows((int) event.obj);
+ break;
+ case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_DLY:
+ delay((int) event.obj);
+ break;
+ case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_DLC:
+ delayCancel();
+ break;
+ case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_RST:
+ reset();
+ break;
+ case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_SPA:
+ setPenAttr((CaptionPenAttr) event.obj);
+ break;
+ case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_SPC:
+ setPenColor((CaptionPenColor) event.obj);
+ break;
+ case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_SPL:
+ setPenLocation((CaptionPenLocation) event.obj);
+ break;
+ case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_SWA:
+ setWindowAttr((CaptionWindowAttr) event.obj);
+ break;
+ case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_DFX:
+ defineWindow((CaptionWindow) event.obj);
+ break;
+ }
+ }
+
+ // The window related caption commands
+ private void setCurrentWindowLayout(int windowId) {
+ if (windowId < 0 || windowId >= mCaptionWindowLayouts.length) {
+ return;
+ }
+ CaptionWindowLayout windowLayout = mCaptionWindowLayouts[windowId];
+ if (windowLayout == null) {
+ return;
+ }
+ if (DEBUG) {
+ Log.d(TAG, "setCurrentWindowLayout to " + windowId);
+ }
+ mCurrentWindowLayout = windowLayout;
+ }
+
+ // Each bit of windowBitmap indicates a window.
+ // If a bit is set, the window id is the same as the number of the trailing zeros of the bit.
+ private ArrayList<CaptionWindowLayout> getWindowsFromBitmap(int windowBitmap) {
+ ArrayList<CaptionWindowLayout> windows = new ArrayList<>();
+ for (int i = 0; i < CAPTION_WINDOWS_MAX; ++i) {
+ if ((windowBitmap & (1 << i)) != 0) {
+ CaptionWindowLayout windowLayout = mCaptionWindowLayouts[i];
+ if (windowLayout != null) {
+ windows.add(windowLayout);
+ }
+ }
+ }
+ return windows;
+ }
+
+ private void clearWindows(int windowBitmap) {
+ if (windowBitmap == 0) {
+ return;
+ }
+ for (CaptionWindowLayout windowLayout : getWindowsFromBitmap(windowBitmap)) {
+ windowLayout.clear();
+ }
+ }
+
+ private void displayWindows(int windowBitmap) {
+ if (windowBitmap == 0) {
+ return;
+ }
+ for (CaptionWindowLayout windowLayout : getWindowsFromBitmap(windowBitmap)) {
+ windowLayout.show();
+ }
+ }
+
+ private void hideWindows(int windowBitmap) {
+ if (windowBitmap == 0) {
+ return;
+ }
+ for (CaptionWindowLayout windowLayout : getWindowsFromBitmap(windowBitmap)) {
+ windowLayout.hide();
+ }
+ }
+
+ private void toggleWindows(int windowBitmap) {
+ if (windowBitmap == 0) {
+ return;
+ }
+ for (CaptionWindowLayout windowLayout : getWindowsFromBitmap(windowBitmap)) {
+ if (windowLayout.isShown()) {
+ windowLayout.hide();
+ } else {
+ windowLayout.show();
+ }
+ }
+ }
+
+ private void deleteWindows(int windowBitmap) {
+ if (windowBitmap == 0) {
+ return;
+ }
+ for (CaptionWindowLayout windowLayout : getWindowsFromBitmap(windowBitmap)) {
+ windowLayout.removeFromCaptionView();
+ mCaptionWindowLayouts[windowLayout.getCaptionWindowId()] = null;
+ }
+ }
+
+ public void clear() {
+ mHandler.sendEmptyMessage(MSG_CAPTION_CLEAR);
+ }
+
+ public void reset() {
+ mCurrentWindowLayout = null;
+ mIsDelayed = false;
+ mPendingCaptionEvents.clear();
+ for (int i = 0; i < CAPTION_WINDOWS_MAX; ++i) {
+ if (mCaptionWindowLayouts[i] != null) {
+ mCaptionWindowLayouts[i].removeFromCaptionView();
+ }
+ mCaptionWindowLayouts[i] = null;
+ }
+ mCaptionLayout.setVisibility(View.INVISIBLE);
+ mHandler.removeMessages(MSG_CAPTION_CLEAR);
+ }
+
+ private void setWindowAttr(CaptionWindowAttr windowAttr) {
+ if (mCurrentWindowLayout != null) {
+ mCurrentWindowLayout.setWindowAttr(windowAttr);
+ }
+ }
+
+ private void defineWindow(CaptionWindow window) {
+ if (window == null) {
+ return;
+ }
+ int windowId = window.id;
+ if (windowId < 0 || windowId >= mCaptionWindowLayouts.length) {
+ return;
+ }
+ CaptionWindowLayout windowLayout = mCaptionWindowLayouts[windowId];
+ if (windowLayout == null) {
+ windowLayout = new CaptionWindowLayout(mCaptionLayout.getContext());
+ }
+ windowLayout.initWindow(mCaptionLayout, window);
+ mCurrentWindowLayout = mCaptionWindowLayouts[windowId] = windowLayout;
+ }
+
+ // The job related caption commands
+ private void delay(int tenthsOfSeconds) {
+ if (tenthsOfSeconds < 0 || tenthsOfSeconds > 255) {
+ return;
+ }
+ mIsDelayed = true;
+ mHandler.sendMessageDelayed(
+ mHandler.obtainMessage(MSG_DELAY_CANCEL), tenthsOfSeconds * DELAY_IN_MILLIS);
+ }
+
+ private void delayCancel() {
+ mIsDelayed = false;
+ processPendingBuffer();
+ }
+
+ private void processPendingBuffer() {
+ for (CaptionEvent event : mPendingCaptionEvents) {
+ processCaptionEvent(event);
+ }
+ mPendingCaptionEvents.clear();
+ }
+
+ // The implicit write caption commands
+ private void sendControlToCurrentWindow(char control) {
+ if (mCurrentWindowLayout != null) {
+ mCurrentWindowLayout.sendControl(control);
+ }
+ }
+
+ private void sendBufferToCurrentWindow(String buffer) {
+ if (mCurrentWindowLayout != null) {
+ mCurrentWindowLayout.sendBuffer(buffer);
+ mHandler.removeMessages(MSG_CAPTION_CLEAR);
+ mHandler.sendMessageDelayed(
+ mHandler.obtainMessage(MSG_CAPTION_CLEAR), CAPTION_CLEAR_INTERVAL_MS);
+ }
+ }
+
+ // The pen related caption commands
+ private void setPenAttr(CaptionPenAttr attr) {
+ if (mCurrentWindowLayout != null) {
+ mCurrentWindowLayout.setPenAttr(attr);
+ }
+ }
+
+ private void setPenColor(CaptionPenColor color) {
+ if (mCurrentWindowLayout != null) {
+ mCurrentWindowLayout.setPenColor(color);
+ }
+ }
+
+ private void setPenLocation(CaptionPenLocation location) {
+ if (mCurrentWindowLayout != null) {
+ mCurrentWindowLayout.setPenLocation(location.row, location.column);
+ }
+ }
+}
diff --git a/src/com/android/tv/tuner/cc/CaptionWindowLayout.java b/src/com/android/tv/tuner/cc/CaptionWindowLayout.java
new file mode 100644
index 00000000..e9371f94
--- /dev/null
+++ b/src/com/android/tv/tuner/cc/CaptionWindowLayout.java
@@ -0,0 +1,680 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.cc;
+
+import android.content.Context;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.Typeface;
+import android.text.Layout.Alignment;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.TextUtils;
+import android.text.style.CharacterStyle;
+import android.text.style.RelativeSizeSpan;
+import android.text.style.StyleSpan;
+import android.text.style.SubscriptSpan;
+import android.text.style.SuperscriptSpan;
+import android.text.style.UnderlineSpan;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.accessibility.CaptioningManager;
+import android.view.accessibility.CaptioningManager.CaptionStyle;
+import android.view.accessibility.CaptioningManager.CaptioningChangeListener;
+import android.widget.RelativeLayout;
+import com.android.tv.tuner.data.Cea708Data.CaptionPenAttr;
+import com.android.tv.tuner.data.Cea708Data.CaptionPenColor;
+import com.android.tv.tuner.data.Cea708Data.CaptionWindow;
+import com.android.tv.tuner.data.Cea708Data.CaptionWindowAttr;
+import com.android.tv.tuner.layout.ScaledLayout;
+import com.google.android.exoplayer.text.CaptionStyleCompat;
+import com.google.android.exoplayer.text.SubtitleView;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Layout which renders a caption window of CEA-708B. It contains a {@link SubtitleView} that takes
+ * care of displaying the actual cc text.
+ */
+public class CaptionWindowLayout extends RelativeLayout implements View.OnLayoutChangeListener {
+ private static final String TAG = "CaptionWindowLayout";
+ private static final boolean DEBUG = false;
+
+ private static final float PROPORTION_PEN_SIZE_SMALL = .75f;
+ private static final float PROPORTION_PEN_SIZE_LARGE = 1.25f;
+
+ // The following values indicates the maximum cell number of a window.
+ private static final int ANCHOR_RELATIVE_POSITIONING_MAX = 99;
+ private static final int ANCHOR_VERTICAL_MAX = 74;
+ private static final int ANCHOR_HORIZONTAL_4_3_MAX = 159;
+ private static final int ANCHOR_HORIZONTAL_16_9_MAX = 209;
+
+ // The following values indicates a gravity of a window.
+ private static final int ANCHOR_MODE_DIVIDER = 3;
+ private static final int ANCHOR_HORIZONTAL_MODE_LEFT = 0;
+ private static final int ANCHOR_HORIZONTAL_MODE_CENTER = 1;
+ private static final int ANCHOR_HORIZONTAL_MODE_RIGHT = 2;
+ private static final int ANCHOR_VERTICAL_MODE_TOP = 0;
+ private static final int ANCHOR_VERTICAL_MODE_CENTER = 1;
+ private static final int ANCHOR_VERTICAL_MODE_BOTTOM = 2;
+
+ private static final int US_MAX_COLUMN_COUNT_16_9 = 42;
+ private static final int US_MAX_COLUMN_COUNT_4_3 = 32;
+ private static final int KR_MAX_COLUMN_COUNT_16_9 = 52;
+ private static final int KR_MAX_COLUMN_COUNT_4_3 = 40;
+ private static final int MAX_ROW_COUNT = 15;
+
+ private static final String KOR_ALPHABET =
+ new String("\uAC00".getBytes(StandardCharsets.UTF_8), StandardCharsets.UTF_8);
+ private static final float WIDE_SCREEN_ASPECT_RATIO_THRESHOLD = 1.6f;
+
+ private CaptionLayout mCaptionLayout;
+ private CaptionStyleCompat mCaptionStyleCompat;
+
+ // TODO: Replace SubtitleView to {@link com.google.android.exoplayer.text.SubtitleLayout}.
+ private final SubtitleView mSubtitleView;
+ private int mRowLimit = 0;
+ private final SpannableStringBuilder mBuilder = new SpannableStringBuilder();
+ private final List<CharacterStyle> mCharacterStyles = new ArrayList<>();
+ private int mCaptionWindowId;
+ private int mCurrentTextRow = -1;
+ private float mFontScale;
+ private float mTextSize;
+ private String mWidestChar;
+ private int mLastCaptionLayoutWidth;
+ private int mLastCaptionLayoutHeight;
+ private int mWindowJustify;
+ private int mPrintDirection;
+
+ private class SystemWideCaptioningChangeListener extends CaptioningChangeListener {
+ @Override
+ public void onUserStyleChanged(CaptionStyle userStyle) {
+ mCaptionStyleCompat = CaptionStyleCompat.createFromCaptionStyle(userStyle);
+ mSubtitleView.setStyle(mCaptionStyleCompat);
+ updateWidestChar();
+ }
+
+ @Override
+ public void onFontScaleChanged(float fontScale) {
+ mFontScale = fontScale;
+ updateTextSize();
+ }
+ }
+
+ public CaptionWindowLayout(Context context) {
+ this(context, null);
+ }
+
+ public CaptionWindowLayout(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public CaptionWindowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+
+ // Add a subtitle view to the layout.
+ mSubtitleView = new SubtitleView(context);
+ LayoutParams params =
+ new RelativeLayout.LayoutParams(
+ ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
+ addView(mSubtitleView, params);
+
+ // Set the system wide cc preferences to the subtitle view.
+ CaptioningManager captioningManager =
+ (CaptioningManager) context.getSystemService(Context.CAPTIONING_SERVICE);
+ mFontScale = captioningManager.getFontScale();
+ mCaptionStyleCompat =
+ CaptionStyleCompat.createFromCaptionStyle(captioningManager.getUserStyle());
+ mSubtitleView.setStyle(mCaptionStyleCompat);
+ mSubtitleView.setText("");
+ captioningManager.addCaptioningChangeListener(new SystemWideCaptioningChangeListener());
+ updateWidestChar();
+ }
+
+ public int getCaptionWindowId() {
+ return mCaptionWindowId;
+ }
+
+ public void setCaptionWindowId(int captionWindowId) {
+ mCaptionWindowId = captionWindowId;
+ }
+
+ public void clear() {
+ clearText();
+ hide();
+ }
+
+ public void show() {
+ setVisibility(View.VISIBLE);
+ requestLayout();
+ }
+
+ public void hide() {
+ setVisibility(View.INVISIBLE);
+ requestLayout();
+ }
+
+ public void setPenAttr(CaptionPenAttr penAttr) {
+ mCharacterStyles.clear();
+ if (penAttr.italic) {
+ mCharacterStyles.add(new StyleSpan(Typeface.ITALIC));
+ }
+ if (penAttr.underline) {
+ mCharacterStyles.add(new UnderlineSpan());
+ }
+ switch (penAttr.penSize) {
+ case CaptionPenAttr.PEN_SIZE_SMALL:
+ mCharacterStyles.add(new RelativeSizeSpan(PROPORTION_PEN_SIZE_SMALL));
+ break;
+ case CaptionPenAttr.PEN_SIZE_LARGE:
+ mCharacterStyles.add(new RelativeSizeSpan(PROPORTION_PEN_SIZE_LARGE));
+ break;
+ }
+ switch (penAttr.penOffset) {
+ case CaptionPenAttr.OFFSET_SUBSCRIPT:
+ mCharacterStyles.add(new SubscriptSpan());
+ break;
+ case CaptionPenAttr.OFFSET_SUPERSCRIPT:
+ mCharacterStyles.add(new SuperscriptSpan());
+ break;
+ }
+ }
+
+ public void setPenColor(CaptionPenColor penColor) {
+ // TODO: apply pen colors or skip this and use the style of system wide cc style as is.
+ }
+
+ public void setPenLocation(int row, int column) {
+ // TODO: change the location of pen when window's justify isn't left.
+ // According to the CEA708B spec 8.7, setPenLocation means set the pen cursor within
+ // window's text buffer. When row > mCurrentTextRow, we add "\n" to make the cursor locate
+ // at row. Adding white space to make cursor locate at column.
+ if (mWindowJustify == CaptionWindowAttr.JUSTIFY_LEFT) {
+ if (mCurrentTextRow >= 0) {
+ for (int r = mCurrentTextRow; r < row; ++r) {
+ appendText("\n");
+ }
+ if (mCurrentTextRow <= row) {
+ for (int i = 0; i < column; ++i) {
+ appendText(" ");
+ }
+ }
+ }
+ }
+ mCurrentTextRow = row;
+ }
+
+ public void setWindowAttr(CaptionWindowAttr windowAttr) {
+ // TODO: apply window attrs or skip this and use the style of system wide cc style as is.
+ mWindowJustify = windowAttr.justify;
+ mPrintDirection = windowAttr.printDirection;
+ }
+
+ public void sendBuffer(String buffer) {
+ appendText(buffer);
+ }
+
+ public void sendControl(char control) {
+ // TODO: there are a bunch of ASCII-style control codes.
+ }
+
+ /**
+ * This method places the window on a given CaptionLayout along with the anchor of the window.
+ *
+ * <p>According to CEA-708B, the anchor id indicates the gravity of the window as the follows.
+ * For example, A value 7 of a anchor id says that a window is align with its parent bottom and
+ * is located at the center horizontally of its parent.
+ *
+ * <h4>Anchor id and the gravity of a window</h4>
+ *
+ * <table>
+ * <tr>
+ * <th>GRAVITY</th>
+ * <th>LEFT</th>
+ * <th>CENTER_HORIZONTAL</th>
+ * <th>RIGHT</th>
+ * </tr>
+ * <tr>
+ * <th>TOP</th>
+ * <td>0</td>
+ * <td>1</td>
+ * <td>2</td>
+ * </tr>
+ * <tr>
+ * <th>CENTER_VERTICAL</th>
+ * <td>3</td>
+ * <td>4</td>
+ * <td>5</td>
+ * </tr>
+ * <tr>
+ * <th>BOTTOM</th>
+ * <td>6</td>
+ * <td>7</td>
+ * <td>8</td>
+ * </tr>
+ * </table>
+ *
+ * <p>In order to handle the gravity of a window, there are two steps. First, set the size of
+ * the window. Since the window will be positioned at {@link ScaledLayout}, the size factors are
+ * determined in a ratio. Second, set the gravity of the window. {@link CaptionWindowLayout} is
+ * inherited from {@link RelativeLayout}. Hence, we could set the gravity of its child view,
+ * {@link SubtitleView}.
+ *
+ * <p>The gravity of the window is also related to its size. When it should be pushed to a one
+ * of the end of the window, like LEFT, RIGHT, TOP or BOTTOM, the anchor point should be a
+ * boundary of the window. When it should be pushed in the horizontal/vertical center of its
+ * container, the horizontal/vertical center point of the window should be the same as the
+ * anchor point.
+ *
+ * @param captionLayout a given {@link CaptionLayout}, which contains a safe title area
+ * @param captionWindow a given {@link CaptionWindow}, which stores the construction info of the
+ * window
+ */
+ public void initWindow(CaptionLayout captionLayout, CaptionWindow captionWindow) {
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "initWindow with "
+ + (captionLayout != null ? captionLayout.getCaptionTrack() : null));
+ }
+ if (mCaptionLayout != captionLayout) {
+ if (mCaptionLayout != null) {
+ mCaptionLayout.removeOnLayoutChangeListener(this);
+ }
+ mCaptionLayout = captionLayout;
+ mCaptionLayout.addOnLayoutChangeListener(this);
+ updateWidestChar();
+ }
+
+ // Both anchor vertical and horizontal indicates the position cell number of the window.
+ float scaleRow =
+ (float) captionWindow.anchorVertical
+ / (captionWindow.relativePositioning
+ ? ANCHOR_RELATIVE_POSITIONING_MAX
+ : ANCHOR_VERTICAL_MAX);
+ float scaleCol =
+ (float) captionWindow.anchorHorizontal
+ / (captionWindow.relativePositioning
+ ? ANCHOR_RELATIVE_POSITIONING_MAX
+ : (isWideAspectRatio()
+ ? ANCHOR_HORIZONTAL_16_9_MAX
+ : ANCHOR_HORIZONTAL_4_3_MAX));
+
+ // The range of scaleRow/Col need to be verified to be in [0, 1].
+ // Otherwise a {@link RuntimeException} will be raised in {@link ScaledLayout}.
+ if (scaleRow < 0 || scaleRow > 1) {
+ Log.i(
+ TAG,
+ "The vertical position of the anchor point should be at the range of 0 and 1"
+ + " but "
+ + scaleRow);
+ scaleRow = Math.max(0, Math.min(scaleRow, 1));
+ }
+ if (scaleCol < 0 || scaleCol > 1) {
+ Log.i(
+ TAG,
+ "The horizontal position of the anchor point should be at the range of 0 and"
+ + " 1 but "
+ + scaleCol);
+ scaleCol = Math.max(0, Math.min(scaleCol, 1));
+ }
+ int gravity = Gravity.CENTER;
+ int horizontalMode = captionWindow.anchorId % ANCHOR_MODE_DIVIDER;
+ int verticalMode = captionWindow.anchorId / ANCHOR_MODE_DIVIDER;
+ float scaleStartRow = 0;
+ float scaleEndRow = 1;
+ float scaleStartCol = 0;
+ float scaleEndCol = 1;
+ switch (horizontalMode) {
+ case ANCHOR_HORIZONTAL_MODE_LEFT:
+ gravity = Gravity.LEFT;
+ mSubtitleView.setTextAlignment(Alignment.ALIGN_NORMAL);
+ scaleStartCol = scaleCol;
+ break;
+ case ANCHOR_HORIZONTAL_MODE_CENTER:
+ float gap = Math.min(1 - scaleCol, scaleCol);
+
+ // Since all TV sets use left text alignment instead of center text alignment
+ // for this case, we follow the industry convention if possible.
+ int columnCount = captionWindow.columnCount + 1;
+ if (isKoreanLanguageTrack()) {
+ columnCount /= 2;
+ }
+ columnCount = Math.min(getScreenColumnCount(), columnCount);
+ StringBuilder widestTextBuilder = new StringBuilder();
+ for (int i = 0; i < columnCount; ++i) {
+ widestTextBuilder.append(mWidestChar);
+ }
+ Paint paint = new Paint();
+ paint.setTypeface(mCaptionStyleCompat.typeface);
+ paint.setTextSize(mTextSize);
+ float maxWindowWidth = paint.measureText(widestTextBuilder.toString());
+ float halfMaxWidthScale =
+ mCaptionLayout.getWidth() > 0
+ ? maxWindowWidth / 2.0f / (mCaptionLayout.getWidth() * 0.8f)
+ : 0.0f;
+ if (halfMaxWidthScale > 0f && halfMaxWidthScale < scaleCol) {
+ // Calculate the expected max window size based on the column count of the
+ // caption window multiplied by average alphabets char width, then align the
+ // left side of the window with the left side of the expected max window.
+ gravity = Gravity.LEFT;
+ mSubtitleView.setTextAlignment(Alignment.ALIGN_NORMAL);
+ scaleStartCol = scaleCol - halfMaxWidthScale;
+ scaleEndCol = 1.0f;
+ } else {
+ // The gap will be the minimum distance value of the distances from both
+ // horizontal end points to the anchor point.
+ // If scaleCol <= 0.5, the range of scaleCol is [0, the anchor point * 2].
+ // If scaleCol > 0.5, the range of scaleCol is [(1 - the anchor point) * 2, 1].
+ // The anchor point is located at the horizontal center of the window in both
+ // cases.
+ gravity = Gravity.CENTER_HORIZONTAL;
+ mSubtitleView.setTextAlignment(Alignment.ALIGN_CENTER);
+ scaleStartCol = scaleCol - gap;
+ scaleEndCol = scaleCol + gap;
+ }
+ break;
+ case ANCHOR_HORIZONTAL_MODE_RIGHT:
+ gravity = Gravity.RIGHT;
+ mSubtitleView.setTextAlignment(Alignment.ALIGN_OPPOSITE);
+ scaleEndCol = scaleCol;
+ break;
+ }
+ switch (verticalMode) {
+ case ANCHOR_VERTICAL_MODE_TOP:
+ gravity |= Gravity.TOP;
+ scaleStartRow = scaleRow;
+ break;
+ case ANCHOR_VERTICAL_MODE_CENTER:
+ gravity |= Gravity.CENTER_VERTICAL;
+
+ // See the above comment.
+ float gap = Math.min(1 - scaleRow, scaleRow);
+ scaleStartRow = scaleRow - gap;
+ scaleEndRow = scaleRow + gap;
+ break;
+ case ANCHOR_VERTICAL_MODE_BOTTOM:
+ gravity |= Gravity.BOTTOM;
+ scaleEndRow = scaleRow;
+ break;
+ }
+ mCaptionLayout.addOrUpdateViewToSafeTitleArea(
+ this,
+ new ScaledLayout.ScaledLayoutParams(
+ scaleStartRow, scaleEndRow, scaleStartCol, scaleEndCol));
+ setCaptionWindowId(captionWindow.id);
+ setRowLimit(captionWindow.rowCount);
+ setGravity(gravity);
+ setWindowStyle(captionWindow.windowStyle);
+ if (mWindowJustify == CaptionWindowAttr.JUSTIFY_CENTER) {
+ mSubtitleView.setTextAlignment(Alignment.ALIGN_CENTER);
+ }
+ if (captionWindow.visible) {
+ show();
+ } else {
+ hide();
+ }
+ }
+
+ @Override
+ public void onLayoutChange(
+ View v,
+ int left,
+ int top,
+ int right,
+ int bottom,
+ int oldLeft,
+ int oldTop,
+ int oldRight,
+ int oldBottom) {
+ int width = right - left;
+ int height = bottom - top;
+ if (width != mLastCaptionLayoutWidth || height != mLastCaptionLayoutHeight) {
+ mLastCaptionLayoutWidth = width;
+ mLastCaptionLayoutHeight = height;
+ updateTextSize();
+ }
+ }
+
+ private boolean isKoreanLanguageTrack() {
+ return mCaptionLayout != null
+ && mCaptionLayout.getCaptionTrack() != null
+ && mCaptionLayout.getCaptionTrack().language != null
+ && "KOR".compareToIgnoreCase(mCaptionLayout.getCaptionTrack().language) == 0;
+ }
+
+ private boolean isWideAspectRatio() {
+ return mCaptionLayout != null
+ && mCaptionLayout.getCaptionTrack() != null
+ && mCaptionLayout.getCaptionTrack().wideAspectRatio;
+ }
+
+ private void updateWidestChar() {
+ if (isKoreanLanguageTrack()) {
+ mWidestChar = KOR_ALPHABET;
+ } else {
+ Paint paint = new Paint();
+ paint.setTypeface(mCaptionStyleCompat.typeface);
+ Charset latin1 = Charset.forName("ISO-8859-1");
+ float widestCharWidth = 0f;
+ for (int i = 0; i < 256; ++i) {
+ String ch = new String(new byte[] {(byte) i}, latin1);
+ float charWidth = paint.measureText(ch);
+ if (widestCharWidth < charWidth) {
+ widestCharWidth = charWidth;
+ mWidestChar = ch;
+ }
+ }
+ }
+ updateTextSize();
+ }
+
+ private void updateTextSize() {
+ if (mCaptionLayout == null) return;
+
+ // Calculate text size based on the max window size.
+ StringBuilder widestTextBuilder = new StringBuilder();
+ int screenColumnCount = getScreenColumnCount();
+ for (int i = 0; i < screenColumnCount; ++i) {
+ widestTextBuilder.append(mWidestChar);
+ }
+ String widestText = widestTextBuilder.toString();
+ Paint paint = new Paint();
+ paint.setTypeface(mCaptionStyleCompat.typeface);
+ float startFontSize = 0f;
+ float endFontSize = 255f;
+ Rect boundRect = new Rect();
+ while (startFontSize < endFontSize) {
+ float testTextSize = (startFontSize + endFontSize) / 2f;
+ paint.setTextSize(testTextSize);
+ float width = paint.measureText(widestText);
+ paint.getTextBounds(widestText, 0, widestText.length(), boundRect);
+ float height = boundRect.height() + width - boundRect.width();
+ // According to CEA-708B Section 9.13, the height of standard font size shouldn't taller
+ // than 1/15 of the height of the safe-title area, and the width shouldn't wider than
+ // 1/{@code getScreenColumnCount()} of the width of the safe-title area.
+ if (mCaptionLayout.getWidth() * 0.8f > width
+ && mCaptionLayout.getHeight() * 0.8f / MAX_ROW_COUNT > height) {
+ startFontSize = testTextSize + 0.01f;
+ } else {
+ endFontSize = testTextSize - 0.01f;
+ }
+ }
+ mTextSize = endFontSize * mFontScale;
+ paint.setTextSize(mTextSize);
+ float whiteSpaceWidth = paint.measureText(" ");
+ mSubtitleView.setWhiteSpaceWidth(whiteSpaceWidth);
+ mSubtitleView.setTextSize(mTextSize);
+ }
+
+ private int getScreenColumnCount() {
+ float screenAspectRatio = (float) mCaptionLayout.getWidth() / mCaptionLayout.getHeight();
+ boolean isWideAspectRationScreen = screenAspectRatio > WIDE_SCREEN_ASPECT_RATIO_THRESHOLD;
+ if (isKoreanLanguageTrack()) {
+ // Each korean character consumes two slots.
+ if (isWideAspectRationScreen || isWideAspectRatio()) {
+ return KR_MAX_COLUMN_COUNT_16_9 / 2;
+ } else {
+ return KR_MAX_COLUMN_COUNT_4_3 / 2;
+ }
+ } else {
+ if (isWideAspectRationScreen || isWideAspectRatio()) {
+ return US_MAX_COLUMN_COUNT_16_9;
+ } else {
+ return US_MAX_COLUMN_COUNT_4_3;
+ }
+ }
+ }
+
+ public void removeFromCaptionView() {
+ if (mCaptionLayout != null) {
+ mCaptionLayout.removeViewFromSafeTitleArea(this);
+ mCaptionLayout.removeOnLayoutChangeListener(this);
+ mCaptionLayout = null;
+ }
+ }
+
+ public void setText(String text) {
+ updateText(text, false);
+ }
+
+ public void appendText(String text) {
+ updateText(text, true);
+ }
+
+ public void clearText() {
+ mBuilder.clear();
+ mSubtitleView.setText("");
+ }
+
+ private void updateText(String text, boolean appended) {
+ if (!appended) {
+ mBuilder.clear();
+ }
+ if (text != null && text.length() > 0) {
+ int length = mBuilder.length();
+ mBuilder.append(text);
+ for (CharacterStyle characterStyle : mCharacterStyles) {
+ mBuilder.setSpan(
+ characterStyle,
+ length,
+ mBuilder.length(),
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ }
+ String[] lines = TextUtils.split(mBuilder.toString(), "\n");
+
+ // Truncate text not to exceed the row limit.
+ // Plus one here since the range of the rows is [0, mRowLimit].
+ int startRow = Math.max(0, lines.length - (mRowLimit + 1));
+ String truncatedText =
+ TextUtils.join("\n", Arrays.copyOfRange(lines, startRow, lines.length));
+ mBuilder.delete(0, mBuilder.length() - truncatedText.length());
+ mCurrentTextRow = lines.length - startRow - 1;
+
+ // Trim the buffer first then set text to {@link SubtitleView}.
+ int start = 0, last = mBuilder.length() - 1;
+ int end = last;
+ while ((start <= end) && (mBuilder.charAt(start) <= ' ')) {
+ ++start;
+ }
+ while (start - 1 >= 0 && start <= end && mBuilder.charAt(start - 1) != '\n') {
+ --start;
+ }
+ while ((end >= start) && (mBuilder.charAt(end) <= ' ')) {
+ --end;
+ }
+ if (start == 0 && end == last) {
+ mSubtitleView.setPrefixSpaces(getPrefixSpaces(mBuilder));
+ mSubtitleView.setText(mBuilder);
+ } else {
+ SpannableStringBuilder trim = new SpannableStringBuilder();
+ trim.append(mBuilder);
+ if (end < last) {
+ trim.delete(end + 1, last + 1);
+ }
+ if (start > 0) {
+ trim.delete(0, start);
+ }
+ mSubtitleView.setPrefixSpaces(getPrefixSpaces(trim));
+ mSubtitleView.setText(trim);
+ }
+ }
+
+ private static ArrayList<Integer> getPrefixSpaces(SpannableStringBuilder builder) {
+ ArrayList<Integer> prefixSpaces = new ArrayList<>();
+ String[] lines = TextUtils.split(builder.toString(), "\n");
+ for (String line : lines) {
+ int start = 0;
+ while (start < line.length() && line.charAt(start) <= ' ') {
+ start++;
+ }
+ prefixSpaces.add(start);
+ }
+ return prefixSpaces;
+ }
+
+ public void setRowLimit(int rowLimit) {
+ if (rowLimit < 0) {
+ throw new IllegalArgumentException("A rowLimit should have a positive number");
+ }
+ mRowLimit = rowLimit;
+ }
+
+ private void setWindowStyle(int windowStyle) {
+ // TODO: Set other attributes of window style. Like fill opacity and fill color.
+ switch (windowStyle) {
+ case 2:
+ mWindowJustify = CaptionWindowAttr.JUSTIFY_LEFT;
+ mPrintDirection = CaptionWindowAttr.PRINT_LEFT_TO_RIGHT;
+ break;
+ case 3:
+ mWindowJustify = CaptionWindowAttr.JUSTIFY_CENTER;
+ mPrintDirection = CaptionWindowAttr.PRINT_LEFT_TO_RIGHT;
+ break;
+ case 4:
+ mWindowJustify = CaptionWindowAttr.JUSTIFY_LEFT;
+ mPrintDirection = CaptionWindowAttr.PRINT_LEFT_TO_RIGHT;
+ break;
+ case 5:
+ mWindowJustify = CaptionWindowAttr.JUSTIFY_LEFT;
+ mPrintDirection = CaptionWindowAttr.PRINT_LEFT_TO_RIGHT;
+ break;
+ case 6:
+ mWindowJustify = CaptionWindowAttr.JUSTIFY_CENTER;
+ mPrintDirection = CaptionWindowAttr.PRINT_LEFT_TO_RIGHT;
+ break;
+ case 7:
+ mWindowJustify = CaptionWindowAttr.JUSTIFY_LEFT;
+ mPrintDirection = CaptionWindowAttr.PRINT_TOP_TO_BOTTOM;
+ break;
+ default:
+ if (windowStyle != 0 && windowStyle != 1) {
+ Log.e(TAG, "Error predefined window style:" + windowStyle);
+ }
+ mWindowJustify = CaptionWindowAttr.JUSTIFY_LEFT;
+ mPrintDirection = CaptionWindowAttr.PRINT_LEFT_TO_RIGHT;
+ break;
+ }
+ }
+}
diff --git a/src/com/android/tv/tuner/cc/Cea708Parser.java b/src/com/android/tv/tuner/cc/Cea708Parser.java
new file mode 100644
index 00000000..4e080276
--- /dev/null
+++ b/src/com/android/tv/tuner/cc/Cea708Parser.java
@@ -0,0 +1,922 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.cc;
+
+import android.os.SystemClock;
+import android.support.annotation.IntDef;
+import android.util.Log;
+import android.util.SparseIntArray;
+import com.android.tv.tuner.data.Cea708Data;
+import com.android.tv.tuner.data.Cea708Data.CaptionColor;
+import com.android.tv.tuner.data.Cea708Data.CaptionEvent;
+import com.android.tv.tuner.data.Cea708Data.CaptionPenAttr;
+import com.android.tv.tuner.data.Cea708Data.CaptionPenColor;
+import com.android.tv.tuner.data.Cea708Data.CaptionPenLocation;
+import com.android.tv.tuner.data.Cea708Data.CaptionWindow;
+import com.android.tv.tuner.data.Cea708Data.CaptionWindowAttr;
+import com.android.tv.tuner.data.Cea708Data.CcPacket;
+import com.android.tv.tuner.util.ByteArrayBuffer;
+import java.io.UnsupportedEncodingException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.TreeSet;
+
+/**
+ * A class for parsing CEA-708, which is the standard for closed captioning for ATSC DTV.
+ *
+ * <p>ATSC DTV closed caption data are carried on picture user data of video streams. This class
+ * starts to parse from picture user data payload, so extraction process of user_data from video
+ * streams is up to outside of this code.
+ *
+ * <p>There are 4 steps to decode user_data to provide closed caption services.
+ *
+ * <h3>Step 1. user_data -&gt; CcPacket ({@link #parseClosedCaption} method)</h3>
+ *
+ * <p>First, user_data consists of cc_data packets, which are 3-byte segments. Here, CcPacket is a
+ * collection of cc_data packets in a frame along with same presentation timestamp. Because cc_data
+ * packets must be reassembled in the frame display order, CcPackets are reordered.
+ *
+ * <h3>Step 2. CcPacket -&gt; DTVCC packet ({@link #parseCcPacket} method)</h3>
+ *
+ * <p>Each cc_data packet has a one byte for declaring a type of itself and data validity, and the
+ * subsequent two bytes for input data of a DTVCC packet. There are 4 types for cc_data packet.
+ * We're interested in DTVCC_PACKET_START(type 3) and DTVCC_PACKET_DATA(type 2). Each DTVCC packet
+ * begins with DTVCC_PACKET_START(type 3) and the following cc_data packets which has
+ * DTVCC_PACKET_DATA(type 2) are appended into the DTVCC packet being assembled.
+ *
+ * <h3>Step 3. DTVCC packet -&gt; Service Blocks ({@link #parseDtvCcPacket} method)</h3>
+ *
+ * <p>A DTVCC packet consists of multiple service blocks. Each service block represents a caption
+ * track and has a service number, which ranges from 1 to 63, that denotes caption track identity.
+ * In here, we listen at most one chosen caption track by {@link #mListenServiceNumber}. Otherwise,
+ * just skip the other service blocks.
+ *
+ * <h3>Step 4. Interpreting Service Block Data ({@link #parseServiceBlockData}, {@code parseXX}, and
+ * {@link #parseExt1} methods)</h3>
+ *
+ * <p>Service block data is actual caption stream. it looks similar to telnet. It uses most parts of
+ * ASCII table and consists of specially defined commands and some ASCII control codes which work in
+ * a behavior slightly different from their original purpose. ASCII control codes and caption
+ * commands are explicit instructions that control the state of a closed caption service and the
+ * other ASCII and text codes are implicit instructions that send their characters to buffer.
+ *
+ * <p>There are 4 main code groups and 4 extended code groups. Both the range of code groups are the
+ * same as the range of a byte.
+ *
+ * <p>4 main code groups: C0, C1, G0, G1 <br>
+ * 4 extended code groups: C2, C3, G2, G3
+ *
+ * <p>Each code group has its own handle method. For example, {@link #parseC0} handles C0 code group
+ * and so on. And {@link #parseServiceBlockData} method maps a stream on the main code groups while
+ * {@link #parseExt1} method maps on the extended code groups.
+ *
+ * <p>The main code groups:
+ *
+ * <ul>
+ * <li>C0 - contains modified ASCII control codes. It is not intended by CEA-708 but Korea TTA
+ * standard for ATSC CC uses P16 character heavily, which is unclear entity in CEA-708 doc,
+ * even for the alphanumeric characters instead of ASCII characters.
+ * <li>C1 - contains the caption commands. There are 3 categories of a caption command.
+ * <ul>
+ * <li>Window commands: The window commands control a caption window which is addressable
+ * area being with in the Safe title area. (CWX, CLW, DSW, HDW, TGW, DLW, SWA, DFX)
+ * <li>Pen commands: Th pen commands control text style and location. (SPA, SPC, SPL)
+ * <li>Job commands: The job commands make a delay and recover from the delay. (DLY, DLC,
+ * RST)
+ * </ul>
+ * <li>G0 - same as printable ASCII character set except music note character.
+ * <li>G1 - same as ISO 8859-1 Latin 1 character set.
+ * </ul>
+ *
+ * <p>Most of the extended code groups are being skipped.
+ */
+public class Cea708Parser {
+ private static final String TAG = "Cea708Parser";
+ private static final boolean DEBUG = false;
+
+ // According to CEA-708B, the maximum value of closed caption bandwidth is 9600bps.
+ private static final int MAX_ALLOCATED_SIZE = 9600 / 8;
+ private static final String MUSIC_NOTE_CHAR =
+ new String("\u266B".getBytes(StandardCharsets.UTF_8), StandardCharsets.UTF_8);
+
+ // The following values are denoting the type of closed caption data.
+ // See CEA-708B section 4.4.1.
+ private static final int CC_TYPE_DTVCC_PACKET_START = 3;
+ private static final int CC_TYPE_DTVCC_PACKET_DATA = 2;
+
+ // The following values are defined in CEA-708B Figure 4 and 6.
+ private static final int DTVCC_MAX_PACKET_SIZE = 64;
+ private static final int DTVCC_PACKET_SIZE_SCALE_FACTOR = 2;
+ private static final int DTVCC_EXTENDED_SERVICE_NUMBER_POINT = 7;
+
+ // The following values are for seeking closed caption tracks.
+ private static final int DISCOVERY_PERIOD_MS = 10000; // 10 sec
+ private static final int DISCOVERY_NUM_BYTES_THRESHOLD = 10; // 10 bytes
+ private static final int DISCOVERY_CC_SERVICE_NUMBER_START = 1; // CC1
+ private static final int DISCOVERY_CC_SERVICE_NUMBER_END = 4; // CC4
+
+ private final ByteArrayBuffer mDtvCcPacket = new ByteArrayBuffer(MAX_ALLOCATED_SIZE);
+ private final TreeSet<CcPacket> mCcPackets = new TreeSet<>();
+ private final StringBuffer mBuffer = new StringBuffer();
+ private final SparseIntArray mDiscoveredNumBytes = new SparseIntArray(); // per service number
+ private long mLastDiscoveryLaunchedMs = SystemClock.elapsedRealtime();
+ 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() {
+ @Override
+ public void emitEvent(CaptionEvent event) {
+ // do nothing
+ }
+
+ @Override
+ public void discoverServiceNumber(int serviceNumber) {
+ // do nothing
+ }
+ };
+
+ /**
+ * {@link Cea708Parser} emits caption event of three different types. {@link
+ * OnCea708ParserListener#emitEvent} is invoked with the parameter {@link CaptionEvent} to pass
+ * all the results to an observer of the decoding process.
+ *
+ * <p>{@link CaptionEvent#type} determines the type of the result and {@link CaptionEvent#obj}
+ * contains the output value of a caption event. The observer must do the casting to the
+ * corresponding type.
+ *
+ * <ul>
+ * <li>{@code CAPTION_EMIT_TYPE_BUFFER}: Passes a caption text buffer to a observer. {@code
+ * obj} must be of {@link String}.
+ * <li>{@code CAPTION_EMIT_TYPE_CONTROL}: Passes a caption character control code to a
+ * observer. {@code obj} must be of {@link Character}.
+ * <li>{@code CAPTION_EMIT_TYPE_CLEAR_COMMAND}: Passes a clear command to a observer. {@code
+ * obj} must be {@code NULL}.
+ * </ul>
+ */
+ @IntDef({
+ CAPTION_EMIT_TYPE_BUFFER,
+ CAPTION_EMIT_TYPE_CONTROL,
+ CAPTION_EMIT_TYPE_COMMAND_CWX,
+ CAPTION_EMIT_TYPE_COMMAND_CLW,
+ CAPTION_EMIT_TYPE_COMMAND_DSW,
+ CAPTION_EMIT_TYPE_COMMAND_HDW,
+ CAPTION_EMIT_TYPE_COMMAND_TGW,
+ CAPTION_EMIT_TYPE_COMMAND_DLW,
+ CAPTION_EMIT_TYPE_COMMAND_DLY,
+ CAPTION_EMIT_TYPE_COMMAND_DLC,
+ CAPTION_EMIT_TYPE_COMMAND_RST,
+ CAPTION_EMIT_TYPE_COMMAND_SPA,
+ CAPTION_EMIT_TYPE_COMMAND_SPC,
+ CAPTION_EMIT_TYPE_COMMAND_SPL,
+ CAPTION_EMIT_TYPE_COMMAND_SWA,
+ CAPTION_EMIT_TYPE_COMMAND_DFX
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface CaptionEmitType {}
+
+ public static final int CAPTION_EMIT_TYPE_BUFFER = 1;
+ public static final int CAPTION_EMIT_TYPE_CONTROL = 2;
+ public static final int CAPTION_EMIT_TYPE_COMMAND_CWX = 3;
+ public static final int CAPTION_EMIT_TYPE_COMMAND_CLW = 4;
+ public static final int CAPTION_EMIT_TYPE_COMMAND_DSW = 5;
+ public static final int CAPTION_EMIT_TYPE_COMMAND_HDW = 6;
+ public static final int CAPTION_EMIT_TYPE_COMMAND_TGW = 7;
+ public static final int CAPTION_EMIT_TYPE_COMMAND_DLW = 8;
+ public static final int CAPTION_EMIT_TYPE_COMMAND_DLY = 9;
+ public static final int CAPTION_EMIT_TYPE_COMMAND_DLC = 10;
+ public static final int CAPTION_EMIT_TYPE_COMMAND_RST = 11;
+ public static final int CAPTION_EMIT_TYPE_COMMAND_SPA = 12;
+ public static final int CAPTION_EMIT_TYPE_COMMAND_SPC = 13;
+ public static final int CAPTION_EMIT_TYPE_COMMAND_SPL = 14;
+ public static final int CAPTION_EMIT_TYPE_COMMAND_SWA = 15;
+ public static final int CAPTION_EMIT_TYPE_COMMAND_DFX = 16;
+
+ public interface OnCea708ParserListener {
+ void emitEvent(CaptionEvent event);
+
+ void discoverServiceNumber(int serviceNumber);
+ }
+
+ public void setListener(OnCea708ParserListener listener) {
+ if (listener != null) {
+ mListener = listener;
+ }
+ }
+
+ public void clear() {
+ mDtvCcPacket.clear();
+ mCcPackets.clear();
+ mBuffer.setLength(0);
+ mDiscoveredNumBytes.clear();
+ mCommand = 0;
+ mDtvCcPacking = false;
+ }
+
+ public void setListenServiceNumber(int serviceNumber) {
+ mListenServiceNumber = serviceNumber;
+ }
+
+ private void emitCaptionEvent(CaptionEvent captionEvent) {
+ // Emit the existing string buffer before a new event is arrived.
+ emitCaptionBuffer();
+ mListener.emitEvent(captionEvent);
+ }
+
+ private void emitCaptionBuffer() {
+ if (mBuffer.length() > 0) {
+ mListener.emitEvent(new CaptionEvent(CAPTION_EMIT_TYPE_BUFFER, mBuffer.toString()));
+ mBuffer.setLength(0);
+ }
+ }
+
+ // Step 1. user_data -> CcPacket ({@link #parseClosedCaption} method)
+ public void parseClosedCaption(ByteBuffer data, long framePtsUs) {
+ int ccCount = data.limit() / 3;
+ byte[] ccBytes = new byte[3 * ccCount];
+ for (int i = 0; i < 3 * ccCount; i++) {
+ ccBytes[i] = data.get(i);
+ }
+ CcPacket ccPacket = new CcPacket(ccBytes, ccCount, framePtsUs);
+ mCcPackets.add(ccPacket);
+ }
+
+ public boolean processClosedCaptions(long framePtsUs) {
+ // To get the sorted cc packets that have lower frame pts than current frame pts,
+ // the following offset divides off the lower side of the packets.
+ CcPacket offsetPacket = new CcPacket(new byte[0], 0, framePtsUs);
+ offsetPacket = mCcPackets.lower(offsetPacket);
+ boolean processed = false;
+ if (offsetPacket != null) {
+ while (!mCcPackets.isEmpty() && offsetPacket.compareTo(mCcPackets.first()) >= 0) {
+ CcPacket packet = mCcPackets.pollFirst();
+ parseCcPacket(packet);
+ processed = true;
+ }
+ }
+ return processed;
+ }
+
+ // Step 2. CcPacket -> DTVCC packet ({@link #parseCcPacket} method)
+ private void parseCcPacket(CcPacket ccPacket) {
+ // For the details of cc packet, see ATSC TSG-676 - Table A8.
+ byte[] bytes = ccPacket.bytes;
+ int pos = 0;
+ for (int i = 0; i < ccPacket.ccCount; ++i) {
+ boolean ccValid = (bytes[pos] & 0x04) != 0;
+ int ccType = bytes[pos] & 0x03;
+
+ // The dtvcc should be considered complete:
+ // - if either ccValid is set and ccType is 3
+ // - or ccValid is clear and ccType is 2 or 3.
+ if (ccValid) {
+ if (ccType == CC_TYPE_DTVCC_PACKET_START) {
+ if (mDtvCcPacking) {
+ parseDtvCcPacket(mDtvCcPacket.buffer(), mDtvCcPacket.length());
+ mDtvCcPacket.clear();
+ }
+ mDtvCcPacking = true;
+ mDtvCcPacket.append(bytes[pos + 1]);
+ mDtvCcPacket.append(bytes[pos + 2]);
+ } else if (mDtvCcPacking && ccType == CC_TYPE_DTVCC_PACKET_DATA) {
+ mDtvCcPacket.append(bytes[pos + 1]);
+ mDtvCcPacket.append(bytes[pos + 2]);
+ }
+ } else {
+ if ((ccType == CC_TYPE_DTVCC_PACKET_START || ccType == CC_TYPE_DTVCC_PACKET_DATA)
+ && mDtvCcPacking) {
+ mDtvCcPacking = false;
+ parseDtvCcPacket(mDtvCcPacket.buffer(), mDtvCcPacket.length());
+ mDtvCcPacket.clear();
+ }
+ }
+ pos += 3;
+ }
+ }
+
+ // Step 3. DTVCC packet -> Service Blocks ({@link #parseDtvCcPacket} method)
+ private void parseDtvCcPacket(byte[] data, int limit) {
+ // For the details of DTVCC packet, see CEA-708B Figure 4.
+ int pos = 0;
+ int packetSize = data[pos] & 0x3f;
+ if (packetSize == 0) {
+ packetSize = DTVCC_MAX_PACKET_SIZE;
+ }
+ int calculatedPacketSize = packetSize * DTVCC_PACKET_SIZE_SCALE_FACTOR;
+ if (limit != calculatedPacketSize) {
+ return;
+ }
+ ++pos;
+ int len = pos + calculatedPacketSize;
+ while (pos < len) {
+ // For the details of Service Block, see CEA-708B Figure 5 and 6.
+ int serviceNumber = (data[pos] & 0xe0) >> 5;
+ int blockSize = data[pos] & 0x1f;
+ ++pos;
+ if (serviceNumber == DTVCC_EXTENDED_SERVICE_NUMBER_POINT) {
+ serviceNumber = (data[pos] & 0x3f);
+ ++pos;
+
+ // Return if invalid service number
+ if (serviceNumber < DTVCC_EXTENDED_SERVICE_NUMBER_POINT) {
+ return;
+ }
+ }
+ if (pos + blockSize > limit) {
+ return;
+ }
+
+ // Send parsed service number in order to find unveiled closed caption tracks which
+ // are not specified in any ATSC PSIP sections. Since some broadcasts send empty closed
+ // caption tracks, it detects the proper closed caption tracks by counting the number of
+ // bytes sent with the same service number during a discovery period.
+ // The viewer in most TV sets chooses between CC1, CC2, CC3, CC4 to view different
+ // language captions. Therefore, only CC1, CC2, CC3, CC4 are allowed to be reported.
+ if (blockSize > 0
+ && serviceNumber >= DISCOVERY_CC_SERVICE_NUMBER_START
+ && serviceNumber <= DISCOVERY_CC_SERVICE_NUMBER_END) {
+ mDiscoveredNumBytes.put(
+ serviceNumber, blockSize + mDiscoveredNumBytes.get(serviceNumber, 0));
+ }
+ 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();
+ mLastDiscoveryLaunchedMs = SystemClock.elapsedRealtime();
+ }
+
+ // Skip current service block if either there is no block data or the service number
+ // is not same as listening service number.
+ if (blockSize == 0 || serviceNumber != mListenServiceNumber) {
+ pos += blockSize;
+ continue;
+ }
+
+ // From this point, starts to read DTVCC coding layer.
+ // First, identify code groups, which is defined in CEA-708B Section 7.1.
+ int blockLimit = pos + blockSize;
+ while (pos < blockLimit) {
+ pos = parseServiceBlockData(data, pos);
+ }
+
+ // Emit the buffer after reading codes.
+ emitCaptionBuffer();
+ pos = blockLimit;
+ }
+ }
+
+ // Step 4. Main code groups
+ private int parseServiceBlockData(byte[] data, int pos) {
+ // For the details of the ranges of DTVCC code groups, see CEA-708B Table 6.
+ mCommand = data[pos] & 0xff;
+ ++pos;
+ if (mCommand == Cea708Data.CODE_C0_EXT1) {
+ pos = parseExt1(data, pos);
+ } else if (mCommand >= Cea708Data.CODE_C0_RANGE_START
+ && mCommand <= Cea708Data.CODE_C0_RANGE_END) {
+ pos = parseC0(data, pos);
+ } else if (mCommand >= Cea708Data.CODE_C1_RANGE_START
+ && mCommand <= Cea708Data.CODE_C1_RANGE_END) {
+ pos = parseC1(data, pos);
+ } else if (mCommand >= Cea708Data.CODE_G0_RANGE_START
+ && mCommand <= Cea708Data.CODE_G0_RANGE_END) {
+ pos = parseG0(data, pos);
+ } else if (mCommand >= Cea708Data.CODE_G1_RANGE_START
+ && mCommand <= Cea708Data.CODE_G1_RANGE_END) {
+ pos = parseG1(data, pos);
+ }
+ return pos;
+ }
+
+ private int parseC0(byte[] data, int pos) {
+ // For the details of C0 code group, see CEA-708B Section 7.4.1.
+ // CL Group: C0 Subset of ASCII Control codes
+ if (mCommand >= Cea708Data.CODE_C0_SKIP2_RANGE_START
+ && mCommand <= Cea708Data.CODE_C0_SKIP2_RANGE_END) {
+ if (mCommand == Cea708Data.CODE_C0_P16) {
+ // TODO : P16 escapes next two bytes for the large character maps.(no standard rule)
+ // TODO : For korea broadcasting, express whole letters by using this.
+ try {
+ if (data[pos] == 0) {
+ mBuffer.append((char) data[pos + 1]);
+ } else {
+ String value = new String(Arrays.copyOfRange(data, pos, pos + 2), "EUC-KR");
+ mBuffer.append(value);
+ }
+ } catch (UnsupportedEncodingException e) {
+ Log.e(TAG, "P16 Code - Could not find supported encoding", e);
+ }
+ }
+ pos += 2;
+ } else if (mCommand >= Cea708Data.CODE_C0_SKIP1_RANGE_START
+ && mCommand <= Cea708Data.CODE_C0_SKIP1_RANGE_END) {
+ ++pos;
+ } else {
+ // NUL, BS, FF, CR interpreted as they are in ASCII control codes.
+ // HCR moves the pen location to th beginning of the current line and deletes contents.
+ // FF clears the screen and moves the pen location to (0,0).
+ // ETX is the NULL command which is used to flush text to the current window when no
+ // other command is pending.
+ switch (mCommand) {
+ case Cea708Data.CODE_C0_NUL:
+ break;
+ case Cea708Data.CODE_C0_ETX:
+ emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_CONTROL, (char) mCommand));
+ break;
+ case Cea708Data.CODE_C0_BS:
+ emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_CONTROL, (char) mCommand));
+ break;
+ case Cea708Data.CODE_C0_FF:
+ emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_CONTROL, (char) mCommand));
+ break;
+ case Cea708Data.CODE_C0_CR:
+ mBuffer.append('\n');
+ break;
+ case Cea708Data.CODE_C0_HCR:
+ emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_CONTROL, (char) mCommand));
+ break;
+ default:
+ break;
+ }
+ }
+ return pos;
+ }
+
+ private int parseC1(byte[] data, int pos) {
+ // For the details of C1 code group, see CEA-708B Section 8.10.
+ // CR Group: C1 Caption Control Codes
+ switch (mCommand) {
+ case Cea708Data.CODE_C1_CW0:
+ case Cea708Data.CODE_C1_CW1:
+ case Cea708Data.CODE_C1_CW2:
+ case Cea708Data.CODE_C1_CW3:
+ case Cea708Data.CODE_C1_CW4:
+ case Cea708Data.CODE_C1_CW5:
+ case Cea708Data.CODE_C1_CW6:
+ case Cea708Data.CODE_C1_CW7:
+ {
+ // SetCurrentWindow0-7
+ int windowId = mCommand - Cea708Data.CODE_C1_CW0;
+ emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_CWX, windowId));
+ if (DEBUG) {
+ Log.d(TAG, String.format("CaptionCommand CWX windowId: %d", windowId));
+ }
+ break;
+ }
+
+ case Cea708Data.CODE_C1_CLW:
+ {
+ // ClearWindows
+ int windowBitmap = data[pos] & 0xff;
+ ++pos;
+ emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_CLW, windowBitmap));
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ String.format("CaptionCommand CLW windowBitmap: %d", windowBitmap));
+ }
+ break;
+ }
+
+ case Cea708Data.CODE_C1_DSW:
+ {
+ // DisplayWindows
+ int windowBitmap = data[pos] & 0xff;
+ ++pos;
+ emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_DSW, windowBitmap));
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ String.format("CaptionCommand DSW windowBitmap: %d", windowBitmap));
+ }
+ break;
+ }
+
+ case Cea708Data.CODE_C1_HDW:
+ {
+ // HideWindows
+ int windowBitmap = data[pos] & 0xff;
+ ++pos;
+ emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_HDW, windowBitmap));
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ String.format("CaptionCommand HDW windowBitmap: %d", windowBitmap));
+ }
+ break;
+ }
+
+ case Cea708Data.CODE_C1_TGW:
+ {
+ // ToggleWindows
+ int windowBitmap = data[pos] & 0xff;
+ ++pos;
+ emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_TGW, windowBitmap));
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ String.format("CaptionCommand TGW windowBitmap: %d", windowBitmap));
+ }
+ break;
+ }
+
+ case Cea708Data.CODE_C1_DLW:
+ {
+ // DeleteWindows
+ int windowBitmap = data[pos] & 0xff;
+ ++pos;
+ emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_DLW, windowBitmap));
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ String.format("CaptionCommand DLW windowBitmap: %d", windowBitmap));
+ }
+ break;
+ }
+
+ case Cea708Data.CODE_C1_DLY:
+ {
+ // Delay
+ int tenthsOfSeconds = data[pos] & 0xff;
+ ++pos;
+ emitCaptionEvent(
+ new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_DLY, tenthsOfSeconds));
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ String.format(
+ "CaptionCommand DLY %d tenths of seconds",
+ tenthsOfSeconds));
+ }
+ break;
+ }
+ case Cea708Data.CODE_C1_DLC:
+ {
+ // DelayCancel
+ emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_DLC, null));
+ if (DEBUG) {
+ Log.d(TAG, "CaptionCommand DLC");
+ }
+ break;
+ }
+
+ case Cea708Data.CODE_C1_RST:
+ {
+ // Reset
+ emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_RST, null));
+ if (DEBUG) {
+ Log.d(TAG, "CaptionCommand RST");
+ }
+ break;
+ }
+
+ case Cea708Data.CODE_C1_SPA:
+ {
+ // SetPenAttributes
+ int textTag = (data[pos] & 0xf0) >> 4;
+ int penSize = data[pos] & 0x03;
+ int penOffset = (data[pos] & 0x0c) >> 2;
+ boolean italic = (data[pos + 1] & 0x80) != 0;
+ boolean underline = (data[pos + 1] & 0x40) != 0;
+ int edgeType = (data[pos + 1] & 0x38) >> 3;
+ int fontTag = data[pos + 1] & 0x7;
+ pos += 2;
+ emitCaptionEvent(
+ new CaptionEvent(
+ CAPTION_EMIT_TYPE_COMMAND_SPA,
+ new CaptionPenAttr(
+ penSize, penOffset, textTag, fontTag, edgeType,
+ underline, italic)));
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ String.format(
+ "CaptionCommand SPA penSize: %d, penOffset: %d, textTag: %d, "
+ + "fontTag: %d, edgeType: %d, underline: %s, italic: %s",
+ penSize, penOffset, textTag, fontTag, edgeType, underline,
+ italic));
+ }
+ break;
+ }
+
+ case Cea708Data.CODE_C1_SPC:
+ {
+ // SetPenColor
+ int opacity = (data[pos] & 0xc0) >> 6;
+ int red = (data[pos] & 0x30) >> 4;
+ int green = (data[pos] & 0x0c) >> 2;
+ int blue = data[pos] & 0x03;
+ CaptionColor foregroundColor = new CaptionColor(opacity, red, green, blue);
+ ++pos;
+ opacity = (data[pos] & 0xc0) >> 6;
+ red = (data[pos] & 0x30) >> 4;
+ green = (data[pos] & 0x0c) >> 2;
+ blue = data[pos] & 0x03;
+ CaptionColor backgroundColor = new CaptionColor(opacity, red, green, blue);
+ ++pos;
+ red = (data[pos] & 0x30) >> 4;
+ green = (data[pos] & 0x0c) >> 2;
+ blue = data[pos] & 0x03;
+ CaptionColor edgeColor =
+ new CaptionColor(CaptionColor.OPACITY_SOLID, red, green, blue);
+ ++pos;
+ emitCaptionEvent(
+ new CaptionEvent(
+ CAPTION_EMIT_TYPE_COMMAND_SPC,
+ new CaptionPenColor(
+ foregroundColor, backgroundColor, edgeColor)));
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ String.format(
+ "CaptionCommand SPC foregroundColor %s backgroundColor %s edgeColor %s",
+ foregroundColor, backgroundColor, edgeColor));
+ }
+ break;
+ }
+
+ case Cea708Data.CODE_C1_SPL:
+ {
+ // SetPenLocation
+ // column is normally 0-31 for 4:3 formats, and 0-41 for 16:9 formats
+ int row = data[pos] & 0x0f;
+ int column = data[pos + 1] & 0x3f;
+ pos += 2;
+ emitCaptionEvent(
+ new CaptionEvent(
+ CAPTION_EMIT_TYPE_COMMAND_SPL,
+ new CaptionPenLocation(row, column)));
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ String.format(
+ "CaptionCommand SPL row: %d, column: %d", row, column));
+ }
+ break;
+ }
+
+ case Cea708Data.CODE_C1_SWA:
+ {
+ // SetWindowAttributes
+ int opacity = (data[pos] & 0xc0) >> 6;
+ int red = (data[pos] & 0x30) >> 4;
+ int green = (data[pos] & 0x0c) >> 2;
+ int blue = data[pos] & 0x03;
+ CaptionColor fillColor = new CaptionColor(opacity, red, green, blue);
+ int borderType = (data[pos + 1] & 0xc0) >> 6 | (data[pos + 2] & 0x80) >> 5;
+ red = (data[pos + 1] & 0x30) >> 4;
+ green = (data[pos + 1] & 0x0c) >> 2;
+ blue = data[pos + 1] & 0x03;
+ CaptionColor borderColor =
+ new CaptionColor(CaptionColor.OPACITY_SOLID, red, green, blue);
+ boolean wordWrap = (data[pos + 2] & 0x40) != 0;
+ int printDirection = (data[pos + 2] & 0x30) >> 4;
+ int scrollDirection = (data[pos + 2] & 0x0c) >> 2;
+ int justify = (data[pos + 2] & 0x03);
+ int effectSpeed = (data[pos + 3] & 0xf0) >> 4;
+ int effectDirection = (data[pos + 3] & 0x0c) >> 2;
+ int displayEffect = data[pos + 3] & 0x3;
+ pos += 4;
+ emitCaptionEvent(
+ new CaptionEvent(
+ CAPTION_EMIT_TYPE_COMMAND_SWA,
+ new CaptionWindowAttr(
+ fillColor,
+ borderColor,
+ borderType,
+ wordWrap,
+ printDirection,
+ scrollDirection,
+ justify,
+ effectDirection,
+ effectSpeed,
+ displayEffect)));
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ String.format(
+ "CaptionCommand SWA fillColor: %s, borderColor: %s, borderType: %d"
+ + "wordWrap: %s, printDirection: %d, scrollDirection: %d, "
+ + "justify: %s, effectDirection: %d, effectSpeed: %d, "
+ + "displayEffect: %d",
+ fillColor,
+ borderColor,
+ borderType,
+ wordWrap,
+ printDirection,
+ scrollDirection,
+ justify,
+ effectDirection,
+ effectSpeed,
+ displayEffect));
+ }
+ break;
+ }
+
+ case Cea708Data.CODE_C1_DF0:
+ case Cea708Data.CODE_C1_DF1:
+ case Cea708Data.CODE_C1_DF2:
+ case Cea708Data.CODE_C1_DF3:
+ case Cea708Data.CODE_C1_DF4:
+ case Cea708Data.CODE_C1_DF5:
+ case Cea708Data.CODE_C1_DF6:
+ case Cea708Data.CODE_C1_DF7:
+ {
+ // DefineWindow0-7
+ int windowId = mCommand - Cea708Data.CODE_C1_DF0;
+ boolean visible = (data[pos] & 0x20) != 0;
+ boolean rowLock = (data[pos] & 0x10) != 0;
+ boolean columnLock = (data[pos] & 0x08) != 0;
+ int priority = data[pos] & 0x07;
+ boolean relativePositioning = (data[pos + 1] & 0x80) != 0;
+ int anchorVertical = data[pos + 1] & 0x7f;
+ int anchorHorizontal = data[pos + 2] & 0xff;
+ int anchorId = (data[pos + 3] & 0xf0) >> 4;
+ int rowCount = data[pos + 3] & 0x0f;
+ int columnCount = data[pos + 4] & 0x3f;
+ int windowStyle = (data[pos + 5] & 0x38) >> 3;
+ int penStyle = data[pos + 5] & 0x07;
+ pos += 6;
+ emitCaptionEvent(
+ new CaptionEvent(
+ CAPTION_EMIT_TYPE_COMMAND_DFX,
+ new CaptionWindow(
+ windowId,
+ visible,
+ rowLock,
+ columnLock,
+ priority,
+ relativePositioning,
+ anchorVertical,
+ anchorHorizontal,
+ anchorId,
+ rowCount,
+ columnCount,
+ penStyle,
+ windowStyle)));
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ String.format(
+ "CaptionCommand DFx windowId: %d, priority: %d, columnLock: %s, "
+ + "rowLock: %s, visible: %s, anchorVertical: %d, "
+ + "relativePositioning: %s, anchorHorizontal: %d, "
+ + "rowCount: %d, anchorId: %d, columnCount: %d, penStyle: %d, "
+ + "windowStyle: %d",
+ windowId,
+ priority,
+ columnLock,
+ rowLock,
+ visible,
+ anchorVertical,
+ relativePositioning,
+ anchorHorizontal,
+ rowCount,
+ anchorId,
+ columnCount,
+ penStyle,
+ windowStyle));
+ }
+ break;
+ }
+
+ default:
+ break;
+ }
+ return pos;
+ }
+
+ private int parseG0(byte[] data, int pos) {
+ // For the details of G0 code group, see CEA-708B Section 7.4.3.
+ // GL Group: G0 Modified version of ANSI X3.4 Printable Character Set (ASCII)
+ if (mCommand == Cea708Data.CODE_G0_MUSICNOTE) {
+ // Music note.
+ mBuffer.append(MUSIC_NOTE_CHAR);
+ } else {
+ // Put ASCII code into buffer.
+ mBuffer.append((char) mCommand);
+ }
+ return pos;
+ }
+
+ private int parseG1(byte[] data, int pos) {
+ // For the details of G0 code group, see CEA-708B Section 7.4.4.
+ // GR Group: G1 ISO 8859-1 Latin 1 Characters
+ // Put ASCII Extended character set into buffer.
+ mBuffer.append((char) mCommand);
+ return pos;
+ }
+
+ // Step 4. Extended code groups
+ private int parseExt1(byte[] data, int pos) {
+ // For the details of EXT1 code group, see CEA-708B Section 7.2.
+ mCommand = data[pos] & 0xff;
+ ++pos;
+ if (mCommand >= Cea708Data.CODE_C2_RANGE_START
+ && mCommand <= Cea708Data.CODE_C2_RANGE_END) {
+ pos = parseC2(data, pos);
+ } else if (mCommand >= Cea708Data.CODE_C3_RANGE_START
+ && mCommand <= Cea708Data.CODE_C3_RANGE_END) {
+ pos = parseC3(data, pos);
+ } else if (mCommand >= Cea708Data.CODE_G2_RANGE_START
+ && mCommand <= Cea708Data.CODE_G2_RANGE_END) {
+ pos = parseG2(data, pos);
+ } else if (mCommand >= Cea708Data.CODE_G3_RANGE_START
+ && mCommand <= Cea708Data.CODE_G3_RANGE_END) {
+ pos = parseG3(data, pos);
+ }
+ return pos;
+ }
+
+ private int parseC2(byte[] data, int pos) {
+ // For the details of C2 code group, see CEA-708B Section 7.4.7.
+ // Extended Miscellaneous Control Codes
+ // C2 Table : No commands as of CEA-708B. A decoder must skip.
+ if (mCommand >= Cea708Data.CODE_C2_SKIP0_RANGE_START
+ && mCommand <= Cea708Data.CODE_C2_SKIP0_RANGE_END) {
+ // Do nothing.
+ } else if (mCommand >= Cea708Data.CODE_C2_SKIP1_RANGE_START
+ && mCommand <= Cea708Data.CODE_C2_SKIP1_RANGE_END) {
+ ++pos;
+ } else if (mCommand >= Cea708Data.CODE_C2_SKIP2_RANGE_START
+ && mCommand <= Cea708Data.CODE_C2_SKIP2_RANGE_END) {
+ pos += 2;
+ } else if (mCommand >= Cea708Data.CODE_C2_SKIP3_RANGE_START
+ && mCommand <= Cea708Data.CODE_C2_SKIP3_RANGE_END) {
+ pos += 3;
+ }
+ return pos;
+ }
+
+ private int parseC3(byte[] data, int pos) {
+ // For the details of C3 code group, see CEA-708B Section 7.4.8.
+ // Extended Control Code Set 2
+ // C3 Table : No commands as of CEA-708B. A decoder must skip.
+ if (mCommand >= Cea708Data.CODE_C3_SKIP4_RANGE_START
+ && mCommand <= Cea708Data.CODE_C3_SKIP4_RANGE_END) {
+ pos += 4;
+ } else if (mCommand >= Cea708Data.CODE_C3_SKIP5_RANGE_START
+ && mCommand <= Cea708Data.CODE_C3_SKIP5_RANGE_END) {
+ pos += 5;
+ }
+ return pos;
+ }
+
+ private int parseG2(byte[] data, int pos) {
+ // For the details of C3 code group, see CEA-708B Section 7.4.5.
+ // Extended Control Code Set 1(G2 Table)
+ switch (mCommand) {
+ case Cea708Data.CODE_G2_TSP:
+ // TODO : TSP is the Transparent space
+ break;
+ case Cea708Data.CODE_G2_NBTSP:
+ // TODO : NBTSP is Non-Breaking Transparent Space.
+ break;
+ case Cea708Data.CODE_G2_BLK:
+ // TODO : BLK indicates a solid block which fills the entire character block
+ // TODO : with a solid foreground color.
+ break;
+ default:
+ break;
+ }
+ return pos;
+ }
+
+ private int parseG3(byte[] data, int pos) {
+ // For the details of C3 code group, see CEA-708B Section 7.4.6.
+ // Future characters and icons(G3 Table)
+ if (mCommand == Cea708Data.CODE_G3_CC) {
+ // TODO : [CC] icon with square corners
+ }
+
+ // Do nothing
+ return pos;
+ }
+}
diff --git a/src/com/android/tv/tuner/data/Cea708Data.java b/src/com/android/tv/tuner/data/Cea708Data.java
new file mode 100644
index 00000000..73a90181
--- /dev/null
+++ b/src/com/android/tv/tuner/data/Cea708Data.java
@@ -0,0 +1,329 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.data;
+
+import android.graphics.Color;
+import android.support.annotation.NonNull;
+import com.android.tv.tuner.cc.Cea708Parser;
+
+/** Collection of CEA-708 structures. */
+public class Cea708Data {
+
+ private Cea708Data() {}
+
+ // According to CEA-708B, the range of valid service number is between 1 and 63.
+ public static final int EMPTY_SERVICE_NUMBER = 0;
+
+ // For the details of the ranges of DTVCC code groups, see CEA-708B Table 6.
+ public static final int CODE_C0_RANGE_START = 0x00;
+ public static final int CODE_C0_RANGE_END = 0x1f;
+ public static final int CODE_C1_RANGE_START = 0x80;
+ public static final int CODE_C1_RANGE_END = 0x9f;
+ public static final int CODE_G0_RANGE_START = 0x20;
+ public static final int CODE_G0_RANGE_END = 0x7f;
+ public static final int CODE_G1_RANGE_START = 0xa0;
+ public static final int CODE_G1_RANGE_END = 0xff;
+ public static final int CODE_C2_RANGE_START = 0x00;
+ public static final int CODE_C2_RANGE_END = 0x1f;
+ public static final int CODE_C3_RANGE_START = 0x80;
+ public static final int CODE_C3_RANGE_END = 0x9f;
+ public static final int CODE_G2_RANGE_START = 0x20;
+ public static final int CODE_G2_RANGE_END = 0x7f;
+ public static final int CODE_G3_RANGE_START = 0xa0;
+ public static final int CODE_G3_RANGE_END = 0xff;
+
+ // The following ranges are defined in CEA-708B Section 7.4.1.
+ public static final int CODE_C0_SKIP2_RANGE_START = 0x18;
+ public static final int CODE_C0_SKIP2_RANGE_END = 0x1f;
+ public static final int CODE_C0_SKIP1_RANGE_START = 0x10;
+ public static final int CODE_C0_SKIP1_RANGE_END = 0x17;
+
+ // The following ranges are defined in CEA-708B Section 7.4.7.
+ public static final int CODE_C2_SKIP0_RANGE_START = 0x00;
+ public static final int CODE_C2_SKIP0_RANGE_END = 0x07;
+ public static final int CODE_C2_SKIP1_RANGE_START = 0x08;
+ public static final int CODE_C2_SKIP1_RANGE_END = 0x0f;
+ public static final int CODE_C2_SKIP2_RANGE_START = 0x10;
+ public static final int CODE_C2_SKIP2_RANGE_END = 0x17;
+ public static final int CODE_C2_SKIP3_RANGE_START = 0x18;
+ public static final int CODE_C2_SKIP3_RANGE_END = 0x1f;
+
+ // The following ranges are defined in CEA-708B Section 7.4.8.
+ public static final int CODE_C3_SKIP4_RANGE_START = 0x80;
+ public static final int CODE_C3_SKIP4_RANGE_END = 0x87;
+ public static final int CODE_C3_SKIP5_RANGE_START = 0x88;
+ public static final int CODE_C3_SKIP5_RANGE_END = 0x8f;
+
+ // The following values are the special characters of CEA-708 spec.
+ public static final int CODE_C0_NUL = 0x00;
+ public static final int CODE_C0_ETX = 0x03;
+ public static final int CODE_C0_BS = 0x08;
+ public static final int CODE_C0_FF = 0x0c;
+ public static final int CODE_C0_CR = 0x0d;
+ public static final int CODE_C0_HCR = 0x0e;
+ public static final int CODE_C0_EXT1 = 0x10;
+ public static final int CODE_C0_P16 = 0x18;
+ public static final int CODE_G0_MUSICNOTE = 0x7f;
+ public static final int CODE_G2_TSP = 0x20;
+ public static final int CODE_G2_NBTSP = 0x21;
+ public static final int CODE_G2_BLK = 0x30;
+ public static final int CODE_G3_CC = 0xa0;
+
+ // The following values are the command bits of CEA-708 spec.
+ public static final int CODE_C1_CW0 = 0x80;
+ public static final int CODE_C1_CW1 = 0x81;
+ public static final int CODE_C1_CW2 = 0x82;
+ public static final int CODE_C1_CW3 = 0x83;
+ public static final int CODE_C1_CW4 = 0x84;
+ public static final int CODE_C1_CW5 = 0x85;
+ public static final int CODE_C1_CW6 = 0x86;
+ public static final int CODE_C1_CW7 = 0x87;
+ public static final int CODE_C1_CLW = 0x88;
+ public static final int CODE_C1_DSW = 0x89;
+ public static final int CODE_C1_HDW = 0x8a;
+ public static final int CODE_C1_TGW = 0x8b;
+ public static final int CODE_C1_DLW = 0x8c;
+ public static final int CODE_C1_DLY = 0x8d;
+ public static final int CODE_C1_DLC = 0x8e;
+ public static final int CODE_C1_RST = 0x8f;
+ public static final int CODE_C1_SPA = 0x90;
+ public static final int CODE_C1_SPC = 0x91;
+ public static final int CODE_C1_SPL = 0x92;
+ public static final int CODE_C1_SWA = 0x97;
+ public static final int CODE_C1_DF0 = 0x98;
+ public static final int CODE_C1_DF1 = 0x99;
+ public static final int CODE_C1_DF2 = 0x9a;
+ public static final int CODE_C1_DF3 = 0x9b;
+ public static final int CODE_C1_DF4 = 0x9c;
+ public static final int CODE_C1_DF5 = 0x9d;
+ public static final int CODE_C1_DF6 = 0x9e;
+ public static final int CODE_C1_DF7 = 0x9f;
+
+ public static class CcPacket implements Comparable<CcPacket> {
+ public final byte[] bytes;
+ public final int ccCount;
+ public final long pts;
+
+ public CcPacket(byte[] bytes, int ccCount, long pts) {
+ this.bytes = bytes;
+ this.ccCount = ccCount;
+ this.pts = pts;
+ }
+
+ @Override
+ public int compareTo(@NonNull CcPacket another) {
+ return Long.compare(pts, another.pts);
+ }
+ }
+
+ /** CEA-708B-specific color. */
+ public static class CaptionColor {
+ public static final int OPACITY_SOLID = 0;
+ public static final int OPACITY_FLASH = 1;
+ public static final int OPACITY_TRANSLUCENT = 2;
+ public static final int OPACITY_TRANSPARENT = 3;
+
+ private static final int[] COLOR_MAP = new int[] {0x00, 0x0f, 0xf0, 0xff};
+ private static final int[] OPACITY_MAP = new int[] {0xff, 0xfe, 0x80, 0x00};
+
+ public final int opacity;
+ public final int red;
+ public final int green;
+ public final int blue;
+
+ public CaptionColor(int opacity, int red, int green, int blue) {
+ this.opacity = opacity;
+ this.red = red;
+ this.green = green;
+ this.blue = blue;
+ }
+
+ public int getArgbValue() {
+ return Color.argb(
+ OPACITY_MAP[opacity], COLOR_MAP[red], COLOR_MAP[green], COLOR_MAP[blue]);
+ }
+ }
+
+ /** Caption event generated by {@link Cea708Parser}. */
+ public static class CaptionEvent {
+ @Cea708Parser.CaptionEmitType public final int type;
+ public final Object obj;
+
+ public CaptionEvent(int type, Object obj) {
+ this.type = type;
+ this.obj = obj;
+ }
+ }
+
+ /** Pen style information. */
+ public static class CaptionPenAttr {
+ // Pen sizes
+ public static final int PEN_SIZE_SMALL = 0;
+ public static final int PEN_SIZE_STANDARD = 1;
+ public static final int PEN_SIZE_LARGE = 2;
+
+ // Offsets
+ public static final int OFFSET_SUBSCRIPT = 0;
+ public static final int OFFSET_NORMAL = 1;
+ public static final int OFFSET_SUPERSCRIPT = 2;
+
+ public final int penSize;
+ public final int penOffset;
+ public final int textTag;
+ public final int fontTag;
+ public final int edgeType;
+ public final boolean underline;
+ public final boolean italic;
+
+ public CaptionPenAttr(
+ int penSize,
+ int penOffset,
+ int textTag,
+ int fontTag,
+ int edgeType,
+ boolean underline,
+ boolean italic) {
+ this.penSize = penSize;
+ this.penOffset = penOffset;
+ this.textTag = textTag;
+ this.fontTag = fontTag;
+ this.edgeType = edgeType;
+ this.underline = underline;
+ this.italic = italic;
+ }
+ }
+
+ /**
+ * {@link CaptionColor} objects that indicate the foreground, background, and edge color of a
+ * pen.
+ */
+ public static class CaptionPenColor {
+ public final CaptionColor foregroundColor;
+ public final CaptionColor backgroundColor;
+ public final CaptionColor edgeColor;
+
+ public CaptionPenColor(
+ CaptionColor foregroundColor,
+ CaptionColor backgroundColor,
+ CaptionColor edgeColor) {
+ this.foregroundColor = foregroundColor;
+ this.backgroundColor = backgroundColor;
+ this.edgeColor = edgeColor;
+ }
+ }
+
+ /** Location information of a pen. */
+ public static class CaptionPenLocation {
+ public final int row;
+ public final int column;
+
+ public CaptionPenLocation(int row, int column) {
+ this.row = row;
+ this.column = column;
+ }
+ }
+
+ /** Attributes of a caption window, which is defined in CEA-708B. */
+ public static class CaptionWindowAttr {
+ public static final int JUSTIFY_LEFT = 0;
+ public static final int JUSTIFY_CENTER = 2;
+ public static final int PRINT_LEFT_TO_RIGHT = 0;
+ public static final int PRINT_RIGHT_TO_LEFT = 1;
+ public static final int PRINT_TOP_TO_BOTTOM = 2;
+ public static final int PRINT_BOTTOM_TO_TOP = 3;
+
+ public final CaptionColor fillColor;
+ public final CaptionColor borderColor;
+ public final int borderType;
+ public final boolean wordWrap;
+ public final int printDirection;
+ public final int scrollDirection;
+ public final int justify;
+ public final int effectDirection;
+ public final int effectSpeed;
+ public final int displayEffect;
+
+ public CaptionWindowAttr(
+ CaptionColor fillColor,
+ CaptionColor borderColor,
+ int borderType,
+ boolean wordWrap,
+ int printDirection,
+ int scrollDirection,
+ int justify,
+ int effectDirection,
+ int effectSpeed,
+ int displayEffect) {
+ this.fillColor = fillColor;
+ this.borderColor = borderColor;
+ this.borderType = borderType;
+ this.wordWrap = wordWrap;
+ this.printDirection = printDirection;
+ this.scrollDirection = scrollDirection;
+ this.justify = justify;
+ this.effectDirection = effectDirection;
+ this.effectSpeed = effectSpeed;
+ this.displayEffect = displayEffect;
+ }
+ }
+
+ /** Construction information of the caption window of CEA-708B. */
+ public static class CaptionWindow {
+ public final int id;
+ public final boolean visible;
+ public final boolean rowLock;
+ public final boolean columnLock;
+ public final int priority;
+ public final boolean relativePositioning;
+ public final int anchorVertical;
+ public final int anchorHorizontal;
+ public final int anchorId;
+ public final int rowCount;
+ public final int columnCount;
+ public final int penStyle;
+ public final int windowStyle;
+
+ public CaptionWindow(
+ int id,
+ boolean visible,
+ boolean rowLock,
+ boolean columnLock,
+ int priority,
+ boolean relativePositioning,
+ int anchorVertical,
+ int anchorHorizontal,
+ int anchorId,
+ int rowCount,
+ int columnCount,
+ int penStyle,
+ int windowStyle) {
+ this.id = id;
+ this.visible = visible;
+ this.rowLock = rowLock;
+ this.columnLock = columnLock;
+ this.priority = priority;
+ this.relativePositioning = relativePositioning;
+ this.anchorVertical = anchorVertical;
+ this.anchorHorizontal = anchorHorizontal;
+ this.anchorId = anchorId;
+ this.rowCount = rowCount;
+ this.columnCount = columnCount;
+ this.penStyle = penStyle;
+ this.windowStyle = windowStyle;
+ }
+ }
+}
diff --git a/src/com/android/tv/tuner/data/PsiData.java b/src/com/android/tv/tuner/data/PsiData.java
new file mode 100644
index 00000000..9b7c2e2c
--- /dev/null
+++ b/src/com/android/tv/tuner/data/PsiData.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.data;
+
+import com.android.tv.tuner.data.nano.Track.AtscAudioTrack;
+import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack;
+import java.util.List;
+
+/** Collection of MPEG PSI table items. */
+public class PsiData {
+
+ private PsiData() {}
+
+ public static class PatItem {
+ private final int mProgramNo;
+ private final int mPmtPid;
+
+ public PatItem(int programNo, int pmtPid) {
+ mProgramNo = programNo;
+ mPmtPid = pmtPid;
+ }
+
+ public int getProgramNo() {
+ return mProgramNo;
+ }
+
+ public int getPmtPid() {
+ return mPmtPid;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("Program No: %x PMT Pid: %x", mProgramNo, mPmtPid);
+ }
+ }
+
+ public static class PmtItem {
+ public static final int ES_PID_PCR = 0x100;
+
+ private final int mStreamType;
+ private final int mEsPid;
+ private final List<AtscAudioTrack> mAudioTracks;
+ private final List<AtscCaptionTrack> mCaptionTracks;
+
+ public PmtItem(
+ int streamType,
+ int esPid,
+ List<AtscAudioTrack> audioTracks,
+ List<AtscCaptionTrack> captionTracks) {
+ mStreamType = streamType;
+ mEsPid = esPid;
+ mAudioTracks = audioTracks;
+ mCaptionTracks = captionTracks;
+ }
+
+ public int getStreamType() {
+ return mStreamType;
+ }
+
+ public int getEsPid() {
+ return mEsPid;
+ }
+
+ public List<AtscAudioTrack> getAudioTracks() {
+ return mAudioTracks;
+ }
+
+ public List<AtscCaptionTrack> getCaptionTracks() {
+ return mCaptionTracks;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ "Stream Type: %x ES Pid: %x AudioTracks: %s CaptionTracks: %s",
+ mStreamType, mEsPid, mAudioTracks, mCaptionTracks);
+ }
+ }
+}
diff --git a/src/com/android/tv/tuner/data/PsipData.java b/src/com/android/tv/tuner/data/PsipData.java
new file mode 100644
index 00000000..6459004c
--- /dev/null
+++ b/src/com/android/tv/tuner/data/PsipData.java
@@ -0,0 +1,871 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.data;
+
+import android.support.annotation.NonNull;
+import android.text.TextUtils;
+import android.text.format.DateUtils;
+import com.android.tv.tuner.data.nano.Track.AtscAudioTrack;
+import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack;
+import com.android.tv.tuner.ts.SectionParser;
+import com.android.tv.tuner.util.ConvertUtils;
+import com.android.tv.util.StringUtils;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+
+/** Collection of ATSC PSIP table items. */
+public class PsipData {
+
+ private PsipData() {}
+
+ public static class PsipSection {
+ private final int mTableId;
+ private final int mTableIdExtension;
+ private final int mSectionNumber;
+ private final boolean mCurrentNextIndicator;
+
+ public static PsipSection create(byte[] data) {
+ if (data.length < 9) {
+ return null;
+ }
+ int tableId = data[0] & 0xff;
+ int tableIdExtension = (data[3] & 0xff) << 8 | (data[4] & 0xff);
+ int sectionNumber = data[6] & 0xff;
+ boolean currentNextIndicator = (data[5] & 0x01) != 0;
+ return new PsipSection(tableId, tableIdExtension, sectionNumber, currentNextIndicator);
+ }
+
+ private PsipSection(
+ int tableId,
+ int tableIdExtension,
+ int sectionNumber,
+ boolean currentNextIndicator) {
+ mTableId = tableId;
+ mTableIdExtension = tableIdExtension;
+ mSectionNumber = sectionNumber;
+ mCurrentNextIndicator = currentNextIndicator;
+ }
+
+ public int getTableId() {
+ return mTableId;
+ }
+
+ public int getTableIdExtension() {
+ return mTableIdExtension;
+ }
+
+ public int getSectionNumber() {
+ return mSectionNumber;
+ }
+
+ // This is for indicating that the section sent is applicable.
+ // We only consider a situation where currentNextIndicator is expected to have a true value.
+ // So, we are not going to compare this variable in hashCode() and equals() methods.
+ public boolean getCurrentNextIndicator() {
+ return mCurrentNextIndicator;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 17;
+ result = 31 * result + mTableId;
+ result = 31 * result + mTableIdExtension;
+ result = 31 * result + mSectionNumber;
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof PsipSection) {
+ PsipSection another = (PsipSection) obj;
+ return mTableId == another.getTableId()
+ && mTableIdExtension == another.getTableIdExtension()
+ && mSectionNumber == another.getSectionNumber();
+ }
+ return false;
+ }
+ }
+
+ /** {@link TvTracksInterface} for serving the audio and caption tracks. */
+ public interface TvTracksInterface {
+ /** Set the flag that tells the caption tracks have been found in this section container. */
+ void setHasCaptionTrack();
+
+ /**
+ * Returns whether or not the caption tracks have been found in this section container. If
+ * true, zero caption track will be interpreted as a clearance of the caption tracks.
+ */
+ boolean hasCaptionTrack();
+
+ /** Returns the audio tracks received. */
+ List<AtscAudioTrack> getAudioTracks();
+
+ /** Returns the caption tracks received. */
+ List<AtscCaptionTrack> getCaptionTracks();
+ }
+
+ public static class MgtItem {
+ public static final int TABLE_TYPE_EIT_RANGE_START = 0x0100;
+ public static final int TABLE_TYPE_EIT_RANGE_END = 0x017f;
+ public static final int TABLE_TYPE_CHANNEL_ETT = 0x0004;
+ public static final int TABLE_TYPE_ETT_RANGE_START = 0x0200;
+ public static final int TABLE_TYPE_ETT_RANGE_END = 0x027f;
+
+ private final int mTableType;
+ private final int mTableTypePid;
+
+ public MgtItem(int tableType, int tableTypePid) {
+ mTableType = tableType;
+ mTableTypePid = tableTypePid;
+ }
+
+ public int getTableType() {
+ return mTableType;
+ }
+
+ public int getTableTypePid() {
+ return mTableTypePid;
+ }
+ }
+
+ public static class VctItem {
+ private final String mShortName;
+ private final String mLongName;
+ private final int mServiceType;
+ private final int mChannelTsid;
+ private final int mProgramNumber;
+ private final int mMajorChannelNumber;
+ private final int mMinorChannelNumber;
+ private final int mSourceId;
+ private String mDescription;
+
+ public VctItem(
+ String shortName,
+ String longName,
+ int serviceType,
+ int channelTsid,
+ int programNumber,
+ int majorChannelNumber,
+ int minorChannelNumber,
+ int sourceId) {
+ mShortName = shortName;
+ mLongName = longName;
+ mServiceType = serviceType;
+ mChannelTsid = channelTsid;
+ mProgramNumber = programNumber;
+ mMajorChannelNumber = majorChannelNumber;
+ mMinorChannelNumber = minorChannelNumber;
+ mSourceId = sourceId;
+ }
+
+ public String getShortName() {
+ return mShortName;
+ }
+
+ public String getLongName() {
+ return mLongName;
+ }
+
+ public int getServiceType() {
+ return mServiceType;
+ }
+
+ public int getChannelTsid() {
+ return mChannelTsid;
+ }
+
+ public int getProgramNumber() {
+ return mProgramNumber;
+ }
+
+ public int getMajorChannelNumber() {
+ return mMajorChannelNumber;
+ }
+
+ public int getMinorChannelNumber() {
+ return mMinorChannelNumber;
+ }
+
+ public int getSourceId() {
+ return mSourceId;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ Locale.US,
+ "ShortName: %s LongName: %s ServiceType: %d ChannelTsid: %x "
+ + "ProgramNumber:%d %d-%d SourceId: %x",
+ mShortName,
+ mLongName,
+ mServiceType,
+ mChannelTsid,
+ mProgramNumber,
+ mMajorChannelNumber,
+ mMinorChannelNumber,
+ mSourceId);
+ }
+
+ public void setDescription(String description) {
+ mDescription = description;
+ }
+
+ public String getDescription() {
+ return mDescription;
+ }
+ }
+
+ 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. */
+ public abstract static class TsDescriptor {
+ public abstract int getTag();
+ }
+
+ public static class ContentAdvisoryDescriptor extends TsDescriptor {
+ private final List<RatingRegion> mRatingRegions;
+
+ public ContentAdvisoryDescriptor(List<RatingRegion> ratingRegions) {
+ mRatingRegions = ratingRegions;
+ }
+
+ @Override
+ public int getTag() {
+ return SectionParser.DESCRIPTOR_TAG_CONTENT_ADVISORY;
+ }
+
+ public List<RatingRegion> getRatingRegions() {
+ return mRatingRegions;
+ }
+ }
+
+ public static class CaptionServiceDescriptor extends TsDescriptor {
+ private final List<AtscCaptionTrack> mCaptionTracks;
+
+ public CaptionServiceDescriptor(List<AtscCaptionTrack> captionTracks) {
+ mCaptionTracks = captionTracks;
+ }
+
+ @Override
+ public int getTag() {
+ return SectionParser.DESCRIPTOR_TAG_CAPTION_SERVICE;
+ }
+
+ public List<AtscCaptionTrack> getCaptionTracks() {
+ return mCaptionTracks;
+ }
+ }
+
+ public static class ExtendedChannelNameDescriptor extends TsDescriptor {
+ private final String mLongChannelName;
+
+ public ExtendedChannelNameDescriptor(String longChannelName) {
+ mLongChannelName = longChannelName;
+ }
+
+ @Override
+ public int getTag() {
+ return SectionParser.DESCRIPTOR_TAG_EXTENDED_CHANNEL_NAME;
+ }
+
+ public String getLongChannelName() {
+ return mLongChannelName;
+ }
+ }
+
+ public static class GenreDescriptor extends TsDescriptor {
+ private final String[] mBroadcastGenres;
+ private final String[] mCanonicalGenres;
+
+ public GenreDescriptor(String[] broadcastGenres, String[] canonicalGenres) {
+ mBroadcastGenres = broadcastGenres;
+ mCanonicalGenres = canonicalGenres;
+ }
+
+ @Override
+ public int getTag() {
+ return SectionParser.DESCRIPTOR_TAG_GENRE;
+ }
+
+ public String[] getBroadcastGenres() {
+ return mBroadcastGenres;
+ }
+
+ public String[] getCanonicalGenres() {
+ return mCanonicalGenres;
+ }
+ }
+
+ public static class Ac3AudioDescriptor extends TsDescriptor {
+ // See A/52 Annex A. Table A4.2
+ private static final byte SAMPLE_RATE_CODE_48000HZ = 0;
+ private static final byte SAMPLE_RATE_CODE_44100HZ = 1;
+ private static final byte SAMPLE_RATE_CODE_32000HZ = 2;
+
+ private final byte mSampleRateCode;
+ private final byte mBsid;
+ private final byte mBitRateCode;
+ private final byte mSurroundMode;
+ private final byte mBsmod;
+ private final int mNumChannels;
+ private final boolean mFullSvc;
+ private final byte mLangCod;
+ private final byte mLangCod2;
+ private final byte mMainId;
+ private final byte mPriority;
+ private final byte mAsvcflags;
+ private final String mText;
+ private final String mLanguage;
+ private final String mLanguage2;
+
+ public Ac3AudioDescriptor(
+ byte sampleRateCode,
+ byte bsid,
+ byte bitRateCode,
+ byte surroundMode,
+ byte bsmod,
+ int numChannels,
+ boolean fullSvc,
+ byte langCod,
+ byte langCod2,
+ byte mainId,
+ byte priority,
+ byte asvcflags,
+ String text,
+ String language,
+ String language2) {
+ mSampleRateCode = sampleRateCode;
+ mBsid = bsid;
+ mBitRateCode = bitRateCode;
+ mSurroundMode = surroundMode;
+ mBsmod = bsmod;
+ mNumChannels = numChannels;
+ mFullSvc = fullSvc;
+ mLangCod = langCod;
+ mLangCod2 = langCod2;
+ mMainId = mainId;
+ mPriority = priority;
+ mAsvcflags = asvcflags;
+ mText = text;
+ mLanguage = language;
+ mLanguage2 = language2;
+ }
+
+ @Override
+ public int getTag() {
+ return SectionParser.DESCRIPTOR_TAG_AC3_AUDIO_STREAM;
+ }
+
+ public byte getSampleRateCode() {
+ return mSampleRateCode;
+ }
+
+ public int getSampleRate() {
+ switch (mSampleRateCode) {
+ case SAMPLE_RATE_CODE_48000HZ:
+ return 48000;
+ case SAMPLE_RATE_CODE_44100HZ:
+ return 44100;
+ case SAMPLE_RATE_CODE_32000HZ:
+ return 32000;
+ default:
+ return 0;
+ }
+ }
+
+ public byte getBsid() {
+ return mBsid;
+ }
+
+ public byte getBitRateCode() {
+ return mBitRateCode;
+ }
+
+ public byte getSurroundMode() {
+ return mSurroundMode;
+ }
+
+ public byte getBsmod() {
+ return mBsmod;
+ }
+
+ public int getNumChannels() {
+ return mNumChannels;
+ }
+
+ public boolean isFullSvc() {
+ return mFullSvc;
+ }
+
+ public byte getLangCod() {
+ return mLangCod;
+ }
+
+ public byte getLangCod2() {
+ return mLangCod2;
+ }
+
+ public byte getMainId() {
+ return mMainId;
+ }
+
+ public byte getPriority() {
+ return mPriority;
+ }
+
+ public byte getAsvcflags() {
+ return mAsvcflags;
+ }
+
+ public String getText() {
+ return mText;
+ }
+
+ public String getLanguage() {
+ return mLanguage;
+ }
+
+ public String getLanguage2() {
+ return mLanguage2;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ Locale.US,
+ "AC3 audio stream sampleRateCode: %d, bsid: %d, bitRateCode: %d, "
+ + "surroundMode: %d, bsmod: %d, numChannels: %d, fullSvc: %s, langCod: %d, "
+ + "langCod2: %d, mainId: %d, priority: %d, avcflags: %d, text: %s, language: %s"
+ + ", language2: %s",
+ mSampleRateCode,
+ mBsid,
+ mBitRateCode,
+ mSurroundMode,
+ mBsmod,
+ mNumChannels,
+ mFullSvc,
+ mLangCod,
+ mLangCod2,
+ mMainId,
+ mPriority,
+ mAsvcflags,
+ mText,
+ mLanguage,
+ mLanguage2);
+ }
+ }
+
+ public static class Iso639LanguageDescriptor extends TsDescriptor {
+ private final List<AtscAudioTrack> mAudioTracks;
+
+ public Iso639LanguageDescriptor(List<AtscAudioTrack> audioTracks) {
+ mAudioTracks = audioTracks;
+ }
+
+ @Override
+ public int getTag() {
+ return SectionParser.DESCRIPTOR_TAG_ISO639LANGUAGE;
+ }
+
+ public List<AtscAudioTrack> getAudioTracks() {
+ return mAudioTracks;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("%s %s", getClass().getName(), mAudioTracks);
+ }
+ }
+
+ 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;
+ private final List<RegionalRating> mRegionalRatings;
+
+ public RatingRegion(int name, String description, List<RegionalRating> regionalRatings) {
+ mName = name;
+ mDescription = description;
+ mRegionalRatings = regionalRatings;
+ }
+
+ public int getName() {
+ return mName;
+ }
+
+ public String getDescription() {
+ return mDescription;
+ }
+
+ public List<RegionalRating> getRegionalRatings() {
+ return mRegionalRatings;
+ }
+ }
+
+ public static class RegionalRating {
+ private final int mDimension;
+ private final int mRating;
+
+ public RegionalRating(int dimension, int rating) {
+ mDimension = dimension;
+ mRating = rating;
+ }
+
+ public int getDimension() {
+ return mDimension;
+ }
+
+ public int getRating() {
+ return mRating;
+ }
+ }
+
+ public static class EitItem implements Comparable<EitItem>, TvTracksInterface {
+ public static final long INVALID_PROGRAM_ID = -1;
+
+ // A program id is a primary key of TvContract.Programs table. So it must be positive.
+ private final long mProgramId;
+ private final int mEventId;
+ private final String mTitleText;
+ private String mDescription;
+ private final long mStartTime;
+ private final int mLengthInSecond;
+ private final String mContentRating;
+ private final List<AtscAudioTrack> mAudioTracks;
+ private final List<AtscCaptionTrack> mCaptionTracks;
+ private boolean mHasCaptionTrack;
+ private final String mBroadcastGenre;
+ private final String mCanonicalGenre;
+
+ public EitItem(
+ long programId,
+ int eventId,
+ String titleText,
+ long startTime,
+ int lengthInSecond,
+ String contentRating,
+ List<AtscAudioTrack> audioTracks,
+ List<AtscCaptionTrack> captionTracks,
+ String broadcastGenre,
+ String canonicalGenre,
+ String description) {
+ mProgramId = programId;
+ mEventId = eventId;
+ mTitleText = titleText;
+ mStartTime = startTime;
+ mLengthInSecond = lengthInSecond;
+ mContentRating = contentRating;
+ mAudioTracks = audioTracks;
+ mCaptionTracks = captionTracks;
+ mBroadcastGenre = broadcastGenre;
+ mCanonicalGenre = canonicalGenre;
+ mDescription = description;
+ }
+
+ public long getProgramId() {
+ return mProgramId;
+ }
+
+ public int getEventId() {
+ return mEventId;
+ }
+
+ public String getTitleText() {
+ return mTitleText;
+ }
+
+ public void setDescription(String description) {
+ mDescription = description;
+ }
+
+ public String getDescription() {
+ return mDescription;
+ }
+
+ public long getStartTime() {
+ return mStartTime;
+ }
+
+ public int getLengthInSecond() {
+ return mLengthInSecond;
+ }
+
+ public long getStartTimeUtcMillis() {
+ return ConvertUtils.convertGPSTimeToUnixEpoch(mStartTime) * DateUtils.SECOND_IN_MILLIS;
+ }
+
+ public long getEndTimeUtcMillis() {
+ return ConvertUtils.convertGPSTimeToUnixEpoch(mStartTime + mLengthInSecond)
+ * DateUtils.SECOND_IN_MILLIS;
+ }
+
+ public String getContentRating() {
+ return mContentRating;
+ }
+
+ @Override
+ public List<AtscAudioTrack> getAudioTracks() {
+ return mAudioTracks;
+ }
+
+ @Override
+ public List<AtscCaptionTrack> getCaptionTracks() {
+ return mCaptionTracks;
+ }
+
+ public String getBroadcastGenre() {
+ return mBroadcastGenre;
+ }
+
+ public String getCanonicalGenre() {
+ return mCanonicalGenre;
+ }
+
+ @Override
+ public void setHasCaptionTrack() {
+ mHasCaptionTrack = true;
+ }
+
+ @Override
+ public boolean hasCaptionTrack() {
+ return mHasCaptionTrack;
+ }
+
+ @Override
+ public int compareTo(@NonNull EitItem item) {
+ // The list of caption tracks and the program ids are not compared in here because the
+ // channels in TIF have the concept of the caption and audio tracks while the programs
+ // do not and the programs in TIF only have a program id since they are the rows of
+ // Content Provider.
+ int ret = mEventId - item.getEventId();
+ if (ret != 0) {
+ return ret;
+ }
+ ret = StringUtils.compare(mTitleText, item.getTitleText());
+ if (ret != 0) {
+ return ret;
+ }
+ if (mStartTime > item.getStartTime()) {
+ return 1;
+ } else if (mStartTime < item.getStartTime()) {
+ return -1;
+ }
+ if (mLengthInSecond > item.getLengthInSecond()) {
+ return 1;
+ } else if (mLengthInSecond < item.getLengthInSecond()) {
+ return -1;
+ }
+
+ // Compares content ratings
+ ret = StringUtils.compare(mContentRating, item.getContentRating());
+ if (ret != 0) {
+ return ret;
+ }
+
+ // Compares broadcast genres
+ ret = StringUtils.compare(mBroadcastGenre, item.getBroadcastGenre());
+ if (ret != 0) {
+ return ret;
+ }
+ // Compares canonical genres
+ ret = StringUtils.compare(mCanonicalGenre, item.getCanonicalGenre());
+ if (ret != 0) {
+ return ret;
+ }
+
+ // Compares descriptions
+ return StringUtils.compare(mDescription, item.getDescription());
+ }
+
+ public String getAudioLanguage() {
+ if (mAudioTracks == null) {
+ return "";
+ }
+ ArrayList<String> languages = new ArrayList<>();
+ for (AtscAudioTrack audioTrack : mAudioTracks) {
+ languages.add(audioTrack.language);
+ }
+ return TextUtils.join(",", languages);
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ Locale.US,
+ "EitItem programId: %d, eventId: %d, title: %s, startTime: %10d, "
+ + "length: %6d, rating: %s, audio tracks: %d, caption tracks: %d, "
+ + "genres (broadcast: %s, canonical: %s), description: %s",
+ mProgramId,
+ mEventId,
+ mTitleText,
+ mStartTime,
+ mLengthInSecond,
+ mContentRating,
+ mAudioTracks != null ? mAudioTracks.size() : 0,
+ mCaptionTracks != null ? mCaptionTracks.size() : 0,
+ mBroadcastGenre,
+ mCanonicalGenre,
+ mDescription);
+ }
+ }
+
+ public static class EttItem {
+ public final int eventId;
+ public final String text;
+
+ public EttItem(int eventId, String text) {
+ this.eventId = eventId;
+ this.text = text;
+ }
+ }
+}
diff --git a/src/com/android/tv/tuner/data/TunerChannel.java b/src/com/android/tv/tuner/data/TunerChannel.java
new file mode 100644
index 00000000..52356c2e
--- /dev/null
+++ b/src/com/android/tv/tuner/data/TunerChannel.java
@@ -0,0 +1,517 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.data;
+
+import android.support.annotation.NonNull;
+import android.util.Log;
+import com.android.tv.tuner.data.nano.Channel;
+import com.android.tv.tuner.data.nano.Channel.TunerChannelProto;
+import com.android.tv.tuner.data.nano.Track.AtscAudioTrack;
+import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack;
+import com.android.tv.tuner.util.Ints;
+import com.android.tv.util.StringUtils;
+import com.google.protobuf.nano.MessageNano;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+/** A class that represents a single channel accessible through a tuner. */
+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",
+ "Analog television channels",
+ "ATSC_digital_television",
+ "ATSC_audio",
+ "ATSC_data_only_service",
+ "Software Download",
+ "Unassociated/Small Screen Service",
+ "Parameterized Service",
+ "ATSC NRT Service",
+ "Extended Parameterized Service"
+ };
+ private static final String ATSC_SERVICE_TYPE_NAME_RESERVED =
+ ATSC_SERVICE_TYPE_NAMES[Channel.SERVICE_TYPE_ATSC_RESERVED];
+
+ public static final int INVALID_FREQUENCY = -1;
+
+ // According to RFC4259, The number of available PIDs ranges from 0 to 8191.
+ public static final int INVALID_PID = -1;
+
+ // 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, List<PsiData.PmtItem> pmtItems, int type) {
+ mProto = new TunerChannelProto();
+ if (channel == null) {
+ mProto.shortName = "";
+ mProto.tsid = 0;
+ mProto.programNumber = programNumber;
+ mProto.virtualMajor = 0;
+ mProto.virtualMinor = 0;
+ } else {
+ mProto.shortName = channel.getShortName();
+ if (channel.getLongName() != null) {
+ mProto.longName = channel.getLongName();
+ }
+ mProto.tsid = channel.getChannelTsid();
+ mProto.programNumber = channel.getProgramNumber();
+ mProto.virtualMajor = channel.getMajorChannelNumber();
+ mProto.virtualMinor = channel.getMinorChannelNumber();
+ if (channel.getDescription() != null) {
+ mProto.description = channel.getDescription();
+ }
+ 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;
+ mProto.videoPid = INVALID_PID;
+ mProto.videoStreamType = INVALID_STREAMTYPE;
+ List<Integer> audioPids = new ArrayList<>();
+ List<Integer> audioStreamTypes = new ArrayList<>();
+ for (PsiData.PmtItem pmt : pmtItems) {
+ switch (pmt.getStreamType()) {
+ // MPEG ES stream video types
+ case Channel.MPEG1:
+ case Channel.MPEG2:
+ case Channel.H263:
+ case Channel.H264:
+ case Channel.H265:
+ mProto.videoPid = pmt.getEsPid();
+ mProto.videoStreamType = pmt.getStreamType();
+ break;
+
+ // MPEG ES stream audio types
+ case Channel.MPEG1AUDIO:
+ case Channel.MPEG2AUDIO:
+ case Channel.MPEG2AACAUDIO:
+ case Channel.MPEG4LATMAACAUDIO:
+ case Channel.A52AC3AUDIO:
+ case Channel.EAC3AUDIO:
+ audioPids.add(pmt.getEsPid());
+ audioStreamTypes.add(pmt.getStreamType());
+ break;
+
+ // Non MPEG ES stream types
+ case 0x100: // PmtItem.ES_PID_PCR:
+ mProto.pcrPid = pmt.getEsPid();
+ break;
+ }
+ }
+ mProto.audioPids = Ints.toArray(audioPids);
+ mProto.audioStreamTypes = Ints.toArray(audioStreamTypes);
+ 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;
+ }
+
+ public static TunerChannel forFile(PsipData.VctItem channel, List<PsiData.PmtItem> pmtItems) {
+ 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;
+ }
+
+ public String getShortName() {
+ return mProto.shortName;
+ }
+
+ public int getProgramNumber() {
+ return mProto.programNumber;
+ }
+
+ public int getServiceType() {
+ return mProto.serviceType;
+ }
+
+ public String getServiceTypeName() {
+ int serviceType = mProto.serviceType;
+ if (serviceType >= 0 && serviceType < ATSC_SERVICE_TYPE_NAMES.length) {
+ return ATSC_SERVICE_TYPE_NAMES[serviceType];
+ }
+ return ATSC_SERVICE_TYPE_NAME_RESERVED;
+ }
+
+ public int getVirtualMajor() {
+ return mProto.virtualMajor;
+ }
+
+ public int getVirtualMinor() {
+ return mProto.virtualMinor;
+ }
+
+ public int getFrequency() {
+ return mProto.frequency;
+ }
+
+ public String getModulation() {
+ return mProto.modulation;
+ }
+
+ public int getTsid() {
+ return mProto.tsid;
+ }
+
+ public int getVideoPid() {
+ return mProto.videoPid;
+ }
+
+ public synchronized void setVideoPid(int videoPid) {
+ mProto.videoPid = videoPid;
+ }
+
+ public int getVideoStreamType() {
+ return mProto.videoStreamType;
+ }
+
+ public int getAudioPid() {
+ if (mProto.audioTrackIndex == -1) {
+ return INVALID_PID;
+ }
+ return mProto.audioPids[mProto.audioTrackIndex];
+ }
+
+ public int getAudioStreamType() {
+ if (mProto.audioTrackIndex == -1) {
+ return INVALID_STREAMTYPE;
+ }
+ return mProto.audioStreamTypes[mProto.audioTrackIndex];
+ }
+
+ public List<Integer> getAudioPids() {
+ return Ints.asList(mProto.audioPids);
+ }
+
+ public synchronized void setAudioPids(List<Integer> audioPids) {
+ mProto.audioPids = Ints.toArray(audioPids);
+ }
+
+ public List<Integer> getAudioStreamTypes() {
+ return Ints.asList(mProto.audioStreamTypes);
+ }
+
+ public synchronized void setAudioStreamTypes(List<Integer> audioStreamTypes) {
+ mProto.audioStreamTypes = Ints.toArray(audioStreamTypes);
+ }
+
+ public int getPcrPid() {
+ return mProto.pcrPid;
+ }
+
+ public int getType() {
+ return mProto.type;
+ }
+
+ public synchronized void setFilepath(String filepath) {
+ mProto.filepath = filepath == null ? "" : filepath;
+ }
+
+ public String getFilepath() {
+ return mProto.filepath;
+ }
+
+ public synchronized void setVirtualMajor(int virtualMajor) {
+ mProto.virtualMajor = virtualMajor;
+ }
+
+ public synchronized void setVirtualMinor(int virtualMinor) {
+ mProto.virtualMinor = virtualMinor;
+ }
+
+ public synchronized void setShortName(String shortName) {
+ mProto.shortName = shortName == null ? "" : shortName;
+ }
+
+ public synchronized void setFrequency(int frequency) {
+ mProto.frequency = frequency;
+ }
+
+ public synchronized void setModulation(String modulation) {
+ mProto.modulation = modulation == null ? "" : modulation;
+ }
+
+ public boolean hasVideo() {
+ return mProto.videoPid != INVALID_PID;
+ }
+
+ public boolean hasAudio() {
+ return getAudioPid() != INVALID_PID;
+ }
+
+ public long getChannelId() {
+ return mProto.channelId;
+ }
+
+ public synchronized void setChannelId(long channelId) {
+ mProto.channelId = channelId;
+ }
+
+ public String getDisplayNumber() {
+ 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 {
+ return Integer.toString(mProto.programNumber);
+ }
+ }
+
+ public String getDescription() {
+ return mProto.description;
+ }
+
+ @Override
+ public synchronized void setHasCaptionTrack() {
+ mProto.hasCaptionTrack = true;
+ }
+
+ @Override
+ public boolean hasCaptionTrack() {
+ return mProto.hasCaptionTrack;
+ }
+
+ @Override
+ public List<AtscAudioTrack> getAudioTracks() {
+ return Collections.unmodifiableList(Arrays.asList(mProto.audioTracks));
+ }
+
+ public synchronized void setAudioTracks(List<AtscAudioTrack> audioTracks) {
+ mProto.audioTracks = audioTracks.toArray(new AtscAudioTrack[audioTracks.size()]);
+ }
+
+ @Override
+ public List<AtscCaptionTrack> getCaptionTracks() {
+ return Collections.unmodifiableList(Arrays.asList(mProto.captionTracks));
+ }
+
+ public synchronized void setCaptionTracks(List<AtscCaptionTrack> captionTracks) {
+ mProto.captionTracks = captionTracks.toArray(new AtscCaptionTrack[captionTracks.size()]);
+ }
+
+ public synchronized void selectAudioTrack(int index) {
+ if (0 <= index && index < mProto.audioPids.length) {
+ mProto.audioTrackIndex = index;
+ } else {
+ mProto.audioTrackIndex = -1;
+ }
+ }
+
+ public synchronized void setRecordingProhibited(boolean recordingProhibited) {
+ mProto.recordingProhibited = recordingProhibited;
+ }
+
+ public boolean isRecordingProhibited() {
+ return mProto.recordingProhibited;
+ }
+
+ public synchronized void setVideoFormat(String videoFormat) {
+ mProto.videoFormat = videoFormat == null ? "" : videoFormat;
+ }
+
+ public String getVideoFormat() {
+ return mProto.videoFormat;
+ }
+
+ @Override
+ public String toString() {
+ switch (mProto.type) {
+ case Channel.TYPE_FILE:
+ return String.format(
+ "{%d-%d %s} Filepath: %s, ProgramNumber %d",
+ mProto.virtualMajor,
+ mProto.virtualMinor,
+ mProto.shortName,
+ mProto.filepath,
+ mProto.programNumber);
+ // case Channel.TYPE_TUNER:
+ default:
+ return String.format(
+ "{%d-%d %s} Frequency: %d, ProgramNumber %d",
+ mProto.virtualMajor,
+ mProto.virtualMinor,
+ mProto.shortName,
+ mProto.frequency,
+ mProto.programNumber);
+ }
+ }
+
+ @Override
+ public int compareTo(@NonNull TunerChannel channel) {
+ // In the same frequency, the program number acts as the sub-channel number.
+ int ret = getFrequency() - channel.getFrequency();
+ if (ret != 0) {
+ return ret;
+ }
+ ret = getProgramNumber() - channel.getProgramNumber();
+ 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());
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof TunerChannel)) {
+ return false;
+ }
+ return compareTo((TunerChannel) o) == 0;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(getFrequency(), getProgramNumber(), getName(), getFilepath());
+ }
+
+ // Serialization
+ public synchronized 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) {
+ if (data == null) {
+ return null;
+ }
+ try {
+ return new TunerChannel(TunerChannelProto.parseFrom(data));
+ } catch (IOException e) {
+ Log.e(TAG, "Could not parse from byte array", e);
+ return null;
+ }
+ }
+}
diff --git a/src/com/android/tv/tuner/exoplayer/Cea708TextTrackRenderer.java b/src/com/android/tv/tuner/exoplayer/Cea708TextTrackRenderer.java
new file mode 100644
index 00000000..1f48c45b
--- /dev/null
+++ b/src/com/android/tv/tuner/exoplayer/Cea708TextTrackRenderer.java
@@ -0,0 +1,305 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.exoplayer;
+
+import android.util.Log;
+import com.android.tv.tuner.cc.Cea708Parser;
+import com.android.tv.tuner.data.Cea708Data.CaptionEvent;
+import com.google.android.exoplayer.ExoPlaybackException;
+import com.google.android.exoplayer.MediaClock;
+import com.google.android.exoplayer.MediaFormat;
+import com.google.android.exoplayer.MediaFormatHolder;
+import com.google.android.exoplayer.SampleHolder;
+import com.google.android.exoplayer.SampleSource;
+import com.google.android.exoplayer.TrackRenderer;
+import com.google.android.exoplayer.util.Assertions;
+import java.io.IOException;
+
+/** A {@link TrackRenderer} for CEA-708 textual subtitles. */
+public class Cea708TextTrackRenderer extends TrackRenderer
+ implements Cea708Parser.OnCea708ParserListener {
+ private static final String TAG = "Cea708TextTrackRenderer";
+ 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;
+
+ private final SampleSource.SampleSourceReader mSource;
+ private final SampleHolder mSampleHolder;
+ private final MediaFormatHolder mFormatHolder;
+ private int mServiceNumber;
+ private boolean mInputStreamEnded;
+ 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);
+ }
+
+ public Cea708TextTrackRenderer(SampleSource source) {
+ mSource = source.register();
+ mTrackIndex = -1;
+ mSampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_DIRECT);
+ mSampleHolder.ensureSpaceForWrite(DEFAULT_INPUT_BUFFER_SIZE);
+ mFormatHolder = new MediaFormatHolder();
+ }
+
+ @Override
+ protected MediaClock getMediaClock() {
+ return null;
+ }
+
+ private boolean handlesMimeType(String mimeType) {
+ return mimeType.equals(MpegTsSampleExtractor.MIMETYPE_TEXT_CEA_708);
+ }
+
+ @Override
+ protected boolean doPrepare(long positionUs) throws ExoPlaybackException {
+ boolean sourcePrepared = mSource.prepare(positionUs);
+ if (!sourcePrepared) {
+ return false;
+ }
+ int trackCount = mSource.getTrackCount();
+ for (int i = 0; i < trackCount; ++i) {
+ MediaFormat trackFormat = mSource.getFormat(i);
+ if (handlesMimeType(trackFormat.mimeType)) {
+ mTrackIndex = i;
+ clearDecodeState();
+ return true;
+ }
+ }
+ // TODO: Check this case. (Source do not have the proper mime type.)
+ return true;
+ }
+
+ @Override
+ protected void onEnabled(int track, long positionUs, boolean joining) {
+ Assertions.checkArgument(mTrackIndex != -1 && track == 0);
+ mSource.enable(mTrackIndex, positionUs);
+ mInputStreamEnded = false;
+ mPresentationTimeUs = positionUs;
+ mCurrentPositionUs = Long.MIN_VALUE;
+ }
+
+ @Override
+ protected void onDisabled() {
+ mSource.disable(mTrackIndex);
+ }
+
+ @Override
+ protected void onReleased() {
+ mSource.release();
+ mCea708Parser = null;
+ }
+
+ @Override
+ protected boolean isEnded() {
+ return mInputStreamEnded;
+ }
+
+ @Override
+ protected boolean isReady() {
+ // Since this track will be fed by {@link VideoTrackRenderer},
+ // it is not required to control transition between ready state and buffering state.
+ return true;
+ }
+
+ @Override
+ protected int getTrackCount() {
+ return mTrackIndex < 0 ? 0 : 1;
+ }
+
+ @Override
+ protected MediaFormat getFormat(int track) {
+ Assertions.checkArgument(mTrackIndex != -1 && track == 0);
+ return mSource.getFormat(mTrackIndex);
+ }
+
+ @Override
+ protected void maybeThrowError() throws ExoPlaybackException {
+ try {
+ mSource.maybeThrowError();
+ } catch (IOException e) {
+ throw new ExoPlaybackException(e);
+ }
+ }
+
+ @Override
+ protected void doSomeWork(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
+ try {
+ mPresentationTimeUs = positionUs;
+ if (!mInputStreamEnded) {
+ processOutput();
+ feedInputBuffer();
+ }
+ } catch (IOException e) {
+ throw new ExoPlaybackException(e);
+ }
+ }
+
+ private boolean processOutput() {
+ return !mInputStreamEnded
+ && mCea708Parser != null
+ && mCea708Parser.processClosedCaptions(mPresentationTimeUs);
+ }
+
+ private boolean feedInputBuffer() throws IOException, ExoPlaybackException {
+ if (mInputStreamEnded) {
+ return false;
+ }
+ long discontinuity = mSource.readDiscontinuity(mTrackIndex);
+ if (discontinuity != SampleSource.NO_DISCONTINUITY) {
+ if (DEBUG) {
+ Log.d(TAG, "Read discontinuity happened");
+ }
+
+ // TODO: handle input discontinuity for trickplay.
+ clearDecodeState();
+ mPresentationTimeUs = discontinuity;
+ return false;
+ }
+ mSampleHolder.data.clear();
+ mSampleHolder.size = 0;
+ int result =
+ mSource.readData(mTrackIndex, mPresentationTimeUs, mFormatHolder, mSampleHolder);
+ switch (result) {
+ case SampleSource.NOTHING_READ:
+ {
+ return false;
+ }
+ case SampleSource.FORMAT_READ:
+ {
+ if (DEBUG) {
+ Log.i(TAG, "Format was read again");
+ }
+ return true;
+ }
+ case SampleSource.END_OF_STREAM:
+ {
+ if (DEBUG) {
+ Log.i(TAG, "End of stream from SampleSource");
+ }
+ mInputStreamEnded = true;
+ return false;
+ }
+ case SampleSource.SAMPLE_READ:
+ {
+ mSampleHolder.data.flip();
+ if (mCea708Parser != null && !mRenderingDisabled) {
+ mCea708Parser.parseClosedCaption(mSampleHolder.data, mSampleHolder.timeUs);
+ }
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void clearDecodeState() {
+ mCea708Parser = new Cea708Parser();
+ mCea708Parser.setListener(this);
+ mCea708Parser.setListenServiceNumber(mServiceNumber);
+ }
+
+ @Override
+ protected long getDurationUs() {
+ return mSource.getFormat(mTrackIndex).durationUs;
+ }
+
+ @Override
+ protected long getBufferedPositionUs() {
+ return mSource.getBufferedPositionUs();
+ }
+
+ @Override
+ protected void seekTo(long currentPositionUs) throws ExoPlaybackException {
+ mSource.seekToUs(currentPositionUs);
+ mInputStreamEnded = false;
+ mPresentationTimeUs = currentPositionUs;
+ mCurrentPositionUs = Long.MIN_VALUE;
+ }
+
+ @Override
+ protected void onStarted() {
+ // do nothing.
+ }
+
+ @Override
+ protected void onStopped() {
+ // do nothing.
+ }
+
+ private void setServiceNumber(int serviceNumber) {
+ mServiceNumber = serviceNumber;
+ if (mCea708Parser != null) {
+ mCea708Parser.setListenServiceNumber(serviceNumber);
+ }
+ }
+
+ @Override
+ public void emitEvent(CaptionEvent event) {
+ if (mCcListener != null) {
+ mCcListener.emitEvent(event);
+ }
+ }
+
+ @Override
+ public void discoverServiceNumber(int serviceNumber) {
+ if (mCcListener != null) {
+ mCcListener.discoverServiceNumber(serviceNumber);
+ }
+ }
+
+ public void setCcListener(CcListener ccListener) {
+ mCcListener = ccListener;
+ }
+
+ @Override
+ public void handleMessage(int messageType, Object message) throws ExoPlaybackException {
+ 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..b5369d69
--- /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.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;
+
+/**
+ * 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
new file mode 100644
index 00000000..df520900
--- /dev/null
+++ b/src/com/android/tv/tuner/exoplayer/ExoPlayerSampleExtractor.java
@@ -0,0 +1,610 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.exoplayer;
+
+import android.net.Uri;
+import android.os.ConditionVariable;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.os.SystemClock;
+import android.util.Pair;
+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;
+import com.android.tv.tuner.tvinput.PlaybackBufferListener;
+import com.google.android.exoplayer.MediaFormat;
+import com.google.android.exoplayer.MediaFormatHolder;
+import com.google.android.exoplayer.SampleHolder;
+import com.google.android.exoplayer.upstream.DataSource;
+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 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;
+
+/**
+ * A class that extracts samples from a live broadcast stream while storing the sample on the disk.
+ * For demux, this class relies on {@link com.google.android.exoplayer.extractor.ts.TsExtractor}.
+ */
+public class ExoPlayerSampleExtractor implements SampleExtractor {
+ private static final String TAG = "ExoPlayerSampleExtracto";
+
+ private static final int INVALID_TRACK_INDEX = -1;
+ private final HandlerThread mSourceReaderThread;
+ private final long mId;
+
+ private final Handler.Callback mSourceReaderWorker;
+
+ private BufferManager.SampleBuffer mSampleBuffer;
+ private Handler mSourceReaderHandler;
+ private volatile boolean mPrepared;
+ 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,
+ final DataSource source,
+ BufferManager bufferManager,
+ PlaybackBufferListener bufferListener,
+ boolean isRecording) {
+ // It'll be used as a timeshift file chunk name's prefix.
+ mId = System.currentTimeMillis();
+
+ EventListener eventListener =
+ new EventListener() {
+ @Override
+ public void onLoadError(IOException error) {
+ mError = error;
+ }
+ };
+
+ mSourceReaderThread = new HandlerThread("SourceReaderThread");
+ 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));
+ if (isRecording) {
+ mSampleBuffer =
+ new RecordingSampleBuffer(
+ bufferManager,
+ bufferListener,
+ false,
+ RecordingSampleBuffer.BUFFER_REASON_RECORDING);
+ } else {
+ if (bufferManager == null) {
+ mSampleBuffer = new SimpleSampleBuffer(bufferListener);
+ } else {
+ mSampleBuffer =
+ new RecordingSampleBuffer(
+ bufferManager,
+ bufferListener,
+ true,
+ RecordingSampleBuffer.BUFFER_REASON_LIVE_PLAYBACK);
+ }
+ }
+ }
+
+ @Override
+ public void setOnCompletionListener(OnCompletionListener listener, Handler handler) {
+ mOnCompletionListener = listener;
+ mOnCompletionListenerHandler = handler;
+ }
+
+ private class 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 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(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:
+ 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;
+ ConditionVariable conditionVariable = new ConditionVariable();
+ int trackCount = mStreams.length;
+ for (int i = 0; i < trackCount; ++i) {
+ 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.
+ break;
+ }
+ didSomething = true;
+ }
+ }
+ mMediaPeriod.continueLoading(mCurrentPosition);
+ if (!mMetEos) {
+ if (didSomething) {
+ mSourceReaderHandler.sendEmptyMessage(MSG_FETCH_SAMPLES);
+ } else {
+ mSourceReaderHandler.sendEmptyMessageDelayed(
+ MSG_FETCH_SAMPLES, RETRY_INTERVAL_MS);
+ }
+ } else {
+ notifyCompletionIfNeeded(false);
+ }
+ return true;
+ case MSG_RELEASE:
+ if (mMediaPeriod != null) {
+ mSampleSource.releasePeriod(mMediaPeriod);
+ mSampleSource.releaseSource();
+ mMediaPeriod = null;
+ }
+ cleanUp();
+ mSourceReaderHandler.removeCallbacksAndMessages(null);
+ return true;
+ }
+ return false;
+ }
+
+ private int fetchSample(
+ int track, SampleHolder sample, ConditionVariable conditionVariable) {
+ 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, mDecoderInputBuffer.timeUs);
+ } else {
+ mLastExtractedPositionUsMap.put(
+ track,
+ Math.max(lastExtractedPositionUs, mDecoderInputBuffer.timeUs));
+ }
+ queueSample(track, conditionVariable);
+ } catch (IOException e) {
+ mLastExtractedPositionUsMap.clear();
+ mMetEos = true;
+ mSampleBuffer.setEos();
+ }
+ } 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) {
+ mMetEos = true;
+ mSampleBuffer.setEos();
+ }
+ }
+ }
+ // TODO: Handle C.RESULT_FORMAT_READ for dynamic resolution change. b/28169263
+ return ret;
+ }
+
+ 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();
+ }
+ }
+ }
+
+ @Override
+ public void maybeThrowError() throws IOException {
+ if (mError != null) {
+ IOException e = mError;
+ mError = null;
+ throw e;
+ }
+ }
+
+ @Override
+ public boolean prepare() throws IOException {
+ if (!mSourceReaderThread.isAlive()) {
+ mSourceReaderThread.start();
+ mSourceReaderHandler =
+ new Handler(mSourceReaderThread.getLooper(), mSourceReaderWorker);
+ mSourceReaderHandler.sendEmptyMessage(SourceReaderWorker.MSG_PREPARE);
+ }
+ if (mExceptionOnPrepare != null) {
+ throw mExceptionOnPrepare;
+ }
+ return mPrepared;
+ }
+
+ @Override
+ public List<MediaFormat> getTrackFormats() {
+ return mTrackFormats;
+ }
+
+ @Override
+ public void getTrackMediaFormat(int track, MediaFormatHolder outMediaFormatHolder) {
+ outMediaFormatHolder.format = mTrackFormats.get(track);
+ outMediaFormatHolder.drmInitData = null;
+ }
+
+ @Override
+ public void selectTrack(int index) {
+ mSampleBuffer.selectTrack(index);
+ }
+
+ @Override
+ public void deselectTrack(int index) {
+ mSampleBuffer.deselectTrack(index);
+ }
+
+ @Override
+ public long getBufferedPositionUs() {
+ return mSampleBuffer.getBufferedPositionUs();
+ }
+
+ @Override
+ public boolean continueBuffering(long positionUs) {
+ return mSampleBuffer.continueBuffering(positionUs);
+ }
+
+ @Override
+ public void seekTo(long positionUs) {
+ mSampleBuffer.seekTo(positionUs);
+ }
+
+ @Override
+ public int readSample(int track, SampleHolder sampleHolder) {
+ return mSampleBuffer.readSample(track, sampleHolder);
+ }
+
+ @Override
+ public void release() {
+ if (mSourceReaderThread.isAlive()) {
+ mSourceReaderHandler.removeCallbacksAndMessages(null);
+ mSourceReaderHandler.sendEmptyMessage(SourceReaderWorker.MSG_RELEASE);
+ mSourceReaderThread.quitSafely();
+ // Return early in this case so that session worker can start working on the next
+ // request as early as it can. The clean up will be done in the reader thread while
+ // handling MSG_RELEASE.
+ } else {
+ cleanUp();
+ }
+ }
+
+ private void cleanUp() {
+ boolean result = true;
+ try {
+ if (mSampleBuffer != null) {
+ mSampleBuffer.release();
+ mSampleBuffer = null;
+ }
+ } catch (IOException e) {
+ result = false;
+ }
+ notifyCompletionIfNeeded(result);
+ setOnCompletionListener(null, null);
+ }
+
+ private void notifyCompletionIfNeeded(final boolean result) {
+ if (!mOnCompletionCalled.getAndSet(true)) {
+ final OnCompletionListener listener = mOnCompletionListener;
+ final long lastExtractedPositionUs = getLastExtractedPositionUs();
+ if (mOnCompletionListenerHandler != null && mOnCompletionListener != null) {
+ mOnCompletionListenerHandler.post(
+ new Runnable() {
+ @Override
+ public void run() {
+ listener.onCompletion(result, lastExtractedPositionUs);
+ }
+ });
+ }
+ }
+ }
+
+ private long getLastExtractedPositionUs() {
+ 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.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
new file mode 100644
index 00000000..e7224422
--- /dev/null
+++ b/src/com/android/tv/tuner/exoplayer/FileSampleExtractor.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.exoplayer;
+
+import android.os.Handler;
+import com.android.tv.tuner.exoplayer.buffer.BufferManager;
+import com.android.tv.tuner.exoplayer.buffer.RecordingSampleBuffer;
+import com.android.tv.tuner.tvinput.PlaybackBufferListener;
+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 java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A class that plays a recorded stream without using {@link android.media.MediaExtractor}, since
+ * all samples are extracted and stored to the permanent storage already.
+ */
+public class FileSampleExtractor implements SampleExtractor {
+ private static final String TAG = "FileSampleExtractor";
+ private static final boolean DEBUG = false;
+
+ private int mTrackCount;
+ private boolean mReleased;
+
+ private final List<MediaFormat> mTrackFormats = new ArrayList<>();
+ private final BufferManager mBufferManager;
+ private final PlaybackBufferListener mBufferListener;
+ private BufferManager.SampleBuffer mSampleBuffer;
+
+ public FileSampleExtractor(BufferManager bufferManager, PlaybackBufferListener bufferListener) {
+ mBufferManager = bufferManager;
+ mBufferListener = bufferListener;
+ mTrackCount = -1;
+ }
+
+ @Override
+ public void maybeThrowError() throws IOException {
+ // Do nothing.
+ }
+
+ @Override
+ public boolean prepare() throws IOException {
+ List<BufferManager.TrackFormat> trackFormatList = mBufferManager.readTrackInfoFiles();
+ if (trackFormatList == null || trackFormatList.isEmpty()) {
+ throw new IOException("Cannot find meta files for the recording.");
+ }
+ mTrackCount = trackFormatList.size();
+ List<String> ids = new ArrayList<>();
+ mTrackFormats.clear();
+ for (int i = 0; i < mTrackCount; ++i) {
+ 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);
+ mSampleBuffer.init(ids, mTrackFormats);
+ return true;
+ }
+
+ @Override
+ public List<MediaFormat> getTrackFormats() {
+ return mTrackFormats;
+ }
+
+ @Override
+ public void getTrackMediaFormat(int track, MediaFormatHolder outMediaFormatHolder) {
+ outMediaFormatHolder.format = mTrackFormats.get(track);
+ outMediaFormatHolder.drmInitData = null;
+ }
+
+ @Override
+ public void release() {
+ if (!mReleased) {
+ if (mSampleBuffer != null) {
+ try {
+ mSampleBuffer.release();
+ } catch (IOException e) {
+ // Do nothing. Playback ends now.
+ }
+ }
+ }
+ mReleased = true;
+ }
+
+ @Override
+ public void selectTrack(int index) {
+ mSampleBuffer.selectTrack(index);
+ }
+
+ @Override
+ public void deselectTrack(int index) {
+ mSampleBuffer.deselectTrack(index);
+ }
+
+ @Override
+ public long getBufferedPositionUs() {
+ return mSampleBuffer.getBufferedPositionUs();
+ }
+
+ @Override
+ public void seekTo(long positionUs) {
+ mSampleBuffer.seekTo(positionUs);
+ }
+
+ @Override
+ public int readSample(int track, SampleHolder sampleHolder) {
+ return mSampleBuffer.readSample(track, sampleHolder);
+ }
+
+ @Override
+ public boolean continueBuffering(long positionUs) {
+ return mSampleBuffer.continueBuffering(positionUs);
+ }
+
+ @Override
+ public void setOnCompletionListener(OnCompletionListener listener, Handler handler) {}
+}
diff --git a/src/com/android/tv/tuner/exoplayer/MpegTsPlayer.java b/src/com/android/tv/tuner/exoplayer/MpegTsPlayer.java
new file mode 100644
index 00000000..a49cbfaf
--- /dev/null
+++ b/src/com/android/tv/tuner/exoplayer/MpegTsPlayer.java
@@ -0,0 +1,672 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.exoplayer;
+
+import android.content.Context;
+import android.media.AudioFormat;
+import android.media.MediaCodec.CryptoException;
+import android.media.PlaybackParams;
+import android.os.Handler;
+import android.support.annotation.IntDef;
+import android.view.Surface;
+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.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 com.google.android.exoplayer.DummyTrackRenderer;
+import com.google.android.exoplayer.ExoPlaybackException;
+import com.google.android.exoplayer.ExoPlayer;
+import com.google.android.exoplayer.MediaCodecAudioTrackRenderer;
+import com.google.android.exoplayer.MediaCodecTrackRenderer.DecoderInitializationException;
+import com.google.android.exoplayer.MediaCodecVideoTrackRenderer;
+import com.google.android.exoplayer.MediaFormat;
+import com.google.android.exoplayer.TrackRenderer;
+import com.google.android.exoplayer.audio.AudioCapabilities;
+import com.google.android.exoplayer.audio.AudioTrack;
+import com.google.android.exoplayer.upstream.DataSource;
+import 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,
+ MpegTsDefaultAudioTrackRenderer.EventListener,
+ MpegTsMediaCodecAudioTrackRenderer.Ac3EventListener {
+ private int mCaptionServiceNumber = Cea708Data.EMPTY_SERVICE_NUMBER;
+
+ /** Interface definition for building specific track renderers. */
+ public interface RendererBuilder {
+ void buildRenderers(
+ MpegTsPlayer mpegTsPlayer,
+ DataSource dataSource,
+ boolean hasSoftwareAudioDecoder,
+ RendererBuilderCallback callback);
+ }
+
+ /** Interface definition for {@link RendererBuilder#buildRenderers} to notify the result. */
+ public interface RendererBuilderCallback {
+ void onRenderers(String[][] trackNames, TrackRenderer[] renderers);
+
+ void onRenderersError(Exception e);
+ }
+
+ /** Interface definition for a callback to be notified of changes in player state. */
+ public interface Listener {
+ void onStateChanged(boolean playWhenReady, int playbackState);
+
+ void onError(Exception e);
+
+ void onVideoSizeChanged(int width, int height, float pixelWidthHeightRatio);
+
+ void onDrawnToSurface(MpegTsPlayer player, Surface surface);
+
+ void onAudioUnplayable();
+
+ void onSmoothTrickplayForceStopped();
+ }
+
+ /** Interface definition for a callback to be notified of changes on video display. */
+ public interface VideoEventListener {
+ /** Notifies the caption event. */
+ void onEmitCaptionEvent(CaptionEvent event);
+
+ /** Notifies clearing up whole closed caption event. */
+ void onClearCaptionEvent();
+
+ /** Notifies the discovered caption service number. */
+ void onDiscoverCaptionServiceNumber(int serviceNumber);
+ }
+
+ public static final int RENDERER_COUNT = 3;
+ public static final int MIN_BUFFER_MS = 0;
+ public static final int MIN_REBUFFER_MS = 500;
+
+ @IntDef({TRACK_TYPE_VIDEO, TRACK_TYPE_AUDIO, TRACK_TYPE_TEXT})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface TrackType {}
+
+ public static final int TRACK_TYPE_VIDEO = 0;
+ public static final int TRACK_TYPE_AUDIO = 1;
+ public static final int TRACK_TYPE_TEXT = 2;
+
+ @IntDef({
+ RENDERER_BUILDING_STATE_IDLE,
+ RENDERER_BUILDING_STATE_BUILDING,
+ RENDERER_BUILDING_STATE_BUILT
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface RendererBuildingState {}
+
+ private static final int RENDERER_BUILDING_STATE_IDLE = 1;
+ private static final int RENDERER_BUILDING_STATE_BUILDING = 2;
+ private static final int RENDERER_BUILDING_STATE_BUILT = 3;
+
+ private static final float MAX_SMOOTH_TRICKPLAY_SPEED = 9.0f;
+ private static final float MIN_SMOOTH_TRICKPLAY_SPEED = 0.1f;
+
+ private final RendererBuilder mRendererBuilder;
+ private final ExoPlayer mPlayer;
+ private final Handler mMainHandler;
+ private final AudioCapabilities mAudioCapabilities;
+ private final TsDataSourceManager mSourceManager;
+
+ private Listener mListener;
+ @RendererBuildingState private int mRendererBuildingState;
+
+ private Surface mSurface;
+ private TsDataSource mDataSource;
+ private InternalRendererBuilderCallback mBuilderCallback;
+ private TrackRenderer mVideoRenderer;
+ private TrackRenderer mAudioRenderer;
+ private Cea708TextTrackRenderer mTextRenderer;
+ private final Cea708TextTrackRenderer.CcListener mCcListener;
+ private VideoEventListener mVideoEventListener;
+ private boolean mTrickplayRunning;
+ private float mVolume;
+
+ /**
+ * Creates MPEG2-TS stream player.
+ *
+ * @param rendererBuilder the builder of track renderers
+ * @param handler the handler for the playback events in track renderers
+ * @param sourceManager the manager for {@link DataSource}
+ * @param capabilities the {@link AudioCapabilities} of the current device
+ * @param listener the listener for playback state changes
+ */
+ public MpegTsPlayer(
+ RendererBuilder rendererBuilder,
+ Handler handler,
+ TsDataSourceManager sourceManager,
+ AudioCapabilities capabilities,
+ Listener listener) {
+ mRendererBuilder = rendererBuilder;
+ mPlayer = ExoPlayer.Factory.newInstance(RENDERER_COUNT, MIN_BUFFER_MS, MIN_REBUFFER_MS);
+ mPlayer.addListener(this);
+ mMainHandler = handler;
+ mAudioCapabilities = capabilities;
+ mRendererBuildingState = RENDERER_BUILDING_STATE_IDLE;
+ mCcListener = new MpegTsCcListener();
+ mSourceManager = sourceManager;
+ mListener = listener;
+ }
+
+ /**
+ * Sets the video event listener.
+ *
+ * @param videoEventListener the listener for video events
+ */
+ public void setVideoEventListener(VideoEventListener videoEventListener) {
+ mVideoEventListener = videoEventListener;
+ }
+
+ /**
+ * Sets the closed caption service number.
+ *
+ * @param captionServiceNumber the service number of CEA-708 closed caption
+ */
+ public void setCaptionServiceNumber(int captionServiceNumber) {
+ mCaptionServiceNumber = captionServiceNumber;
+ if (mTextRenderer != null) {
+ mPlayer.sendMessage(
+ mTextRenderer,
+ Cea708TextTrackRenderer.MSG_SERVICE_NUMBER,
+ mCaptionServiceNumber);
+ }
+ }
+
+ /**
+ * Sets the surface for the player.
+ *
+ * @param surface the {@link Surface} to render video
+ */
+ public void setSurface(Surface surface) {
+ mSurface = surface;
+ pushSurface(false);
+ }
+
+ /** Returns the current surface of the player. */
+ public Surface getSurface() {
+ return mSurface;
+ }
+
+ /** Clears the surface and waits until the surface is being cleaned. */
+ public void blockingClearSurface() {
+ mSurface = null;
+ pushSurface(true);
+ }
+
+ /**
+ * 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,
+ boolean hasSoftwareAudioDecoder,
+ EventDetector.EventListener eventListener) {
+ TsDataSource source = null;
+ if (channel != null) {
+ source = mSourceManager.createDataSource(context, channel, eventListener);
+ if (source == null) {
+ return false;
+ }
+ }
+ mDataSource = source;
+ if (mRendererBuildingState == RENDERER_BUILDING_STATE_BUILT) {
+ mPlayer.stop();
+ }
+ if (mBuilderCallback != null) {
+ mBuilderCallback.cancel();
+ }
+ mRendererBuildingState = RENDERER_BUILDING_STATE_BUILDING;
+ mBuilderCallback = new InternalRendererBuilderCallback();
+ mRendererBuilder.buildRenderers(this, source, hasSoftwareAudioDecoder, mBuilderCallback);
+ return true;
+ }
+
+ /** Returns {@link TsDataSource} which provides MPEG2-TS stream. */
+ public TsDataSource getDataSource() {
+ return mDataSource;
+ }
+
+ private void onRenderers(TrackRenderer[] renderers) {
+ mBuilderCallback = null;
+ for (int i = 0; i < RENDERER_COUNT; i++) {
+ if (renderers[i] == null) {
+ // Convert a null renderer to a dummy renderer.
+ renderers[i] = new DummyTrackRenderer();
+ }
+ }
+ mVideoRenderer = renderers[TRACK_TYPE_VIDEO];
+ mAudioRenderer = renderers[TRACK_TYPE_AUDIO];
+ mTextRenderer = (Cea708TextTrackRenderer) renderers[TRACK_TYPE_TEXT];
+ mTextRenderer.setCcListener(mCcListener);
+ mPlayer.sendMessage(
+ mTextRenderer, Cea708TextTrackRenderer.MSG_SERVICE_NUMBER, mCaptionServiceNumber);
+ mRendererBuildingState = RENDERER_BUILDING_STATE_BUILT;
+ pushSurface(false);
+ mPlayer.prepare(renderers);
+ pushTrackSelection(TRACK_TYPE_VIDEO, true);
+ pushTrackSelection(TRACK_TYPE_AUDIO, true);
+ pushTrackSelection(TRACK_TYPE_TEXT, true);
+ }
+
+ private void onRenderersError(Exception e) {
+ mBuilderCallback = null;
+ mRendererBuildingState = RENDERER_BUILDING_STATE_IDLE;
+ if (mListener != null) {
+ mListener.onError(e);
+ }
+ }
+
+ /**
+ * Sets the player state to pause or play.
+ *
+ * @param playWhenReady sets the player state to being ready to play when {@code true}, sets the
+ * player state to being paused when {@code false}
+ */
+ public void setPlayWhenReady(boolean playWhenReady) {
+ mPlayer.setPlayWhenReady(playWhenReady);
+ stopSmoothTrickplay(false);
+ }
+
+ /** Returns true, if trickplay is supported. */
+ public boolean supportSmoothTrickPlay(float playbackSpeed) {
+ return playbackSpeed > MIN_SMOOTH_TRICKPLAY_SPEED
+ && playbackSpeed < MAX_SMOOTH_TRICKPLAY_SPEED;
+ }
+
+ /**
+ * Starts trickplay. It'll be reset, if {@link #seekTo} or {@link #setPlayWhenReady} is called.
+ */
+ public void startSmoothTrickplay(PlaybackParams playbackParams) {
+ SoftPreconditions.checkState(supportSmoothTrickPlay(playbackParams.getSpeed()));
+ mPlayer.setPlayWhenReady(true);
+ mTrickplayRunning = true;
+ if (mAudioRenderer instanceof MpegTsDefaultAudioTrackRenderer) {
+ mPlayer.sendMessage(
+ mAudioRenderer,
+ MpegTsDefaultAudioTrackRenderer.MSG_SET_PLAYBACK_SPEED,
+ playbackParams.getSpeed());
+ } else {
+ mPlayer.sendMessage(
+ mAudioRenderer,
+ MediaCodecAudioTrackRenderer.MSG_SET_PLAYBACK_PARAMS,
+ playbackParams);
+ }
+ }
+
+ private void stopSmoothTrickplay(boolean calledBySeek) {
+ if (mTrickplayRunning) {
+ mTrickplayRunning = false;
+ if (mAudioRenderer instanceof MpegTsDefaultAudioTrackRenderer) {
+ mPlayer.sendMessage(
+ mAudioRenderer,
+ MpegTsDefaultAudioTrackRenderer.MSG_SET_PLAYBACK_SPEED,
+ 1.0f);
+ } else {
+ mPlayer.sendMessage(
+ mAudioRenderer,
+ MediaCodecAudioTrackRenderer.MSG_SET_PLAYBACK_PARAMS,
+ new PlaybackParams().setSpeed(1.0f));
+ }
+ if (!calledBySeek) {
+ mPlayer.seekTo(mPlayer.getCurrentPosition());
+ }
+ }
+ }
+
+ /**
+ * Seeks to the specified position of the current playback.
+ *
+ * @param positionMs the specified position in milli seconds.
+ */
+ public void seekTo(long positionMs) {
+ mPlayer.seekTo(positionMs);
+ stopSmoothTrickplay(true);
+ }
+
+ /** Releases the player. */
+ public void release() {
+ if (mDataSource != null) {
+ mSourceManager.releaseDataSource(mDataSource);
+ mDataSource = null;
+ }
+ if (mBuilderCallback != null) {
+ mBuilderCallback.cancel();
+ mBuilderCallback = null;
+ }
+ mRendererBuildingState = RENDERER_BUILDING_STATE_IDLE;
+ mSurface = null;
+ mListener = null;
+ mPlayer.release();
+ }
+
+ /** Returns the current status of the player. */
+ public int getPlaybackState() {
+ if (mRendererBuildingState == RENDERER_BUILDING_STATE_BUILDING) {
+ return ExoPlayer.STATE_PREPARING;
+ }
+ return mPlayer.getPlaybackState();
+ }
+
+ /** Returns {@code true} when the player is prepared to play, {@code false} otherwise. */
+ public boolean isPrepared() {
+ int state = getPlaybackState();
+ return state == ExoPlayer.STATE_READY || state == ExoPlayer.STATE_BUFFERING;
+ }
+
+ /** Returns {@code true} when the player is being ready to play, {@code false} otherwise. */
+ public boolean isPlaying() {
+ int state = getPlaybackState();
+ return (state == ExoPlayer.STATE_READY || state == ExoPlayer.STATE_BUFFERING)
+ && mPlayer.getPlayWhenReady();
+ }
+
+ /** Returns {@code true} when the player is buffering, {@code false} otherwise. */
+ public boolean isBuffering() {
+ return getPlaybackState() == ExoPlayer.STATE_BUFFERING;
+ }
+
+ /** Returns the current position of the playback in milli seconds. */
+ public long getCurrentPosition() {
+ return mPlayer.getCurrentPosition();
+ }
+
+ /** Returns the total duration of the playback. */
+ public long getDuration() {
+ return mPlayer.getDuration();
+ }
+
+ /**
+ * Returns {@code true} when the player is being ready to play, {@code false} when the player is
+ * paused.
+ */
+ public boolean getPlayWhenReady() {
+ return mPlayer.getPlayWhenReady();
+ }
+
+ /**
+ * Sets the volume of the audio.
+ *
+ * @param volume see also {@link AudioTrack#setVolume(float)}
+ */
+ public void setVolume(float volume) {
+ mVolume = volume;
+ if (mAudioRenderer instanceof MpegTsDefaultAudioTrackRenderer) {
+ mPlayer.sendMessage(
+ mAudioRenderer, MpegTsDefaultAudioTrackRenderer.MSG_SET_VOLUME, volume);
+ } else {
+ mPlayer.sendMessage(
+ mAudioRenderer, MediaCodecAudioTrackRenderer.MSG_SET_VOLUME, volume);
+ }
+ }
+
+ /**
+ * Enables or disables audio and closed caption.
+ *
+ * @param enable enables the audio and closed caption when {@code true}, disables otherwise.
+ */
+ 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);
+ }
+
+ /** Returns {@code true} when AC3 audio can be played, {@code false} otherwise. */
+ public boolean isAc3Playable() {
+ return mAudioCapabilities != null
+ && mAudioCapabilities.supportsEncoding(AudioFormat.ENCODING_AC3);
+ }
+
+ /** Notifies when the audio cannot be played by the current device. */
+ public void onAudioUnplayable() {
+ if (mListener != null) {
+ mListener.onAudioUnplayable();
+ }
+ }
+
+ /** Returns {@code true} if the player has any video track, {@code false} otherwise. */
+ public boolean hasVideo() {
+ return mPlayer.getTrackCount(TRACK_TYPE_VIDEO) > 0;
+ }
+
+ /** Returns {@code true} if the player has any audio trock, {@code false} otherwise. */
+ public boolean hasAudio() {
+ return mPlayer.getTrackCount(TRACK_TYPE_AUDIO) > 0;
+ }
+
+ /** Returns the number of tracks exposed by the specified renderer. */
+ public int getTrackCount(int rendererIndex) {
+ return mPlayer.getTrackCount(rendererIndex);
+ }
+
+ /** Selects a track for the specified renderer. */
+ public void setSelectedTrack(int rendererIndex, int trackIndex) {
+ if (trackIndex >= getTrackCount(rendererIndex)) {
+ return;
+ }
+ mPlayer.setSelectedTrack(rendererIndex, trackIndex);
+ }
+
+ /**
+ * 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() {
+ return mMainHandler;
+ }
+
+ @Override
+ public void onPlayerStateChanged(boolean playWhenReady, int state) {
+ if (mListener == null) {
+ return;
+ }
+ mListener.onStateChanged(playWhenReady, state);
+ if (state == ExoPlayer.STATE_READY
+ && mPlayer.getTrackCount(TRACK_TYPE_VIDEO) > 0
+ && playWhenReady) {
+ MediaFormat format = mPlayer.getTrackFormat(TRACK_TYPE_VIDEO, 0);
+ mListener.onVideoSizeChanged(format.width, format.height, format.pixelWidthHeightRatio);
+ }
+ }
+
+ @Override
+ public void onPlayerError(ExoPlaybackException exception) {
+ mRendererBuildingState = RENDERER_BUILDING_STATE_IDLE;
+ if (mListener != null) {
+ mListener.onError(exception);
+ }
+ }
+
+ @Override
+ public void onVideoSizeChanged(
+ int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) {
+ if (mListener != null) {
+ mListener.onVideoSizeChanged(width, height, pixelWidthHeightRatio);
+ }
+ }
+
+ @Override
+ public void onDecoderInitialized(
+ String decoderName, long elapsedRealtimeMs, long initializationDurationMs) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onDecoderInitializationError(DecoderInitializationException e) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onAudioTrackInitializationError(AudioTrack.InitializationException e) {
+ if (mListener != null) {
+ mListener.onAudioUnplayable();
+ }
+ }
+
+ @Override
+ public void onAudioTrackWriteError(AudioTrack.WriteException e) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onAudioTrackUnderrun(
+ int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onCryptoError(CryptoException e) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onPlayWhenReadyCommitted() {
+ // Do nothing.
+ }
+
+ @Override
+ public void onDrawnToSurface(Surface surface) {
+ if (mListener != null) {
+ mListener.onDrawnToSurface(this, surface);
+ }
+ }
+
+ @Override
+ public void onDroppedFrames(int count, long elapsed) {
+ TunerDebug.notifyVideoFrameDrop(count, elapsed);
+ if (mTrickplayRunning && mListener != null) {
+ mListener.onSmoothTrickplayForceStopped();
+ }
+ }
+
+ @Override
+ public void onAudioTrackSetPlaybackParamsError(IllegalArgumentException e) {
+ if (mTrickplayRunning && mListener != null) {
+ mListener.onSmoothTrickplayForceStopped();
+ }
+ }
+
+ private void pushSurface(boolean blockForSurfacePush) {
+ if (mRendererBuildingState != RENDERER_BUILDING_STATE_BUILT) {
+ return;
+ }
+
+ if (blockForSurfacePush) {
+ mPlayer.blockingSendMessage(
+ mVideoRenderer, MediaCodecVideoTrackRenderer.MSG_SET_SURFACE, mSurface);
+ } else {
+ mPlayer.sendMessage(
+ mVideoRenderer, MediaCodecVideoTrackRenderer.MSG_SET_SURFACE, mSurface);
+ }
+ }
+
+ private void pushTrackSelection(@TrackType int type, boolean allowRendererEnable) {
+ if (mRendererBuildingState != RENDERER_BUILDING_STATE_BUILT) {
+ return;
+ }
+ mPlayer.setSelectedTrack(type, allowRendererEnable ? 0 : -1);
+ }
+
+ private class MpegTsCcListener implements Cea708TextTrackRenderer.CcListener {
+
+ @Override
+ public void emitEvent(CaptionEvent captionEvent) {
+ if (mVideoEventListener != null) {
+ mVideoEventListener.onEmitCaptionEvent(captionEvent);
+ }
+ }
+
+ @Override
+ public void clearCaption() {
+ if (mVideoEventListener != null) {
+ mVideoEventListener.onClearCaptionEvent();
+ }
+ }
+
+ @Override
+ public void discoverServiceNumber(int serviceNumber) {
+ if (mVideoEventListener != null) {
+ mVideoEventListener.onDiscoverCaptionServiceNumber(serviceNumber);
+ }
+ }
+ }
+
+ private class InternalRendererBuilderCallback implements RendererBuilderCallback {
+ private boolean canceled;
+
+ public void cancel() {
+ canceled = true;
+ }
+
+ @Override
+ public void onRenderers(String[][] trackNames, TrackRenderer[] renderers) {
+ if (!canceled) {
+ MpegTsPlayer.this.onRenderers(renderers);
+ }
+ }
+
+ @Override
+ public void onRenderersError(Exception e) {
+ if (!canceled) {
+ MpegTsPlayer.this.onRenderersError(e);
+ }
+ }
+ }
+}
diff --git a/src/com/android/tv/tuner/exoplayer/MpegTsRendererBuilder.java b/src/com/android/tv/tuner/exoplayer/MpegTsRendererBuilder.java
new file mode 100644
index 00000000..c7f5b333
--- /dev/null
+++ b/src/com/android/tv/tuner/exoplayer/MpegTsRendererBuilder.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.exoplayer;
+
+import android.content.Context;
+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.audio.MpegTsDefaultAudioTrackRenderer;
+import com.android.tv.tuner.exoplayer.buffer.BufferManager;
+import com.android.tv.tuner.tvinput.PlaybackBufferListener;
+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;
+
+/** Builder for renderer objects for {@link MpegTsPlayer}. */
+public class MpegTsRendererBuilder implements RendererBuilder {
+ private final Context mContext;
+ private final BufferManager mBufferManager;
+ private final PlaybackBufferListener mBufferListener;
+
+ public MpegTsRendererBuilder(
+ Context context, BufferManager bufferManager, PlaybackBufferListener bufferListener) {
+ mContext = context;
+ mBufferManager = bufferManager;
+ mBufferListener = bufferListener;
+ }
+
+ @Override
+ public void buildRenderers(
+ MpegTsPlayer mpegTsPlayer,
+ DataSource dataSource,
+ boolean mHasSoftwareAudioDecoder,
+ RendererBuilderCallback callback) {
+ // Build the video and audio renderers.
+ SampleExtractor extractor =
+ dataSource == null
+ ? new MpegTsSampleExtractor(mBufferManager, mBufferListener)
+ : new MpegTsSampleExtractor(dataSource, mBufferManager, mBufferListener);
+ SampleSource sampleSource = new MpegTsSampleSource(extractor);
+ MpegTsVideoTrackRenderer videoRenderer =
+ new MpegTsVideoTrackRenderer(
+ mContext, 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];
+ renderers[MpegTsPlayer.TRACK_TYPE_VIDEO] = videoRenderer;
+ renderers[MpegTsPlayer.TRACK_TYPE_AUDIO] = audioRenderer;
+ renderers[MpegTsPlayer.TRACK_TYPE_TEXT] = textRenderer;
+ callback.onRenderers(null, renderers);
+ }
+}
diff --git a/src/com/android/tv/tuner/exoplayer/MpegTsSampleExtractor.java b/src/com/android/tv/tuner/exoplayer/MpegTsSampleExtractor.java
new file mode 100644
index 00000000..593b576e
--- /dev/null
+++ b/src/com/android/tv/tuner/exoplayer/MpegTsSampleExtractor.java
@@ -0,0 +1,345 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.exoplayer;
+
+import android.net.Uri;
+import android.os.Handler;
+import com.android.tv.tuner.exoplayer.buffer.BufferManager;
+import com.android.tv.tuner.exoplayer.buffer.SamplePool;
+import com.android.tv.tuner.tvinput.PlaybackBufferListener;
+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.upstream.DataSource;
+import com.google.android.exoplayer.util.MimeTypes;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+
+/** Extracts samples from {@link DataSource} for MPEG-TS streams. */
+public final class MpegTsSampleExtractor implements SampleExtractor {
+ public static final String MIMETYPE_TEXT_CEA_708 = "text/cea-708";
+
+ private static final int CC_BUFFER_SIZE_IN_BYTES = 9600 / 8;
+
+ private final SampleExtractor mSampleExtractor;
+ private final List<MediaFormat> mTrackFormats = new ArrayList<>();
+ private final List<Boolean> mReachedEos = new ArrayList<>();
+ private int mVideoTrackIndex;
+ private final SamplePool mCcSamplePool = new SamplePool();
+ private final List<SampleHolder> mPendingCcSamples = new LinkedList<>();
+
+ private int mCea708TextTrackIndex;
+ private boolean mCea708TextTrackSelected;
+
+ private CcParser mCcParser;
+
+ private void init() {
+ mVideoTrackIndex = -1;
+ mCea708TextTrackIndex = -1;
+ mCea708TextTrackSelected = false;
+ }
+
+ /**
+ * Creates MpegTsSampleExtractor for {@link DataSource}.
+ *
+ * @param source the {@link DataSource} to extract from
+ * @param bufferManager the manager for reading & writing samples backed by physical storage
+ * @param bufferListener the {@link PlaybackBufferListener} to notify buffer storage status
+ * change
+ */
+ public MpegTsSampleExtractor(
+ DataSource source, BufferManager bufferManager, PlaybackBufferListener bufferListener) {
+ mSampleExtractor =
+ new ExoPlayerSampleExtractor(
+ Uri.EMPTY, source, bufferManager, bufferListener, false);
+ init();
+ }
+
+ /**
+ * Creates MpegTsSampleExtractor for a recorded program.
+ *
+ * @param bufferManager the samples provider which is stored in physical storage
+ * @param bufferListener the {@link PlaybackBufferListener} to notify buffer storage status
+ * change
+ */
+ public MpegTsSampleExtractor(
+ BufferManager bufferManager, PlaybackBufferListener bufferListener) {
+ mSampleExtractor = new FileSampleExtractor(bufferManager, bufferListener);
+ init();
+ }
+
+ @Override
+ public void maybeThrowError() throws IOException {
+ if (mSampleExtractor != null) {
+ mSampleExtractor.maybeThrowError();
+ }
+ }
+
+ @Override
+ public boolean prepare() throws IOException {
+ if (!mSampleExtractor.prepare()) {
+ return false;
+ }
+ List<MediaFormat> formats = mSampleExtractor.getTrackFormats();
+ int trackCount = formats.size();
+ mTrackFormats.clear();
+ mReachedEos.clear();
+
+ for (int i = 0; i < trackCount; ++i) {
+ mTrackFormats.add(formats.get(i));
+ mReachedEos.add(false);
+ String mime = formats.get(i).mimeType;
+ if (MimeTypes.isVideo(mime) && mVideoTrackIndex == -1) {
+ mVideoTrackIndex = i;
+ if (android.media.MediaFormat.MIMETYPE_VIDEO_MPEG2.equals(mime)) {
+ mCcParser = new Mpeg2CcParser();
+ } else if (android.media.MediaFormat.MIMETYPE_VIDEO_AVC.equals(mime)) {
+ mCcParser = new H264CcParser();
+ }
+ }
+ }
+
+ if (mVideoTrackIndex != -1) {
+ mCea708TextTrackIndex = trackCount;
+ }
+ if (mCea708TextTrackIndex >= 0) {
+ mTrackFormats.add(
+ MediaFormat.createTextFormat(
+ null, MIMETYPE_TEXT_CEA_708, 0, mTrackFormats.get(0).durationUs, ""));
+ }
+ return true;
+ }
+
+ @Override
+ public List<MediaFormat> getTrackFormats() {
+ return mTrackFormats;
+ }
+
+ @Override
+ public void selectTrack(int index) {
+ if (index == mCea708TextTrackIndex) {
+ mCea708TextTrackSelected = true;
+ return;
+ }
+ mSampleExtractor.selectTrack(index);
+ }
+
+ @Override
+ public void deselectTrack(int index) {
+ if (index == mCea708TextTrackIndex) {
+ mCea708TextTrackSelected = false;
+ return;
+ }
+ mSampleExtractor.deselectTrack(index);
+ }
+
+ @Override
+ public long getBufferedPositionUs() {
+ return mSampleExtractor.getBufferedPositionUs();
+ }
+
+ @Override
+ public void seekTo(long positionUs) {
+ mSampleExtractor.seekTo(positionUs);
+ for (SampleHolder holder : mPendingCcSamples) {
+ mCcSamplePool.releaseSample(holder);
+ }
+ mPendingCcSamples.clear();
+ }
+
+ @Override
+ public void getTrackMediaFormat(int track, MediaFormatHolder outMediaFormatHolder) {
+ if (track != mCea708TextTrackIndex) {
+ mSampleExtractor.getTrackMediaFormat(track, outMediaFormatHolder);
+ }
+ }
+
+ @Override
+ public int readSample(int track, SampleHolder sampleHolder) {
+ if (track == mCea708TextTrackIndex) {
+ if (mCea708TextTrackSelected && !mPendingCcSamples.isEmpty()) {
+ SampleHolder holder = mPendingCcSamples.remove(0);
+ holder.data.flip();
+ sampleHolder.timeUs = holder.timeUs;
+ sampleHolder.data.put(holder.data);
+ mCcSamplePool.releaseSample(holder);
+ return SampleSource.SAMPLE_READ;
+ } else {
+ return mVideoTrackIndex < 0 || mReachedEos.get(mVideoTrackIndex)
+ ? SampleSource.END_OF_STREAM
+ : SampleSource.NOTHING_READ;
+ }
+ }
+
+ int result = mSampleExtractor.readSample(track, sampleHolder);
+ switch (result) {
+ case SampleSource.END_OF_STREAM:
+ {
+ mReachedEos.set(track, true);
+ break;
+ }
+ case SampleSource.SAMPLE_READ:
+ {
+ if (mCea708TextTrackSelected
+ && track == mVideoTrackIndex
+ && sampleHolder.data != null) {
+ mCcParser.mayParseClosedCaption(sampleHolder.data, sampleHolder.timeUs);
+ }
+ break;
+ }
+ }
+ return result;
+ }
+
+ @Override
+ public void release() {
+ mSampleExtractor.release();
+ mVideoTrackIndex = -1;
+ mCea708TextTrackIndex = -1;
+ mCea708TextTrackSelected = false;
+ }
+
+ @Override
+ public boolean continueBuffering(long positionUs) {
+ return mSampleExtractor.continueBuffering(positionUs);
+ }
+
+ @Override
+ public void setOnCompletionListener(OnCompletionListener listener, Handler handler) {}
+
+ private abstract class CcParser {
+ // Interim buffer for reduce direct access to ByteBuffer which is expensive. Using
+ // relatively small buffer size in order to minimize memory footprint increase.
+ protected final byte[] mBuffer = new byte[1024];
+
+ abstract void mayParseClosedCaption(ByteBuffer buffer, long presentationTimeUs);
+
+ protected int parseClosedCaption(ByteBuffer buffer, int offset, long presentationTimeUs) {
+ // For the details of user_data_type_structure, see ATSC A/53 Part 4 - Table 6.9.
+ int pos = offset;
+ if (pos + 2 >= buffer.position()) {
+ return offset;
+ }
+ boolean processCcDataFlag = (buffer.get(pos) & 64) != 0;
+ int ccCount = buffer.get(pos) & 0x1f;
+ pos += 2;
+ if (!processCcDataFlag || pos + 3 * ccCount >= buffer.position() || ccCount == 0) {
+ return offset;
+ }
+ SampleHolder holder = mCcSamplePool.acquireSample(CC_BUFFER_SIZE_IN_BYTES);
+ for (int i = 0; i < 3 * ccCount; i++) {
+ holder.data.put(buffer.get(pos++));
+ }
+ holder.timeUs = presentationTimeUs;
+ mPendingCcSamples.add(holder);
+ return pos;
+ }
+ }
+
+ private class Mpeg2CcParser extends CcParser {
+ private static final int PATTERN_LENGTH = 9;
+
+ @Override
+ public void mayParseClosedCaption(ByteBuffer buffer, long presentationTimeUs) {
+ int totalSize = buffer.position();
+ // Reading the frame in bulk to reduce the overhead from ByteBuffer.get() with
+ // overlapping to handle the case that the pattern exists in the boundary.
+ for (int i = 0; i < totalSize; i += mBuffer.length - PATTERN_LENGTH) {
+ buffer.position(i);
+ int size = Math.min(totalSize - i, mBuffer.length);
+ buffer.get(mBuffer, 0, size);
+ int j = 0;
+ while (j < size - PATTERN_LENGTH) {
+ // Find the start prefix code of private user data.
+ if (mBuffer[j] == 0
+ && mBuffer[j + 1] == 0
+ && mBuffer[j + 2] == 1
+ && (mBuffer[j + 3] & 0xff) == 0xb2) {
+ // ATSC closed caption data embedded in MPEG2VIDEO stream has 'GA94' user
+ // identifier and user data type code 3.
+ if (mBuffer[j + 4] == 'G'
+ && mBuffer[j + 5] == 'A'
+ && mBuffer[j + 6] == '9'
+ && mBuffer[j + 7] == '4'
+ && mBuffer[j + 8] == 3) {
+ j =
+ parseClosedCaption(
+ buffer,
+ i + j + PATTERN_LENGTH,
+ presentationTimeUs)
+ - i;
+ } else {
+ j += PATTERN_LENGTH;
+ }
+ } else {
+ ++j;
+ }
+ }
+ }
+ buffer.position(totalSize);
+ }
+ }
+
+ private class H264CcParser extends CcParser {
+ private static final int PATTERN_LENGTH = 14;
+
+ @Override
+ public void mayParseClosedCaption(ByteBuffer buffer, long presentationTimeUs) {
+ int totalSize = buffer.position();
+ // Reading the frame in bulk to reduce the overhead from ByteBuffer.get() with
+ // overlapping to handle the case that the pattern exists in the boundary.
+ for (int i = 0; i < totalSize; i += mBuffer.length - PATTERN_LENGTH) {
+ buffer.position(i);
+ int size = Math.min(totalSize - i, mBuffer.length);
+ buffer.get(mBuffer, 0, size);
+ int j = 0;
+ while (j < size - PATTERN_LENGTH) {
+ // Find the start prefix code of a NAL Unit.
+ if (mBuffer[j] == 0 && mBuffer[j + 1] == 0 && mBuffer[j + 2] == 1) {
+ int nalType = mBuffer[j + 3] & 0x1f;
+ int payloadType = mBuffer[j + 4] & 0xff;
+
+ // ATSC closed caption data embedded in H264 private user data has NAL type
+ // 6, payload type 4, and 'GA94' user identifier for ATSC.
+ if (nalType == 6
+ && payloadType == 4
+ && mBuffer[j + 9] == 'G'
+ && mBuffer[j + 10] == 'A'
+ && mBuffer[j + 11] == '9'
+ && mBuffer[j + 12] == '4') {
+ j =
+ parseClosedCaption(
+ buffer,
+ i + j + PATTERN_LENGTH,
+ presentationTimeUs)
+ - i;
+ } else {
+ j += 7;
+ }
+ } else {
+ ++j;
+ }
+ }
+ }
+ buffer.position(totalSize);
+ }
+ }
+}
diff --git a/src/com/android/tv/tuner/exoplayer/MpegTsSampleSource.java b/src/com/android/tv/tuner/exoplayer/MpegTsSampleSource.java
new file mode 100644
index 00000000..3b5d1011
--- /dev/null
+++ b/src/com/android/tv/tuner/exoplayer/MpegTsSampleSource.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.tuner.exoplayer;
+
+import 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.SampleSource.SampleSourceReader;
+import com.google.android.exoplayer.util.Assertions;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/** {@link SampleSource} that extracts sample data using a {@link SampleExtractor}. */
+public final class MpegTsSampleSource implements SampleSource, SampleSourceReader {
+
+ private static final int TRACK_STATE_DISABLED = 0;
+ private static final int TRACK_STATE_ENABLED = 1;
+ private static final int TRACK_STATE_FORMAT_SENT = 2;
+
+ private final SampleExtractor mSampleExtractor;
+ private final List<Integer> mTrackStates = new ArrayList<>();
+ private final List<Boolean> mPendingDiscontinuities = new ArrayList<>();
+
+ private boolean mPrepared;
+ private IOException mPreparationError;
+ private int mRemainingReleaseCount;
+
+ private long mLastSeekPositionUs;
+ private long mPendingSeekPositionUs;
+
+ /**
+ * Creates a new sample source that extracts samples using {@code mSampleExtractor}.
+ *
+ * @param sampleExtractor a sample extractor for accessing media samples
+ */
+ public MpegTsSampleSource(SampleExtractor sampleExtractor) {
+ mSampleExtractor = Assertions.checkNotNull(sampleExtractor);
+ }
+
+ @Override
+ public SampleSourceReader register() {
+ mRemainingReleaseCount++;
+ return this;
+ }
+
+ @Override
+ public boolean prepare(long positionUs) {
+ if (!mPrepared) {
+ if (mPreparationError != null) {
+ return false;
+ }
+ try {
+ if (mSampleExtractor.prepare()) {
+ int trackCount = mSampleExtractor.getTrackFormats().size();
+ mTrackStates.clear();
+ mPendingDiscontinuities.clear();
+ for (int i = 0; i < trackCount; ++i) {
+ mTrackStates.add(i, TRACK_STATE_DISABLED);
+ mPendingDiscontinuities.add(i, false);
+ }
+ mPrepared = true;
+ } else {
+ return false;
+ }
+ } catch (IOException e) {
+ mPreparationError = e;
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public int getTrackCount() {
+ Assertions.checkState(mPrepared);
+ return mSampleExtractor.getTrackFormats().size();
+ }
+
+ @Override
+ public MediaFormat getFormat(int track) {
+ Assertions.checkState(mPrepared);
+ return mSampleExtractor.getTrackFormats().get(track);
+ }
+
+ @Override
+ public void enable(int track, long positionUs) {
+ Assertions.checkState(mPrepared);
+ Assertions.checkState(mTrackStates.get(track) == TRACK_STATE_DISABLED);
+ mTrackStates.set(track, TRACK_STATE_ENABLED);
+ mSampleExtractor.selectTrack(track);
+ seekToUsInternal(positionUs, positionUs != 0);
+ }
+
+ @Override
+ public void disable(int track) {
+ Assertions.checkState(mPrepared);
+ Assertions.checkState(mTrackStates.get(track) != TRACK_STATE_DISABLED);
+ mSampleExtractor.deselectTrack(track);
+ mPendingDiscontinuities.set(track, false);
+ mTrackStates.set(track, TRACK_STATE_DISABLED);
+ }
+
+ @Override
+ public boolean continueBuffering(int track, long positionUs) {
+ return mSampleExtractor.continueBuffering(positionUs);
+ }
+
+ @Override
+ public long readDiscontinuity(int track) {
+ if (mPendingDiscontinuities.get(track)) {
+ mPendingDiscontinuities.set(track, false);
+ return mLastSeekPositionUs;
+ }
+ return NO_DISCONTINUITY;
+ }
+
+ @Override
+ public int readData(
+ int track, long positionUs, MediaFormatHolder formatHolder, SampleHolder sampleHolder) {
+ Assertions.checkState(mPrepared);
+ Assertions.checkState(mTrackStates.get(track) != TRACK_STATE_DISABLED);
+ if (mPendingDiscontinuities.get(track)) {
+ return NOTHING_READ;
+ }
+ if (mTrackStates.get(track) != TRACK_STATE_FORMAT_SENT) {
+ mSampleExtractor.getTrackMediaFormat(track, formatHolder);
+ mTrackStates.set(track, TRACK_STATE_FORMAT_SENT);
+ return FORMAT_READ;
+ }
+
+ mPendingSeekPositionUs = C.UNKNOWN_TIME_US;
+ return mSampleExtractor.readSample(track, sampleHolder);
+ }
+
+ @Override
+ public void maybeThrowError() throws IOException {
+ if (mPreparationError != null) {
+ throw mPreparationError;
+ }
+ if (mSampleExtractor != null) {
+ mSampleExtractor.maybeThrowError();
+ }
+ }
+
+ @Override
+ public void seekToUs(long positionUs) {
+ Assertions.checkState(mPrepared);
+ seekToUsInternal(positionUs, false);
+ }
+
+ @Override
+ public long getBufferedPositionUs() {
+ Assertions.checkState(mPrepared);
+ return mSampleExtractor.getBufferedPositionUs();
+ }
+
+ @Override
+ public void release() {
+ Assertions.checkState(mRemainingReleaseCount > 0);
+ if (--mRemainingReleaseCount == 0) {
+ mSampleExtractor.release();
+ }
+ }
+
+ private void seekToUsInternal(long positionUs, boolean force) {
+ // Unless forced, avoid duplicate calls to the underlying extractor's seek method
+ // in the case that there have been no interleaving calls to readSample.
+ if (force || mPendingSeekPositionUs != positionUs) {
+ mLastSeekPositionUs = positionUs;
+ mPendingSeekPositionUs = positionUs;
+ mSampleExtractor.seekTo(positionUs);
+ for (int i = 0; i < mTrackStates.size(); ++i) {
+ if (mTrackStates.get(i) != TRACK_STATE_DISABLED) {
+ mPendingDiscontinuities.set(i, true);
+ }
+ }
+ }
+ }
+}
diff --git a/src/com/android/tv/tuner/exoplayer/MpegTsVideoTrackRenderer.java b/src/com/android/tv/tuner/exoplayer/MpegTsVideoTrackRenderer.java
new file mode 100644
index 00000000..c4400b47
--- /dev/null
+++ b/src/com/android/tv/tuner/exoplayer/MpegTsVideoTrackRenderer.java
@@ -0,0 +1,111 @@
+package com.android.tv.tuner.exoplayer;
+
+import android.content.Context;
+import android.media.MediaCodec;
+import android.os.Handler;
+import android.util.Log;
+import com.android.tv.common.feature.CommonFeatures;
+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.MediaCodecVideoTrackRenderer;
+import com.google.android.exoplayer.MediaFormatHolder;
+import com.google.android.exoplayer.MediaSoftwareCodecUtil;
+import com.google.android.exoplayer.SampleSource;
+import java.lang.reflect.Field;
+
+/** MPEG-2 TS video track renderer */
+public class MpegTsVideoTrackRenderer extends MediaCodecVideoTrackRenderer {
+ private static final String TAG = "MpegTsVideoTrackRender";
+
+ private static final int VIDEO_PLAYBACK_DEADLINE_IN_MS = 5000;
+ // If DROPPED_FRAMES_NOTIFICATION_THRESHOLD frames are consecutively dropped, it'll be notified.
+ private static final int DROPPED_FRAMES_NOTIFICATION_THRESHOLD = 10;
+ private static final int MIN_HD_HEIGHT = 720;
+ private static final String MIMETYPE_MPEG2 = "video/mpeg2";
+ private static Field sRenderedFirstFrameField;
+
+ private final boolean mIsSwCodecEnabled;
+ private boolean mCodecIsSwPreferred;
+ private boolean mSetRenderedFirstFrame;
+
+ static {
+ // Remove the reflection below once b/31223646 is resolved.
+ try {
+ sRenderedFirstFrameField =
+ MediaCodecVideoTrackRenderer.class.getDeclaredField("renderedFirstFrame");
+ sRenderedFirstFrameField.setAccessible(true);
+ } catch (NoSuchFieldException e) {
+ // Null-checking for {@code sRenderedFirstFrameField} will do the error handling.
+ }
+ }
+
+ public MpegTsVideoTrackRenderer(
+ Context context,
+ SampleSource source,
+ Handler handler,
+ MediaCodecVideoTrackRenderer.EventListener listener) {
+ super(
+ context,
+ source,
+ MediaCodecSelector.DEFAULT,
+ MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT,
+ VIDEO_PLAYBACK_DEADLINE_IN_MS,
+ handler,
+ listener,
+ DROPPED_FRAMES_NOTIFICATION_THRESHOLD);
+ mIsSwCodecEnabled = CommonFeatures.USE_SW_CODEC_FOR_SD.isEnabled(context);
+ }
+
+ @Override
+ protected DecoderInfo getDecoderInfo(
+ MediaCodecSelector codecSelector, String mimeType, boolean requiresSecureDecoder)
+ throws MediaCodecUtil.DecoderQueryException {
+ try {
+ if (mIsSwCodecEnabled && mCodecIsSwPreferred) {
+ DecoderInfo swCodec =
+ MediaSoftwareCodecUtil.getSoftwareDecoderInfo(
+ mimeType, requiresSecureDecoder);
+ if (swCodec != null) {
+ return swCodec;
+ }
+ }
+ } catch (MediaSoftwareCodecUtil.DecoderQueryException e) {
+ }
+ return super.getDecoderInfo(codecSelector, mimeType, requiresSecureDecoder);
+ }
+
+ @Override
+ protected void onInputFormatChanged(MediaFormatHolder holder) throws ExoPlaybackException {
+ mCodecIsSwPreferred =
+ MIMETYPE_MPEG2.equalsIgnoreCase(holder.format.mimeType)
+ && holder.format.height < MIN_HD_HEIGHT;
+ super.onInputFormatChanged(holder);
+ }
+
+ @Override
+ protected void onDiscontinuity(long positionUs) throws ExoPlaybackException {
+ super.onDiscontinuity(positionUs);
+ // Disabling pre-rendering of the first frame in order to avoid a frozen picture when
+ // starting the playback. We do this only once, when the renderer is enabled at first, since
+ // we need to pre-render the frame in advance when we do trickplay backed by seeking.
+ if (!mSetRenderedFirstFrame) {
+ setRenderedFirstFrame(true);
+ mSetRenderedFirstFrame = true;
+ }
+ }
+
+ private void setRenderedFirstFrame(boolean renderedFirstFrame) {
+ if (sRenderedFirstFrameField != null) {
+ try {
+ sRenderedFirstFrameField.setBoolean(this, renderedFirstFrame);
+ } catch (IllegalAccessException e) {
+ Log.w(
+ TAG,
+ "renderedFirstFrame is not accessible. Playback may start with a frozen"
+ + " picture.");
+ }
+ }
+ }
+}
diff --git a/src/com/android/tv/tuner/exoplayer/SampleExtractor.java b/src/com/android/tv/tuner/exoplayer/SampleExtractor.java
new file mode 100644
index 00000000..256aea92
--- /dev/null
+++ b/src/com/android/tv/tuner/exoplayer/SampleExtractor.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.tuner.exoplayer;
+
+import android.os.Handler;
+import com.google.android.exoplayer.MediaFormat;
+import com.google.android.exoplayer.MediaFormatHolder;
+import com.google.android.exoplayer.SampleHolder;
+import com.google.android.exoplayer.SampleSource;
+import com.google.android.exoplayer.TrackRenderer;
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * Extractor for reading track metadata and samples stored in tracks.
+ *
+ * <p>Call {@link #prepare} until it returns {@code true}, then access track metadata via {@link
+ * #getTrackFormats} and {@link #getTrackMediaFormat}.
+ *
+ * <p>Pass indices of tracks to read from to {@link #selectTrack}. A track can later be deselected
+ * by calling {@link #deselectTrack}. It is safe to select/deselect tracks after reading sample data
+ * or seeking. Initially, all tracks are deselected.
+ *
+ * <p>Call {@link #release()} when the extractor is no longer needed to free resources.
+ */
+public interface SampleExtractor {
+
+ /**
+ * If the extractor is currently having difficulty preparing or loading samples, then this
+ * method throws the underlying error. Otherwise does nothing.
+ *
+ * @throws IOException The underlying error.
+ */
+ void maybeThrowError() throws IOException;
+
+ /**
+ * Prepares the extractor for reading track metadata and samples.
+ *
+ * @return whether the source is ready; if {@code false}, this method must be called again.
+ * @throws IOException thrown if the source can't be read
+ */
+ boolean prepare() throws IOException;
+
+ /** Returns track information about all tracks that can be selected. */
+ List<MediaFormat> getTrackFormats();
+
+ /** Selects the track at {@code index} for reading sample data. */
+ void selectTrack(int index);
+
+ /** Deselects the track at {@code index}, so no more samples will be read from that track. */
+ void deselectTrack(int index);
+
+ /**
+ * Returns an estimate of the position up to which data is buffered.
+ *
+ * <p>This method should not be called until after the extractor has been successfully prepared.
+ *
+ * @return an estimate of the absolute position in microseconds up to which data is buffered, or
+ * {@link TrackRenderer#END_OF_TRACK_US} if data is buffered to the end of the stream, or
+ * {@link TrackRenderer#UNKNOWN_TIME_US} if no estimate is available.
+ */
+ long getBufferedPositionUs();
+
+ /**
+ * Seeks to the specified time in microseconds.
+ *
+ * <p>This method should not be called until after the extractor has been successfully prepared.
+ *
+ * @param positionUs the seek position in microseconds
+ */
+ void seekTo(long positionUs);
+
+ /** Stores the {@link MediaFormat} of {@code track}. */
+ void getTrackMediaFormat(int track, MediaFormatHolder outMediaFormatHolder);
+
+ /**
+ * Reads the next sample in the track at index {@code track} into {@code sampleHolder},
+ * returning {@link SampleSource#SAMPLE_READ} if it is available.
+ *
+ * <p>Advances to the next sample if a sample was read.
+ *
+ * @param track the index of the track from which to read a sample
+ * @param sampleHolder the holder for read sample data, if {@link SampleSource#SAMPLE_READ} is
+ * returned
+ * @return {@link SampleSource#SAMPLE_READ} if a sample was read into {@code sampleHolder}, or
+ * {@link SampleSource#END_OF_STREAM} if the last samples in all tracks have been read, or
+ * {@link SampleSource#NOTHING_READ} if the sample cannot be read immediately as it is not
+ * loaded.
+ */
+ int readSample(int track, SampleHolder sampleHolder);
+
+ /** Releases resources associated with this extractor. */
+ void release();
+
+ /** Indicates to the source that it should still be buffering data. */
+ boolean continueBuffering(long positionUs);
+
+ /**
+ * Sets OnCompletionListener for notifying the completion of SampleExtractor.
+ *
+ * @param listener the OnCompletionListener
+ * @param handler the {@link Handler} for {@link Handler#post(Runnable)} of OnCompletionListener
+ */
+ void setOnCompletionListener(OnCompletionListener listener, Handler handler);
+
+ /** The listener for SampleExtractor being completed. */
+ interface OnCompletionListener {
+
+ /**
+ * Called when sample extraction is completed.
+ *
+ * @param result {@code true} when the extractor is finished without an error, {@code false}
+ * otherwise (storage error, weak signal, being reached at EoS prematurely, etc.)
+ * @param lastExtractedPositionUs the last extracted position when extractor is completed
+ */
+ void onCompletion(boolean result, long lastExtractedPositionUs);
+ }
+}
diff --git a/src/com/android/tv/tuner/exoplayer/audio/AudioClock.java b/src/com/android/tv/tuner/exoplayer/audio/AudioClock.java
new file mode 100644
index 00000000..13eabc3a
--- /dev/null
+++ b/src/com/android/tv/tuner/exoplayer/audio/AudioClock.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.exoplayer.audio;
+
+import android.os.SystemClock;
+import com.android.tv.common.SoftPreconditions;
+
+/**
+ * Copy of {@link com.google.android.exoplayer.MediaClock}.
+ *
+ * <p>A simple clock for tracking the progression of media time. The clock can be started, stopped
+ * and its time can be set and retrieved. When started, this clock is based on {@link
+ * SystemClock#elapsedRealtime()}.
+ */
+/* package */ class AudioClock {
+ private boolean mStarted;
+
+ /** The media time when the clock was last set or stopped. */
+ private long mPositionUs;
+
+ /**
+ * The difference between {@link SystemClock#elapsedRealtime()} and {@link #mPositionUs} when
+ * the clock was last set or mStarted.
+ */
+ private long mDeltaUs;
+
+ private float mPlaybackSpeed = 1.0f;
+ private long mDeltaUpdatedTimeUs;
+
+ /** Starts the clock. Does nothing if the clock is already started. */
+ public void start() {
+ if (!mStarted) {
+ mStarted = true;
+ mDeltaUs = elapsedRealtimeMinus(mPositionUs);
+ mDeltaUpdatedTimeUs = SystemClock.elapsedRealtime() * 1000;
+ }
+ }
+
+ /** Stops the clock. Does nothing if the clock is already stopped. */
+ public void stop() {
+ if (mStarted) {
+ mPositionUs = elapsedRealtimeMinus(mDeltaUs);
+ mStarted = false;
+ }
+ }
+
+ /** @param timeUs The position to set in microseconds. */
+ public void setPositionUs(long timeUs) {
+ this.mPositionUs = timeUs;
+ mDeltaUs = elapsedRealtimeMinus(timeUs);
+ mDeltaUpdatedTimeUs = SystemClock.elapsedRealtime() * 1000;
+ }
+
+ /** @return The current position in microseconds. */
+ public long getPositionUs() {
+ if (!mStarted) {
+ return mPositionUs;
+ }
+ if (mPlaybackSpeed != 1.0f) {
+ long elapsedTimeFromPlaybackSpeedChanged =
+ SystemClock.elapsedRealtime() * 1000 - mDeltaUpdatedTimeUs;
+ return elapsedRealtimeMinus(mDeltaUs)
+ + (long) ((mPlaybackSpeed - 1.0f) * elapsedTimeFromPlaybackSpeedChanged);
+ } else {
+ return elapsedRealtimeMinus(mDeltaUs);
+ }
+ }
+
+ /** Sets playback speed. {@code speed} should be positive. */
+ public void setPlaybackSpeed(float speed) {
+ SoftPreconditions.checkState(speed > 0);
+ mDeltaUs = elapsedRealtimeMinus(getPositionUs());
+ mDeltaUpdatedTimeUs = SystemClock.elapsedRealtime() * 1000;
+ mPlaybackSpeed = speed;
+ }
+
+ private long elapsedRealtimeMinus(long toSubtractUs) {
+ return SystemClock.elapsedRealtime() * 1000 - toSubtractUs;
+ }
+}
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..fa489883
--- /dev/null
+++ b/src/com/android/tv/tuner/exoplayer/audio/AudioDecoder.java
@@ -0,0 +1,69 @@
+/*
+ * 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/audio/AudioTrackMonitor.java b/src/com/android/tv/tuner/exoplayer/audio/AudioTrackMonitor.java
new file mode 100644
index 00000000..28389017
--- /dev/null
+++ b/src/com/android/tv/tuner/exoplayer/audio/AudioTrackMonitor.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.exoplayer.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;
+
+/** Monitors the rendering position of {@link AudioTrack}. */
+public class AudioTrackMonitor {
+ private static final String TAG = "AudioTrackMonitor";
+ private static final boolean DEBUG = false;
+
+ // For fetched audio samples
+ 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> mHeader = new HashSet<>();
+
+ private long mExpireMs;
+ private long mDuration;
+ private long mSampleCount;
+ private long mTotalCount;
+ private long mStartMs;
+
+ private boolean mIsMp2;
+
+ private void flush() {
+ mExpireMs += mDuration;
+ mSampleCount = 0;
+ mCurSampleSize.clear();
+ mPtsList.clear();
+ }
+
+ /**
+ * Resets and initializes {@link AudioTrackMonitor}.
+ *
+ * @param duration the frequency of monitoring in milliseconds
+ */
+ public void reset(long duration) {
+ mExpireMs = SystemClock.elapsedRealtime();
+ mDuration = duration;
+ mTotalCount = 0;
+ mStartMs = 0;
+ mSampleSize.clear();
+ mHeader.clear();
+ flush();
+ }
+
+ public void setEncoding(String mime) {
+ mIsMp2 = MimeTypes.AUDIO_MPEG_L2.equalsIgnoreCase(mime);
+ }
+
+ /**
+ * Adds an audio sample information for monitoring.
+ *
+ * @param pts the presentation timestamp of the sample
+ * @param sampleSize the size in bytes of the sample
+ * @param header the bitrate &amp; sampling information header of the sample
+ */
+ public void addPts(long pts, int sampleSize, int header) {
+ mTotalCount++;
+ mSampleCount++;
+ mSampleSize.add(sampleSize);
+ mHeader.add(header);
+ mCurSampleSize.add(sampleSize);
+ if (mTotalCount == 1) {
+ mStartMs = SystemClock.elapsedRealtime();
+ }
+ if (mPtsList.isEmpty() || mPtsList.get(mPtsList.size() - 1).first != pts) {
+ mPtsList.add(Pair.create(pts, 1));
+ return;
+ }
+ Pair<Long, Integer> pair = mPtsList.get(mPtsList.size() - 1);
+ mPtsList.set(mPtsList.size() - 1, Pair.create(pair.first, pair.second + 1));
+ }
+
+ /**
+ * Logs if interested events are present.
+ *
+ * <p>Periodic logging is not enabled in release mode in order to avoid verbose logging.
+ */
+ public void maybeLog() {
+ long now = SystemClock.elapsedRealtime();
+ if (mExpireMs != 0 && now >= mExpireMs) {
+ if (DEBUG) {
+ 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(", ")
+ .append(totalDuration - sampleDuration)
+ .append(' ');
+
+ for (Pair<Long, Integer> pair : mPtsList) {
+ ptsBuilder
+ .append('[')
+ .append(pair.first)
+ .append(':')
+ .append(pair.second)
+ .append("], ");
+ }
+ Log.d(TAG, ptsBuilder.toString());
+ }
+ if (DEBUG || mCurSampleSize.size() > 1) {
+ Log.d(
+ TAG,
+ "PTS received sample size: "
+ + String.valueOf(mSampleSize)
+ + mCurSampleSize
+ + mHeader);
+ }
+ flush();
+ }
+ }
+}
diff --git a/src/com/android/tv/tuner/exoplayer/audio/AudioTrackWrapper.java b/src/com/android/tv/tuner/exoplayer/audio/AudioTrackWrapper.java
new file mode 100644
index 00000000..7446c923
--- /dev/null
+++ b/src/com/android/tv/tuner/exoplayer/audio/AudioTrackWrapper.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.exoplayer.audio;
+
+import android.media.MediaFormat;
+import com.google.android.exoplayer.C;
+import com.google.android.exoplayer.audio.AudioTrack;
+import java.nio.ByteBuffer;
+
+/**
+ * {@link AudioTrack} wrapper class for trickplay operations including FF/RW. FF/RW trickplay
+ * operations do not need framework {@link AudioTrack}. 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;
+
+ AudioTrackWrapper() {
+ mIsEnabled = true;
+ }
+
+ public void resetSessionId() {
+ mAudioSessionID = AudioTrack.SESSION_ID_NOT_SET;
+ }
+
+ public boolean isInitialized() {
+ return mIsEnabled && mAudioTrack.isInitialized();
+ }
+
+ public void restart() {
+ if (mAudioTrack.isInitialized()) {
+ mAudioTrack.release();
+ }
+ mIsEnabled = true;
+ resetSessionId();
+ }
+
+ public void release() {
+ if (mAudioSessionID != AudioTrack.SESSION_ID_NOT_SET) {
+ mAudioTrack.release();
+ }
+ }
+
+ public void initialize() throws AudioTrack.InitializationException {
+ if (!mIsEnabled) {
+ return;
+ }
+ if (mAudioSessionID != AudioTrack.SESSION_ID_NOT_SET) {
+ mAudioTrack.initialize(mAudioSessionID);
+ } else {
+ mAudioSessionID = mAudioTrack.initialize();
+ }
+ }
+
+ public void reset() {
+ if (!mIsEnabled) {
+ return;
+ }
+ mAudioTrack.reset();
+ }
+
+ public boolean isEnded() {
+ return !mIsEnabled || !mAudioTrack.hasPendingData();
+ }
+
+ public boolean isReady() {
+ // In the case of not playing actual audio data, Audio track is always ready.
+ return !mIsEnabled || mAudioTrack.hasPendingData();
+ }
+
+ public void play() {
+ if (!mIsEnabled) {
+ return;
+ }
+ mAudioTrack.play();
+ }
+
+ public void pause() {
+ if (!mIsEnabled) {
+ return;
+ }
+ mAudioTrack.pause();
+ }
+
+ public void setVolume(float volume) {
+ if (!mIsEnabled) {
+ return;
+ }
+ mAudioTrack.setVolume(volume);
+ }
+
+ public void reconfigure(MediaFormat format, int audioBufferSize) {
+ if (!mIsEnabled || format == null) {
+ return;
+ }
+ String mimeType = format.getString(MediaFormat.KEY_MIME);
+ int channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
+ int sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE);
+ int pcmEncoding;
+ try {
+ pcmEncoding = format.getInteger(MediaFormat.KEY_PCM_ENCODING);
+ } catch (Exception e) {
+ pcmEncoding = C.ENCODING_PCM_16BIT;
+ }
+ // 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,
+ // It is safe to fake non-stereo AC3 as AC3 stereo which is default passthrough mode.
+ // In other words, the channel count should be always 2.
+ channelCount = 2;
+ }
+ 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() {
+ if (!mIsEnabled) {
+ return;
+ }
+ mAudioTrack.handleDiscontinuity();
+ }
+
+ public int handleBuffer(ByteBuffer buffer, int offset, int size, long presentationTimeUs)
+ throws AudioTrack.WriteException {
+ if (!mIsEnabled) {
+ return AudioTrack.RESULT_BUFFER_CONSUMED;
+ }
+ return mAudioTrack.handleBuffer(buffer, offset, size, presentationTimeUs);
+ }
+
+ public void setStatus(boolean enable) {
+ if (enable == mIsEnabled) {
+ return;
+ }
+ mAudioTrack.reset();
+ mIsEnabled = enable;
+ }
+
+ public boolean isEnabled() {
+ return mIsEnabled;
+ }
+
+ // This should be used only in case of being enabled.
+ public long getCurrentPositionUs(boolean isEnded) {
+ return mAudioTrack.getCurrentPositionUs(isEnded);
+ }
+}
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..80f91ebd
--- /dev/null
+++ b/src/com/android/tv/tuner/exoplayer/audio/MediaCodecAudioDecoder.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.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/audio/MpegTsDefaultAudioTrackRenderer.java b/src/com/android/tv/tuner/exoplayer/audio/MpegTsDefaultAudioTrackRenderer.java
new file mode 100644
index 00000000..ae18e05d
--- /dev/null
+++ b/src/com/android/tv/tuner/exoplayer/audio/MpegTsDefaultAudioTrackRenderer.java
@@ -0,0 +1,743 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.exoplayer.audio;
+
+import android.media.MediaCodec;
+import android.os.Build;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.util.Log;
+import com.android.tv.tuner.exoplayer.ffmpeg.FfmpegDecoderClient;
+import com.android.tv.tuner.tvinput.TunerDebug;
+import com.google.android.exoplayer.CodecCounters;
+import com.google.android.exoplayer.ExoPlaybackException;
+import com.google.android.exoplayer.MediaClock;
+import com.google.android.exoplayer.MediaCodecSelector;
+import com.google.android.exoplayer.MediaFormat;
+import com.google.android.exoplayer.MediaFormatHolder;
+import com.google.android.exoplayer.SampleHolder;
+import com.google.android.exoplayer.SampleSource;
+import com.google.android.exoplayer.TrackRenderer;
+import com.google.android.exoplayer.audio.AudioTrack;
+import com.google.android.exoplayer.util.Assertions;
+import com.google.android.exoplayer.util.MimeTypes;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+
+/**
+ * Decodes and renders DTV audio. Supports MediaCodec based decoding, passthrough playback and
+ * ffmpeg based software decoding (AC3, MP2).
+ */
+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;
+
+ // ATSC/53 allows sample rate to be only 48Khz.
+ // One AC3 sample has 1536 frames, and its duration is 32ms.
+ public static final long AC3_SAMPLE_DURATION_US = 32000;
+
+ // 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;
+
+ /**
+ * Interface definition for a callback to be notified of {@link
+ * com.google.android.exoplayer.audio.AudioTrack} error.
+ */
+ public interface EventListener {
+ void onAudioTrackInitializationError(AudioTrack.InitializationException e);
+
+ void onAudioTrackWriteError(AudioTrack.WriteException e);
+ }
+
+ private static final int DEFAULT_INPUT_BUFFER_SIZE = 16384 * 2;
+ private static final int DEFAULT_OUTPUT_BUFFER_SIZE = 1024 * 1024;
+ private static final int MONITOR_DURATION_MS = 1000;
+ private static final int AC3_HEADER_BITRATE_OFFSET = 4;
+ 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.
+ private static final AudioTrackWrapper AUDIO_TRACK = new AudioTrackWrapper();
+ private static final long KEEP_ALIVE_AFTER_EOS_DURATION_MS = 3000;
+
+ // Ignore AudioTrack backward movement if duration of movement is below the threshold.
+ private static final long BACKWARD_AUDIO_TRACK_MOVE_THRESHOLD_US = 3000;
+
+ // AudioTrack position cannot go ahead beyond this limit.
+ private static final long CURRENT_POSITION_FROM_PTS_LIMIT_US = 1000000;
+
+ // Since MediaCodec processing and AudioTrack playing add delay,
+ // PTS interpolated time should be delayed reasonably when AudioTrack is not used.
+ private static final long ESTIMATED_TRACK_RENDERING_DELAY_US = 500000;
+
+ private final MediaCodecSelector mSelector;
+
+ private final CodecCounters mCodecCounters;
+ private final SampleSource.SampleSourceReader mSource;
+ 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;
+ private boolean mInputStreamEnded;
+ private boolean mOutputStreamEnded;
+ private long mEndOfStreamMs;
+ private long mCurrentPositionUs;
+ private int mPresentationCount;
+ private long mPresentationTimeUs;
+ private long mInterpolatedTimeUs;
+ private long mPreviousPositionUs;
+ private boolean mIsStopped;
+ private boolean mEnabled = true;
+ private boolean mIsMuted;
+ private ArrayList<Integer> mTracksIndex;
+ 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;
+ mOutputBuffer = ByteBuffer.allocate(DEFAULT_OUTPUT_BUFFER_SIZE);
+ mFormatHolder = new MediaFormatHolder();
+ AUDIO_TRACK.restart();
+ mCodecCounters = new CodecCounters();
+ mMonitor = new AudioTrackMonitor();
+ mAudioClock = new AudioClock();
+ mTracksIndex = new ArrayList<>();
+ mAc3Passthrough = usePassthrough;
+ mSoftwareDecoderAvailable = hasSoftwareAudioDecoder && FfmpegDecoderClient.isAvailable();
+ }
+
+ @Override
+ protected MediaClock getMediaClock() {
+ return this;
+ }
+
+ 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
+ protected boolean doPrepare(long positionUs) throws ExoPlaybackException {
+ boolean sourcePrepared = mSource.prepare(positionUs);
+ if (!sourcePrepared) {
+ return false;
+ }
+ for (int i = 0; i < mSource.getTrackCount(); i++) {
+ String mimeType = mSource.getFormat(i).mimeType;
+ if (MimeTypes.isAudio(mimeType) && handlesMimeType(mimeType)) {
+ if (mTrackIndex < 0) {
+ mTrackIndex = i;
+ }
+ mTracksIndex.add(i);
+ }
+ }
+
+ // TODO: Check this case. Source does not have the proper mime type.
+ return true;
+ }
+
+ @Override
+ protected int getTrackCount() {
+ return mTracksIndex.size();
+ }
+
+ @Override
+ protected MediaFormat getFormat(int track) {
+ Assertions.checkArgument(track >= 0 && track < mTracksIndex.size());
+ return mSource.getFormat(mTracksIndex.get(track));
+ }
+
+ @Override
+ protected void onEnabled(int track, long positionUs, boolean joining) {
+ Assertions.checkArgument(track >= 0 && track < mTracksIndex.size());
+ mTrackIndex = mTracksIndex.get(track);
+ mSource.enable(mTrackIndex, positionUs);
+ seekToInternal(positionUs);
+ }
+
+ @Override
+ protected void onDisabled() {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
+ AUDIO_TRACK.resetSessionId();
+ }
+ clearDecodeState();
+ mFormat = null;
+ mSource.disable(mTrackIndex);
+ }
+
+ @Override
+ protected void onReleased() {
+ releaseDecoder();
+ AUDIO_TRACK.release();
+ mSource.release();
+ }
+
+ @Override
+ protected boolean isEnded() {
+ return mOutputStreamEnded && AUDIO_TRACK.isEnded();
+ }
+
+ @Override
+ protected boolean isReady() {
+ return AUDIO_TRACK.isReady() || (mFormat != null && (mSourceStateReady || mOutputReady));
+ }
+
+ private void seekToInternal(long positionUs) {
+ mMonitor.reset(MONITOR_DURATION_MS);
+ mSourceStateReady = false;
+ mInputStreamEnded = false;
+ mOutputStreamEnded = false;
+ mPresentationTimeUs = positionUs;
+ mPresentationCount = 0;
+ mPreviousPositionUs = 0;
+ mCurrentPositionUs = Long.MIN_VALUE;
+ mInterpolatedTimeUs = Long.MIN_VALUE;
+ mAudioClock.setPositionUs(positionUs);
+ }
+
+ @Override
+ protected void seekTo(long positionUs) {
+ mSource.seekToUs(positionUs);
+ AUDIO_TRACK.reset();
+ 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
+ protected void onStarted() {
+ AUDIO_TRACK.play();
+ mAudioClock.start();
+ mIsStopped = false;
+ }
+
+ @Override
+ protected void onStopped() {
+ AUDIO_TRACK.pause();
+ mAudioClock.stop();
+ mIsStopped = true;
+ }
+
+ @Override
+ protected void maybeThrowError() throws ExoPlaybackException {
+ try {
+ mSource.maybeThrowError();
+ } catch (IOException e) {
+ throw new ExoPlaybackException(e);
+ }
+ }
+
+ @Override
+ protected void doSomeWork(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
+ mMonitor.maybeLog();
+ try {
+ if (mEndOfStreamMs != 0) {
+ // Ensure playback stops, after EoS was notified.
+ // Sometimes MediaCodecTrackRenderer does not fetch EoS timely
+ // after EoS was notified here long before.
+ long diff = SystemClock.elapsedRealtime() - mEndOfStreamMs;
+ if (diff >= KEEP_ALIVE_AFTER_EOS_DURATION_MS && !mIsStopped) {
+ throw new ExoPlaybackException("Much time has elapsed after EoS");
+ }
+ }
+ boolean continueBuffering = mSource.continueBuffering(mTrackIndex, positionUs);
+ if (mSourceStateReady != continueBuffering) {
+ mSourceStateReady = continueBuffering;
+ if (DEBUG) {
+ Log.d(TAG, "mSourceStateReady: " + String.valueOf(mSourceStateReady));
+ }
+ }
+ long discontinuity = mSource.readDiscontinuity(mTrackIndex);
+ if (discontinuity != SampleSource.NO_DISCONTINUITY) {
+ AUDIO_TRACK.handleDiscontinuity();
+ mPresentationTimeUs = discontinuity;
+ mPresentationCount = 0;
+ clearDecodeState();
+ return;
+ }
+ if (mFormat == null) {
+ readFormat();
+ return;
+ }
+
+ 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()) {
+ if (mOutputReady) break;
+ }
+ }
+ }
+ mCodecCounters.ensureUpdated();
+ } catch (IOException e) {
+ throw new ExoPlaybackException(e);
+ }
+ }
+
+ private void ensureAudioTrackInitialized() {
+ if (!AUDIO_TRACK.isInitialized()) {
+ try {
+ if (DEBUG) {
+ Log.d(TAG, "AudioTrack initialized");
+ }
+ AUDIO_TRACK.initialize();
+ } catch (AudioTrack.InitializationException e) {
+ Log.e(TAG, "Error on AudioTrack initialization", e);
+ notifyAudioTrackInitializationError(e);
+
+ // Do not throw exception here but just disabling audioTrack to keep playing
+ // video without audio.
+ AUDIO_TRACK.setStatus(false);
+ }
+ if (getState() == TrackRenderer.STATE_STARTED) {
+ if (DEBUG) {
+ Log.d(TAG, "AudioTrack played");
+ }
+ AUDIO_TRACK.play();
+ }
+ }
+ }
+
+ private void clearDecodeState() {
+ mOutputReady = false;
+ 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);
+ if (result == SampleSource.FORMAT_READ) {
+ onInputFormatChanged(mFormatHolder);
+ }
+ }
+
+ private MediaFormat convertMediaFormatToRaw(MediaFormat format) {
+ return MediaFormat.createAudioFormat(
+ format.trackId,
+ MimeTypes.AUDIO_RAW,
+ format.bitrate,
+ format.maxInputSize,
+ format.durationUs,
+ format.channelCount,
+ format.sampleRate,
+ format.initializationData,
+ format.language);
+ }
+
+ private void onInputFormatChanged(MediaFormatHolder formatHolder) throws ExoPlaybackException {
+ 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();
+ 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 {
+ if (mInputStreamEnded) {
+ return false;
+ }
+
+ 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;
+ }
+ case SampleSource.FORMAT_READ:
+ {
+ Log.i(TAG, "Format was read again");
+ onInputFormatChanged(mFormatHolder);
+ return true;
+ }
+ case SampleSource.END_OF_STREAM:
+ {
+ Log.i(TAG, "End of stream from SampleSource");
+ mInputStreamEnded = true;
+ return false;
+ }
+ default:
+ {
+ if (mSampleHolder.size != mSampleSize
+ && mFormatConfigured
+ && !mUseFrameworkDecoder) {
+ onSampleSizeChanged(mSampleHolder.size);
+ }
+ mSampleHolder.data.flip();
+ 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;
+ }
+ }
+ }
+
+ private boolean processOutput() throws ExoPlaybackException {
+ if (mOutputStreamEnded) {
+ return false;
+ }
+ if (!mOutputReady) {
+ if (mInputStreamEnded) {
+ mOutputStreamEnded = true;
+ mEndOfStreamMs = SystemClock.elapsedRealtime();
+ return false;
+ }
+ return true;
+ }
+
+ ensureAudioTrackInitialized();
+ int handleBufferResult;
+ try {
+ // To reduce discontinuity, interpolate presentation time.
+ if (MimeTypes.AUDIO_MPEG_L2.equalsIgnoreCase(mDecodingMime)) {
+ mInterpolatedTimeUs =
+ mPresentationTimeUs + mPresentationCount * MP2_SAMPLE_DURATION_US;
+ } else if (!mUseFrameworkDecoder) {
+ mInterpolatedTimeUs =
+ mPresentationTimeUs + mPresentationCount * AC3_SAMPLE_DURATION_US;
+ } 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;
+ }
+ if ((handleBufferResult & AudioTrack.RESULT_BUFFER_CONSUMED) != 0) {
+ mCodecCounters.renderedOutputBufferCount++;
+ mOutputReady = false;
+ if (mUseFrameworkDecoder) {
+ ((MediaCodecAudioDecoder) mAudioDecoder).releaseOutputBuffer();
+ }
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ protected long getDurationUs() {
+ return mSource.getFormat(mTrackIndex).durationUs;
+ }
+
+ @Override
+ protected long getBufferedPositionUs() {
+ long pos = mSource.getBufferedPositionUs();
+ return pos == UNKNOWN_TIME_US || pos == END_OF_TRACK_US
+ ? pos
+ : Math.max(pos, getPositionUs());
+ }
+
+ @Override
+ public long getPositionUs() {
+ if (!AUDIO_TRACK.isInitialized()) {
+ return mAudioClock.getPositionUs();
+ } else if (!AUDIO_TRACK.isEnabled()) {
+ if (mInterpolatedTimeUs > 0 && !mUseFrameworkDecoder) {
+ return mInterpolatedTimeUs - ESTIMATED_TRACK_RENDERING_DELAY_US;
+ }
+ return mPresentationTimeUs;
+ }
+ long audioTrackCurrentPositionUs = AUDIO_TRACK.getCurrentPositionUs(isEnded());
+ if (audioTrackCurrentPositionUs == AudioTrack.CURRENT_POSITION_NOT_SET) {
+ mPreviousPositionUs = 0L;
+ if (DEBUG) {
+ long oldPositionUs = Math.max(mCurrentPositionUs, 0);
+ long currentPositionUs = Math.max(mPresentationTimeUs, mCurrentPositionUs);
+ Log.d(
+ TAG,
+ "Audio position is not set, diff in us: "
+ + String.valueOf(currentPositionUs - oldPositionUs));
+ }
+ mCurrentPositionUs = Math.max(mPresentationTimeUs, mCurrentPositionUs);
+ } else {
+ if (mPreviousPositionUs
+ > audioTrackCurrentPositionUs + BACKWARD_AUDIO_TRACK_MOVE_THRESHOLD_US) {
+ Log.e(
+ TAG,
+ "audio_position BACK JUMP: "
+ + (mPreviousPositionUs - audioTrackCurrentPositionUs));
+ mCurrentPositionUs = audioTrackCurrentPositionUs;
+ } else {
+ mCurrentPositionUs = Math.max(mCurrentPositionUs, audioTrackCurrentPositionUs);
+ }
+ mPreviousPositionUs = audioTrackCurrentPositionUs;
+ }
+ long upperBound = mPresentationTimeUs + CURRENT_POSITION_FROM_PTS_LIMIT_US;
+ if (mCurrentPositionUs > upperBound) {
+ mCurrentPositionUs = upperBound;
+ }
+ return mCurrentPositionUs;
+ }
+
+ private void decodeDone(ByteBuffer outputBuffer, long presentationTimeUs) {
+ if (outputBuffer == null || mOutputBuffer == null) {
+ return;
+ }
+ if (presentationTimeUs < 0) {
+ Log.e(TAG, "decodeDone - invalid presentationTimeUs");
+ return;
+ }
+
+ if (TunerDebug.ENABLED) {
+ TunerDebug.setAudioPtsUs(presentationTimeUs);
+ }
+
+ mOutputBuffer.clear();
+ Assertions.checkState(mOutputBuffer.remaining() >= outputBuffer.limit());
+
+ mOutputBuffer.put(outputBuffer);
+ if (presentationTimeUs == mPresentationTimeUs) {
+ mPresentationCount++;
+ } else {
+ mPresentationCount = 0;
+ mPresentationTimeUs = presentationTimeUs;
+ }
+ mOutputBuffer.flip();
+ mOutputReady = true;
+ }
+
+ private void notifyAudioTrackInitializationError(final AudioTrack.InitializationException e) {
+ if (mEventHandler == null || mEventListener == null) {
+ return;
+ }
+ mEventHandler.post(
+ new Runnable() {
+ @Override
+ public void run() {
+ mEventListener.onAudioTrackInitializationError(e);
+ }
+ });
+ }
+
+ private void notifyAudioTrackWriteError(final AudioTrack.WriteException e) {
+ if (mEventHandler == null || mEventListener == null) {
+ return;
+ }
+ mEventHandler.post(
+ new Runnable() {
+ @Override
+ public void run() {
+ mEventListener.onAudioTrackWriteError(e);
+ }
+ });
+ }
+
+ @Override
+ public void handleMessage(int messageType, Object message) throws ExoPlaybackException {
+ switch (messageType) {
+ case MSG_SET_VOLUME:
+ float volume = (Float) message;
+ // Workaround: we cannot mute the audio track by setting the volume to 0, we need to
+ // disable the AUDIO_TRACK for this intent. However, enabling/disabling audio track
+ // whenever volume is being set might cause side effects, therefore we only handle
+ // "explicit mute operations", i.e., only after certain non-zero volume has been
+ // set, the subsequent volume setting operations will be consider as mute/un-mute
+ // operations and thus enable/disable the audio track.
+ if (mIsMuted && volume > 0) {
+ mIsMuted = false;
+ if (mEnabled) {
+ setStatus(true);
+ }
+ } else if (!mIsMuted && volume == 0) {
+ mIsMuted = true;
+ if (mEnabled) {
+ setStatus(false);
+ }
+ }
+ AUDIO_TRACK.setVolume(volume);
+ break;
+ case MSG_SET_AUDIO_TRACK:
+ mEnabled = (Integer) message == 1;
+ setStatus(mEnabled);
+ break;
+ case MSG_SET_PLAYBACK_SPEED:
+ mAudioClock.setPlaybackSpeed((Float) message);
+ break;
+ default:
+ super.handleMessage(messageType, message);
+ }
+ }
+
+ private void setStatus(boolean enabled) {
+ if (enabled == AUDIO_TRACK.isEnabled()) {
+ return;
+ }
+ if (!enabled) {
+ // mAudioClock can be different from getPositionUs. In order to sync them,
+ // we set mAudioClock.
+ mAudioClock.setPositionUs(getPositionUs());
+ }
+ AUDIO_TRACK.setStatus(enabled);
+ if (enabled) {
+ // When AUDIO_TRACK is enabled, we need to clear AUDIO_TRACK and seek to
+ // the current position. If not, AUDIO_TRACK has the obsolete data.
+ seekTo(mAudioClock.getPositionUs());
+ }
+ }
+}
diff --git a/src/com/android/tv/tuner/exoplayer/audio/MpegTsMediaCodecAudioTrackRenderer.java b/src/com/android/tv/tuner/exoplayer/audio/MpegTsMediaCodecAudioTrackRenderer.java
new file mode 100644
index 00000000..b382545f
--- /dev/null
+++ b/src/com/android/tv/tuner/exoplayer/audio/MpegTsMediaCodecAudioTrackRenderer.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.exoplayer.audio;
+
+import android.os.Handler;
+import com.google.android.exoplayer.ExoPlaybackException;
+import com.google.android.exoplayer.MediaCodecAudioTrackRenderer;
+import com.google.android.exoplayer.MediaCodecSelector;
+import com.google.android.exoplayer.SampleSource;
+
+/**
+ * MPEG-2 TS audio track renderer.
+ *
+ * <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 MpegTsMediaCodecAudioTrackRenderer extends MediaCodecAudioTrackRenderer {
+ private final Ac3EventListener mListener;
+
+ public interface Ac3EventListener extends EventListener {
+ /**
+ * Invoked when a {@link android.media.PlaybackParams} set to an {@link
+ * android.media.AudioTrack} is not valid.
+ *
+ * @param e The corresponding exception.
+ */
+ void onAudioTrackSetPlaybackParamsError(IllegalArgumentException e);
+ }
+
+ public MpegTsMediaCodecAudioTrackRenderer(
+ SampleSource source,
+ MediaCodecSelector mediaCodecSelector,
+ Handler eventHandler,
+ EventListener eventListener) {
+ super(source, mediaCodecSelector, eventHandler, eventListener);
+ mListener = (Ac3EventListener) eventListener;
+ }
+
+ @Override
+ public void handleMessage(int messageType, Object message) throws ExoPlaybackException {
+ if (messageType == MSG_SET_PLAYBACK_PARAMS) {
+ try {
+ super.handleMessage(messageType, message);
+ } catch (IllegalArgumentException e) {
+ if (isAudioTrackSetPlaybackParamsError(e)) {
+ notifyAudioTrackSetPlaybackParamsError(e);
+ }
+ }
+ return;
+ }
+ super.handleMessage(messageType, message);
+ }
+
+ private void notifyAudioTrackSetPlaybackParamsError(final IllegalArgumentException e) {
+ if (eventHandler != null && mListener != null) {
+ eventHandler.post(
+ new Runnable() {
+ @Override
+ public void run() {
+ mListener.onAudioTrackSetPlaybackParamsError(e);
+ }
+ });
+ }
+ }
+
+ private static boolean isAudioTrackSetPlaybackParamsError(IllegalArgumentException e) {
+ if (e.getStackTrace() == null || e.getStackTrace().length < 1) {
+ return false;
+ }
+ for (StackTraceElement element : e.getStackTrace()) {
+ String elementString = element.toString();
+ if (elementString.startsWith("android.media.AudioTrack.setPlaybackParams")) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java b/src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java
new file mode 100644
index 00000000..206e2098
--- /dev/null
+++ b/src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java
@@ -0,0 +1,682 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.exoplayer.buffer;
+
+import android.media.MediaFormat;
+import android.os.ConditionVariable;
+import android.support.annotation.NonNull;
+import android.support.annotation.VisibleForTesting;
+import android.util.ArrayMap;
+import android.util.Log;
+import android.util.Pair;
+import com.android.tv.common.SoftPreconditions;
+import com.android.tv.tuner.exoplayer.SampleExtractor;
+import com.android.tv.util.Utils;
+import com.google.android.exoplayer.SampleHolder;
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.ConcurrentModificationException;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+/**
+ * Manages {@link SampleChunk} objects.
+ *
+ * <p>The buffer manager can be disabled, while running, if the write throughput to the associated
+ * external storage is detected to be lower than a threshold {@code MINIMUM_DISK_WRITE_SPEED_MBPS}".
+ * This leads to restarting playback flow.
+ */
+public class BufferManager {
+ private static final String TAG = "BufferManager";
+ private static final boolean DEBUG = false;
+
+ // Constants for the disk write speed checking
+ private static final long MINIMUM_WRITE_SIZE_FOR_SPEED_CHECK =
+ 10L * 1024 * 1024; // Checks for every 10M disk write
+ private static final int MINIMUM_SAMPLE_SIZE_FOR_SPEED_CHECK = 15 * 1024;
+ private static final int MAXIMUM_SPEED_CHECK_COUNT = 5; // Checks only 5 times
+ private static final int MINIMUM_DISK_WRITE_SPEED_MBPS = 3; // 3 Megabytes per second
+
+ 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, 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;
+ private long mBufferSize = 0;
+ private final EvictChunkQueueMap mPendingDelete = new EvictChunkQueueMap();
+ private final SampleChunk.ChunkCallback mChunkCallback =
+ new SampleChunk.ChunkCallback() {
+ @Override
+ public void onChunkWrite(SampleChunk chunk) {
+ mBufferSize += chunk.getSize();
+ }
+
+ @Override
+ public void onChunkDelete(SampleChunk chunk) {
+ mBufferSize -= chunk.getSize();
+ }
+ };
+
+ private int mMinSampleSizeForSpeedCheck = MINIMUM_SAMPLE_SIZE_FOR_SPEED_CHECK;
+ private long mTotalWriteSize;
+ private long mTotalWriteTimeNs;
+ private float mWriteBandwidth = 0.0f;
+ private volatile int mSpeedCheckCount;
+
+ public interface ChunkEvictedListener {
+ void onChunkEvicted(String id, long createdTimeMs);
+ }
+ /** Handles I/O between BufferManager and {@link SampleExtractor}. */
+ public interface SampleBuffer {
+
+ /**
+ * Initializes SampleBuffer.
+ *
+ * @param Ids track identifiers for storage read/write.
+ * @param mediaFormats meta-data for each track.
+ * @throws IOException
+ */
+ void init(
+ @NonNull List<String> Ids,
+ @NonNull List<com.google.android.exoplayer.MediaFormat> mediaFormats)
+ throws IOException;
+
+ /** Selects the track {@code index} for reading sample data. */
+ void selectTrack(int index);
+
+ /**
+ * Deselects the track at {@code index}, so that no more samples will be read from the
+ * track.
+ */
+ void deselectTrack(int index);
+
+ /**
+ * Writes sample to storage.
+ *
+ * @param index track index
+ * @param sample sample to write at storage
+ * @param conditionVariable notifies the completion of writing sample.
+ * @throws IOException
+ */
+ void writeSample(int index, SampleHolder sample, ConditionVariable conditionVariable)
+ throws IOException;
+
+ /** Checks whether storage write speed is slow. */
+ boolean isWriteSpeedSlow(int sampleSize, long writeDurationNs);
+
+ /**
+ * Handles when write speed is slow.
+ *
+ * @throws IOException
+ */
+ void handleWriteSpeedSlow() throws IOException;
+
+ /** Sets the flag when EoS was reached. */
+ void setEos();
+
+ /**
+ * Reads the next sample in the track at index {@code track} into {@code sampleHolder},
+ * returning {@link com.google.android.exoplayer.SampleSource#SAMPLE_READ} if it is
+ * available. If the next sample is not available, returns {@link
+ * com.google.android.exoplayer.SampleSource#NOTHING_READ}.
+ */
+ int readSample(int index, SampleHolder outSample);
+
+ /** Seeks to the specified time in microseconds. */
+ void seekTo(long positionUs);
+
+ /** Returns an estimate of the position up to which data is buffered. */
+ long getBufferedPositionUs();
+
+ /** Returns whether there is buffered data. */
+ boolean continueBuffering(long positionUs);
+
+ /**
+ * Cleans up and releases everything.
+ *
+ * @throws IOException
+ */
+ void release() throws IOException;
+ }
+
+ /** A Track format which will be loaded and saved from the permanent storage for recordings. */
+ public static class TrackFormat {
+
+ /**
+ * The track id for the specified track. The track id will be used as a track identifier for
+ * recordings.
+ */
+ public final String trackId;
+
+ /** The {@link MediaFormat} for the specified track. */
+ public final MediaFormat format;
+
+ /**
+ * Creates TrackFormat.
+ *
+ * @param trackId
+ * @param format
+ */
+ public TrackFormat(String trackId, MediaFormat format) {
+ this.trackId = trackId;
+ this.format = format;
+ }
+ }
+
+ /** A Holder for a sample position which will be loaded from the index file for recordings. */
+ public static class PositionHolder {
+
+ /**
+ * The current sample position in microseconds. The position is identical to the
+ * PTS(presentation time stamp) of the sample.
+ */
+ public final long positionUs;
+
+ /** Base sample position for the current {@link SampleChunk}. */
+ public final long basePositionUs;
+
+ /** The file offset for the current sample in the current {@link SampleChunk}. */
+ public final int offset;
+
+ /**
+ * Creates a holder for a specific position in the recording.
+ *
+ * @param positionUs
+ * @param offset
+ */
+ public PositionHolder(long positionUs, long basePositionUs, int offset) {
+ this.positionUs = positionUs;
+ this.basePositionUs = basePositionUs;
+ this.offset = offset;
+ }
+ }
+
+ /** Storage configuration and policy manager for {@link BufferManager} */
+ public interface StorageManager {
+
+ /**
+ * Provides eligible storage directory for {@link BufferManager}.
+ *
+ * @return a directory to save buffer(chunks) and meta files
+ */
+ File getBufferDir();
+
+ /**
+ * Informs whether the storage is used for persistent use. (eg. dvr recording/play)
+ *
+ * @return {@code true} if stored files are persistent
+ */
+ boolean isPersistent();
+
+ /**
+ * Informs whether the storage usage exceeds pre-determined size.
+ *
+ * @param bufferSize the current total usage of Storage in bytes.
+ * @param pendingDelete the current storage usage which will be deleted in near future by
+ * bytes
+ * @return {@code true} if it reached pre-determined max size
+ */
+ boolean reachedStorageMax(long bufferSize, long pendingDelete);
+
+ /**
+ * Informs whether the storage has enough remained space.
+ *
+ * @param pendingDelete the current storage usage which will be deleted in near future by
+ * bytes
+ * @return {@code true} if it has enough space
+ */
+ boolean hasEnoughBuffer(long pendingDelete);
+
+ /**
+ * Reads track name & {@link MediaFormat} from storage.
+ *
+ * @param isAudio {@code true} if it is for audio track
+ * @return {@link List} of TrackFormat
+ */
+ List<TrackFormat> readTrackInfoFiles(boolean isAudio);
+
+ /**
+ * Reads key sample positions for each written sample from storage.
+ *
+ * @param trackId track name
+ * @return indexes of the specified track
+ * @throws IOException
+ */
+ ArrayList<PositionHolder> readIndexFile(String trackId) throws IOException;
+
+ /**
+ * Writes track information to storage.
+ *
+ * @param formatList {@list List} of TrackFormat
+ * @param isAudio {@code true} if it is for audio track
+ * @throws IOException
+ */
+ void writeTrackInfoFiles(List<TrackFormat> formatList, boolean isAudio) throws IOException;
+
+ /**
+ * Writes index file to storage.
+ *
+ * @param trackName track name
+ * @param index {@link SampleChunk} container
+ * @throws IOException
+ */
+ void writeIndexFile(String trackName, SortedMap<Long, Pair<SampleChunk, Integer>> index)
+ throws IOException;
+ }
+
+ private static class EvictChunkQueueMap {
+ private final Map<String, LinkedList<SampleChunk>> mEvictMap = new ArrayMap<>();
+ private long mSize;
+
+ private void init(String key) {
+ mEvictMap.put(key, new LinkedList<>());
+ }
+
+ private void add(String key, SampleChunk chunk) {
+ LinkedList<SampleChunk> queue = mEvictMap.get(key);
+ if (queue != null) {
+ mSize += chunk.getSize();
+ queue.add(chunk);
+ }
+ }
+
+ private SampleChunk poll(String key, long startPositionUs) {
+ LinkedList<SampleChunk> queue = mEvictMap.get(key);
+ if (queue != null) {
+ SampleChunk chunk = queue.peek();
+ if (chunk != null && chunk.getStartPositionUs() < startPositionUs) {
+ mSize -= chunk.getSize();
+ return queue.poll();
+ }
+ }
+ return null;
+ }
+
+ private long getSize() {
+ return mSize;
+ }
+
+ private void release() {
+ for (Map.Entry<String, LinkedList<SampleChunk>> entry : mEvictMap.entrySet()) {
+ for (SampleChunk chunk : entry.getValue()) {
+ SampleChunk.IoState.release(chunk, true);
+ }
+ }
+ mEvictMap.clear();
+ mSize = 0;
+ }
+ }
+
+ public BufferManager(StorageManager storageManager) {
+ this(storageManager, new SampleChunk.SampleChunkCreator());
+ }
+
+ public BufferManager(
+ StorageManager storageManager, SampleChunk.SampleChunkCreator sampleChunkCreator) {
+ mStorageManager = storageManager;
+ mSampleChunkCreator = sampleChunkCreator;
+ }
+
+ public void registerChunkEvictedListener(String id, ChunkEvictedListener listener) {
+ mEvictListeners.put(id, listener);
+ }
+
+ public void unregisterChunkEvictedListener(String id) {
+ mEvictListeners.remove(id);
+ }
+
+ 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 if it is needed.
+ *
+ * @param id the name of the track
+ * @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 createNewWriteFileIfNeeded(
+ String id,
+ long positionUs,
+ SamplePool samplePool,
+ SampleChunk currentChunk,
+ int currentOffset)
+ throws IOException {
+ if (!maybeEvictChunk()) {
+ throw new IOException("Not enough storage space");
+ }
+ 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);
+ }
+ 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;
+ }
+ }
+
+ /**
+ * Loads a track using {@link BufferManager.StorageManager}.
+ *
+ * @param trackId the name of the track.
+ * @param samplePool {@link SamplePool} for the fast creation of samples.
+ * @throws IOException
+ */
+ public void loadTrackFromStorage(String trackId, SamplePool samplePool) throws IOException {
+ ArrayList<PositionHolder> keyPositions = mStorageManager.readIndexFile(trackId);
+ long startPositionUs = keyPositions.size() > 0 ? keyPositions.get(0).positionUs : 0;
+
+ SortedMap<Long, Pair<SampleChunk, Integer>> map = mChunkMap.get(trackId);
+ if (map == null) {
+ map = new TreeMap<>();
+ mChunkMap.put(trackId, map);
+ mStartPositionMap.put(trackId, startPositionUs);
+ mPendingDelete.init(trackId);
+ }
+ SampleChunk chunk = null;
+ 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));
+ }
+ }
+
+ /**
+ * Finds a {@link SampleChunk} for the specified track name and the position.
+ *
+ * @param id the name of the track.
+ * @param positionUs the position.
+ * @return returns the found {@link SampleChunk}.
+ */
+ public Pair<SampleChunk, Integer> getReadFile(String id, long positionUs) {
+ SortedMap<Long, Pair<SampleChunk, Integer>> map = mChunkMap.get(id);
+ if (map == null) {
+ return null;
+ }
+ Pair<SampleChunk, Integer> ret;
+ SortedMap<Long, Pair<SampleChunk, Integer>> headMap = map.headMap(positionUs + 1);
+ if (!headMap.isEmpty()) {
+ ret = headMap.get(headMap.lastKey());
+ } else {
+ ret = map.get(map.firstKey());
+ }
+ return ret;
+ }
+
+ /**
+ * Evicts chunks which are ready to be evicted for the specified track
+ *
+ * @param id the specified track
+ * @param earlierThanPositionUs the start position of the {@link SampleChunk} should be earlier
+ * than
+ */
+ public void evictChunks(String id, long earlierThanPositionUs) {
+ SampleChunk chunk = null;
+ while ((chunk = mPendingDelete.poll(id, earlierThanPositionUs)) != null) {
+ SampleChunk.IoState.release(chunk, !mStorageManager.isPersistent());
+ }
+ }
+
+ /**
+ * Returns the start position of the specified track in micro seconds.
+ *
+ * @param id the specified track
+ */
+ public long getStartPositionUs(String id) {
+ Long ret = mStartPositionMap.get(id);
+ return ret == null ? 0 : ret;
+ }
+
+ private boolean maybeEvictChunk() {
+ long pendingDelete = mPendingDelete.getSize();
+ while (mStorageManager.reachedStorageMax(mBufferSize, pendingDelete)
+ || !mStorageManager.hasEnoughBuffer(pendingDelete)) {
+ if (mStorageManager.isPersistent()) {
+ // Since chunks are persistent, we cannot evict chunks.
+ return false;
+ }
+ SortedMap<Long, Pair<SampleChunk, Integer>> earliestChunkMap = null;
+ SampleChunk earliestChunk = null;
+ String earliestChunkId = null;
+ 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()).first;
+ if (earliestChunk == null
+ || chunk.getCreatedTimeMs() < earliestChunk.getCreatedTimeMs()) {
+ earliestChunkMap = map;
+ earliestChunk = chunk;
+ earliestChunkId = entry.getKey();
+ }
+ }
+ if (earliestChunk == null) {
+ break;
+ }
+ mPendingDelete.add(earliestChunkId, earliestChunk);
+ earliestChunkMap.remove(earliestChunk.getStartPositionUs());
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ String.format(
+ "bufferSize = %d; pendingDelete = %b; "
+ + "earliestChunk size = %d; %s@%d (%s)",
+ mBufferSize,
+ pendingDelete,
+ earliestChunk.getSize(),
+ earliestChunkId,
+ earliestChunk.getStartPositionUs(),
+ Utils.toIsoDateTimeString(earliestChunk.getCreatedTimeMs())));
+ }
+ ChunkEvictedListener listener = mEvictListeners.get(earliestChunkId);
+ if (listener != null) {
+ listener.onChunkEvicted(earliestChunkId, earliestChunk.getCreatedTimeMs());
+ }
+ pendingDelete = mPendingDelete.getSize();
+ }
+ for (Map.Entry<String, SortedMap<Long, Pair<SampleChunk, Integer>>> entry :
+ mChunkMap.entrySet()) {
+ SortedMap<Long, Pair<SampleChunk, Integer>> map = entry.getValue();
+ if (map.isEmpty()) {
+ continue;
+ }
+ mStartPositionMap.put(entry.getKey(), map.firstKey());
+ }
+ return true;
+ }
+
+ /**
+ * Reads track information which includes {@link MediaFormat}.
+ *
+ * @return returns all track information which is found by {@link BufferManager.StorageManager}.
+ * @throws IOException
+ */
+ 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");
+ }
+ return trackFormatList;
+ }
+
+ /**
+ * Writes track information and index information for all tracks.
+ *
+ * @param audios list of audio track information
+ * @param videos list of audio track information
+ * @throws IOException
+ */
+ public void writeMetaFiles(List<TrackFormat> audios, List<TrackFormat> videos)
+ throws IOException {
+ 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);
+ }
+ }
+ 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);
+ }
+ }
+ }
+
+ /** Releases all the resources. */
+ public void release() {
+ 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();
+ } 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());
+ }
+ }
+
+ private void resetWriteStat(float writeBandwidth) {
+ mWriteBandwidth = writeBandwidth;
+ mTotalWriteSize = 0;
+ mTotalWriteTimeNs = 0;
+ }
+
+ /** Adds a disk write sample size to calculate the average disk write bandwidth. */
+ public void addWriteStat(long size, long timeNs) {
+ if (size >= mMinSampleSizeForSpeedCheck) {
+ mTotalWriteSize += size;
+ mTotalWriteTimeNs += timeNs;
+ }
+ }
+
+ /**
+ * Returns if the average disk write bandwidth is slower than threshold {@code
+ * MINIMUM_DISK_WRITE_SPEED_MBPS}.
+ */
+ public boolean isWriteSlow() {
+ if (mTotalWriteSize < MINIMUM_WRITE_SIZE_FOR_SPEED_CHECK) {
+ return false;
+ }
+
+ // Checks write speed for only MAXIMUM_SPEED_CHECK_COUNT times to ignore outliers
+ // by temporary system overloading during the playback.
+ if (mSpeedCheckCount > MAXIMUM_SPEED_CHECK_COUNT) {
+ return false;
+ }
+ mSpeedCheckCount++;
+ float megabytePerSecond = calculateWriteBandwidth();
+ resetWriteStat(megabytePerSecond);
+ if (DEBUG) {
+ Log.d(TAG, "Measured disk write performance: " + megabytePerSecond + "MBps");
+ }
+ return megabytePerSecond < MINIMUM_DISK_WRITE_SPEED_MBPS;
+ }
+
+ /**
+ * Returns recent write bandwidth in MBps. If recent bandwidth is not available, returns {float
+ * -1.0f}.
+ */
+ public float getWriteBandwidth() {
+ return mWriteBandwidth == 0.0f ? -1.0f : mWriteBandwidth;
+ }
+
+ private float calculateWriteBandwidth() {
+ if (mTotalWriteTimeNs == 0) {
+ return -1;
+ }
+ return ((float) mTotalWriteSize * 1000 / mTotalWriteTimeNs);
+ }
+
+ /**
+ * Returns if {@link BufferManager} has checked the write speed, which is suitable for
+ * Trickplay.
+ */
+ @VisibleForTesting
+ public boolean hasSpeedCheckDone() {
+ return mSpeedCheckCount > 0;
+ }
+
+ /**
+ * Sets minimum sample size for write speed check.
+ *
+ * @param sampleSize minimum sample size for write speed check.
+ */
+ @VisibleForTesting
+ public void setMinimumSampleSizeForSpeedCheck(int sampleSize) {
+ mMinSampleSizeForSpeedCheck = sampleSize;
+ }
+}
diff --git a/src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java b/src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java
new file mode 100644
index 00000000..2a58ffcf
--- /dev/null
+++ b/src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java
@@ -0,0 +1,391 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.exoplayer.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;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+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.
+ private static final long MIN_BUFFER_BYTES = 256L * 1024 * 1024;
+ private static final int NO_VALUE = -1;
+ private static final long NO_VALUE_LONG = -1L;
+
+ private final File mBufferDir;
+
+ // {@code true} when this is for recording, {@code false} when this is for replaying.
+ private final boolean mIsRecording;
+
+ public DvrStorageManager(File file, boolean isRecording) {
+ mBufferDir = file;
+ mBufferDir.mkdirs();
+ mIsRecording = isRecording;
+ }
+
+ @Override
+ public File getBufferDir() {
+ return mBufferDir;
+ }
+
+ @Override
+ public boolean isPersistent() {
+ return true;
+ }
+
+ @Override
+ public boolean reachedStorageMax(long bufferSize, long pendingDelete) {
+ return false;
+ }
+
+ @Override
+ public boolean hasEnoughBuffer(long pendingDelete) {
+ return !mIsRecording || mBufferDir.getUsableSpace() >= MIN_BUFFER_BYTES;
+ }
+
+ private void readFormatInt(DataInputStream in, MediaFormat format, String key)
+ throws IOException {
+ int val = in.readInt();
+ if (val != NO_VALUE) {
+ format.setInteger(key, val);
+ }
+ }
+
+ private void readFormatLong(DataInputStream in, MediaFormat format, String key)
+ throws IOException {
+ long val = in.readLong();
+ if (val != NO_VALUE_LONG) {
+ format.setLong(key, val);
+ }
+ }
+
+ private void readFormatFloat(DataInputStream in, MediaFormat format, String key)
+ throws IOException {
+ float val = in.readFloat();
+ if (val != NO_VALUE) {
+ format.setFloat(key, val);
+ }
+ }
+
+ private String readString(DataInputStream in) throws IOException {
+ int len = in.readInt();
+ if (len <= 0) {
+ return null;
+ }
+ byte[] strBytes = new byte[len];
+ in.readFully(strBytes);
+ return new String(strBytes, StandardCharsets.UTF_8);
+ }
+
+ private void readFormatString(DataInputStream in, MediaFormat format, String key)
+ throws IOException {
+ String str = readString(in);
+ if (str != null) {
+ format.setString(key, str);
+ }
+ }
+
+ 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) {
+ return null;
+ }
+ byte[] bytes = new byte[len];
+ in.readFully(bytes);
+ ByteBuffer buffer = ByteBuffer.allocate(len);
+ buffer.put(bytes);
+ buffer.flip();
+
+ return buffer;
+ }
+
+ private void readFormatByteBuffer(DataInputStream in, MediaFormat format, String key)
+ throws IOException {
+ ByteBuffer buffer = readByteBuffer(in);
+ if (buffer != null) {
+ format.setByteBuffer(key, buffer);
+ }
+ }
+
+ @Override
+ 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;
+ }
+ 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;
+ }
+ }
+
+ 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) {
+ 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)) {
+ out.writeInt(format.getInteger(key));
+ } else {
+ out.writeInt(NO_VALUE);
+ }
+ }
+
+ private void writeFormatLong(DataOutputStream out, MediaFormat format, String key)
+ throws IOException {
+ if (format.containsKey(key)) {
+ out.writeLong(format.getLong(key));
+ } else {
+ out.writeLong(NO_VALUE_LONG);
+ }
+ }
+
+ private void writeFormatFloat(DataOutputStream out, MediaFormat format, String key)
+ throws IOException {
+ if (format.containsKey(key)) {
+ out.writeFloat(format.getFloat(key));
+ } else {
+ out.writeFloat(NO_VALUE);
+ }
+ }
+
+ private void writeString(DataOutputStream out, String str) throws IOException {
+ byte[] data = str.getBytes(StandardCharsets.UTF_8);
+ out.writeInt(data.length);
+ if (data.length > 0) {
+ out.write(data);
+ }
+ }
+
+ private void writeFormatString(DataOutputStream out, MediaFormat format, String key)
+ throws IOException {
+ if (format.containsKey(key)) {
+ writeString(out, format.getString(key));
+ } else {
+ out.writeInt(0);
+ }
+ }
+
+ private void writeByteBuffer(DataOutputStream out, ByteBuffer buffer) throws IOException {
+ byte[] data = new byte[buffer.limit()];
+ buffer.get(data);
+ buffer.flip();
+ out.writeInt(data.length);
+ if (data.length > 0) {
+ out.write(data);
+ } else {
+ out.writeInt(0);
+ }
+ }
+
+ private void writeFormatByteBuffer(DataOutputStream out, MediaFormat format, String key)
+ throws IOException {
+ if (format.containsKey(key)) {
+ writeByteBuffer(out, format.getByteBuffer(key));
+ } else {
+ out.writeInt(0);
+ }
+ }
+
+ @Override
+ public void writeTrackInfoFiles(List<BufferManager.TrackFormat> formatList, boolean isAudio)
+ throws IOException {
+ 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);
+ }
+ }
+ }
+
+ @Override
+ public void writeIndexFile(String trackName, SortedMap<Long, Pair<SampleChunk, Integer>> index)
+ throws IOException {
+ File indexFile = new File(getBufferDir(), trackName + IDX_FILE_SUFFIX_V2);
+ try (DataOutputStream out = new DataOutputStream(new FileOutputStream(indexFile))) {
+ out.writeLong(index.size());
+ 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
new file mode 100644
index 00000000..ebf00f59
--- /dev/null
+++ b/src/com/android/tv/tuner/exoplayer/buffer/RecordingSampleBuffer.java
@@ -0,0 +1,303 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.exoplayer.buffer;
+
+import android.os.ConditionVariable;
+import android.support.annotation.IntDef;
+import android.support.annotation.NonNull;
+import android.util.Log;
+import com.android.tv.tuner.exoplayer.MpegTsPlayer;
+import com.android.tv.tuner.exoplayer.SampleExtractor;
+import com.android.tv.tuner.tvinput.PlaybackBufferListener;
+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.google.android.exoplayer.util.Assertions;
+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.concurrent.TimeUnit;
+
+/**
+ * Handles I/O between {@link SampleExtractor} and {@link BufferManager}.Reads & writes samples
+ * from/to {@link SampleChunk} which is backed by physical storage.
+ */
+public class RecordingSampleBuffer
+ implements BufferManager.SampleBuffer, BufferManager.ChunkEvictedListener {
+ private static final String TAG = "RecordingSampleBuffer";
+
+ @IntDef({BUFFER_REASON_LIVE_PLAYBACK, BUFFER_REASON_RECORDED_PLAYBACK, BUFFER_REASON_RECORDING})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface BufferReason {}
+
+ /** A buffer reason for live-stream playback. */
+ public static final int BUFFER_REASON_LIVE_PLAYBACK = 0;
+
+ /** A buffer reason for playback of a recorded program. */
+ public static final int BUFFER_REASON_RECORDED_PLAYBACK = 1;
+
+ /** A buffer reason for recording a program. */
+ public static final int BUFFER_REASON_RECORDING = 2;
+
+ /** The minimum duration to support seek in Trickplay. */
+ 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);
+
+ private final BufferManager mBufferManager;
+ private final PlaybackBufferListener mBufferListener;
+ private final @BufferReason int mBufferReason;
+
+ private int mTrackCount;
+ private boolean[] mTrackSelected;
+ private List<SampleQueue> mReadSampleQueues;
+ private final SamplePool mSamplePool = new SamplePool();
+ private long mLastBufferedPositionUs = C.UNKNOWN_TIME_US;
+ private long mCurrentPlaybackPositionUs = 0;
+
+ // An error in I/O thread of {@link SampleChunkIoHelper} will be notified.
+ private volatile boolean mError;
+
+ // Eos was reached in I/O thread of {@link SampleChunkIoHelper}.
+ private volatile boolean mEos;
+ private SampleChunkIoHelper mSampleChunkIoHelper;
+ private final SampleChunkIoHelper.IoCallback mIoCallback =
+ new SampleChunkIoHelper.IoCallback() {
+ @Override
+ public void onIoReachedEos() {
+ mEos = true;
+ }
+
+ @Override
+ public void onIoError() {
+ mError = true;
+ }
+ };
+
+ /**
+ * Creates {@link BufferManager.SampleBuffer} with cached I/O backed by physical storage (e.g.
+ * trickplay,recording,recorded-playback).
+ *
+ * @param bufferManager the manager of {@link SampleChunk}
+ * @param bufferListener the listener for buffer I/O event
+ * @param enableTrickplay {@code true} when trickplay should be enabled
+ * @param bufferReason the reason for caching samples {@link RecordingSampleBuffer.BufferReason}
+ */
+ public RecordingSampleBuffer(
+ BufferManager bufferManager,
+ PlaybackBufferListener bufferListener,
+ boolean enableTrickplay,
+ @BufferReason int bufferReason) {
+ mBufferManager = bufferManager;
+ mBufferListener = bufferListener;
+ if (bufferListener != null) {
+ bufferListener.onBufferStateChanged(enableTrickplay);
+ }
+ mBufferReason = bufferReason;
+ }
+
+ @Override
+ public void init(@NonNull List<String> ids, @NonNull List<MediaFormat> mediaFormats)
+ throws IOException {
+ mTrackCount = ids.size();
+ if (mTrackCount <= 0) {
+ throw new IOException("No tracks to initialize");
+ }
+ mTrackSelected = new boolean[mTrackCount];
+ mReadSampleQueues = new ArrayList<>();
+ mSampleChunkIoHelper =
+ new SampleChunkIoHelper(
+ ids, mediaFormats, mBufferReason, mBufferManager, mSamplePool, mIoCallback);
+ for (int i = 0; i < mTrackCount; ++i) {
+ mReadSampleQueues.add(i, new SampleQueue(mSamplePool));
+ }
+ mSampleChunkIoHelper.init();
+ for (int i = 0; i < mTrackCount; ++i) {
+ mBufferManager.registerChunkEvictedListener(ids.get(i), RecordingSampleBuffer.this);
+ }
+ }
+
+ @Override
+ public void selectTrack(int index) {
+ if (!mTrackSelected[index]) {
+ mTrackSelected[index] = true;
+ mReadSampleQueues.get(index).clear();
+ mSampleChunkIoHelper.openRead(index, mCurrentPlaybackPositionUs);
+ }
+ }
+
+ @Override
+ public void deselectTrack(int index) {
+ if (mTrackSelected[index]) {
+ mTrackSelected[index] = false;
+ mReadSampleQueues.get(index).clear();
+ mSampleChunkIoHelper.closeRead(index);
+ }
+ }
+
+ @Override
+ public void writeSample(int index, SampleHolder sample, ConditionVariable conditionVariable)
+ throws IOException {
+ mSampleChunkIoHelper.writeSample(index, sample, conditionVariable);
+
+ if (!conditionVariable.block(BUFFER_WRITE_TIMEOUT_MS)) {
+ Log.e(TAG, "Error: Serious delay on writing buffer");
+ conditionVariable.block();
+ }
+ }
+
+ @Override
+ public boolean isWriteSpeedSlow(int sampleSize, long writeDurationNs) {
+ if (mBufferReason == BUFFER_REASON_RECORDED_PLAYBACK) {
+ return false;
+ }
+ mBufferManager.addWriteStat(sampleSize, writeDurationNs);
+ return mBufferManager.isWriteSlow();
+ }
+
+ @Override
+ public void handleWriteSpeedSlow() throws IOException {
+ if (mBufferReason == BUFFER_REASON_RECORDING) {
+ // Recording does not need to stop because I/O speed is slow temporarily.
+ // If fixed size buffer of TsStreamer overflows, TsDataSource will reach EoS.
+ // Reaching EoS will stop recording eventually.
+ Log.w(
+ TAG,
+ "Disk I/O speed is slow for recording temporarily: "
+ + mBufferManager.getWriteBandwidth()
+ + "MBps");
+ return;
+ }
+ // Disables buffering samples afterwards, and notifies the disk speed is slow.
+ Log.w(TAG, "Disk is too slow for trickplay");
+ mBufferListener.onDiskTooSlow();
+ }
+
+ @Override
+ public void setEos() {
+ mSampleChunkIoHelper.closeWrite();
+ }
+
+ private boolean maybeReadSample(SampleQueue queue, int index) {
+ if (queue.getLastQueuedPositionUs() != null
+ && queue.getLastQueuedPositionUs() > mCurrentPlaybackPositionUs + BUFFER_NEEDED_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.
+ // But, the throttling should provide enough samples for the player to
+ // finish the buffering state.
+ return false;
+ }
+ SampleHolder sample = mSampleChunkIoHelper.readSample(index);
+ if (sample != null) {
+ queue.queueSample(sample);
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public int readSample(int track, SampleHolder outSample) {
+ Assertions.checkState(mTrackSelected[track]);
+ maybeReadSample(mReadSampleQueues.get(track), track);
+ int result = mReadSampleQueues.get(track).dequeueSample(outSample);
+ if ((result != SampleSource.SAMPLE_READ && mEos) || mError) {
+ return SampleSource.END_OF_STREAM;
+ }
+ return result;
+ }
+
+ @Override
+ public void seekTo(long positionUs) {
+ for (int i = 0; i < mTrackCount; ++i) {
+ if (mTrackSelected[i]) {
+ mReadSampleQueues.get(i).clear();
+ mSampleChunkIoHelper.openRead(i, positionUs);
+ }
+ }
+ mLastBufferedPositionUs = positionUs;
+ }
+
+ @Override
+ public long getBufferedPositionUs() {
+ Long result = null;
+ for (int i = 0; i < mTrackCount; ++i) {
+ if (!mTrackSelected[i]) {
+ continue;
+ }
+ Long lastQueuedSamplePositionUs = mReadSampleQueues.get(i).getLastQueuedPositionUs();
+ if (lastQueuedSamplePositionUs == null) {
+ // No sample has been queued.
+ result = mLastBufferedPositionUs;
+ continue;
+ }
+ if (result == null || result > lastQueuedSamplePositionUs) {
+ result = lastQueuedSamplePositionUs;
+ }
+ }
+ if (result == null) {
+ return mLastBufferedPositionUs;
+ }
+ return (mLastBufferedPositionUs = result);
+ }
+
+ @Override
+ public boolean continueBuffering(long positionUs) {
+ mCurrentPlaybackPositionUs = positionUs;
+ for (int i = 0; i < mTrackCount; ++i) {
+ if (!mTrackSelected[i]) {
+ continue;
+ }
+ SampleQueue queue = mReadSampleQueues.get(i);
+ maybeReadSample(queue, i);
+ if (queue.getLastQueuedPositionUs() == null
+ || positionUs > queue.getLastQueuedPositionUs()) {
+ // No more buffered data.
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public void release() throws IOException {
+ if (mTrackCount <= 0) {
+ return;
+ }
+ if (mSampleChunkIoHelper != null) {
+ mSampleChunkIoHelper.release();
+ }
+ }
+
+ // onChunkEvictedListener
+ @Override
+ public void onChunkEvicted(String id, long createdTimeMs) {
+ if (mBufferListener != null) {
+ mBufferListener.onBufferStartTimeChanged(
+ 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
new file mode 100644
index 00000000..023d3295
--- /dev/null
+++ b/src/com/android/tv/tuner/exoplayer/buffer/SampleChunk.java
@@ -0,0 +1,428 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.exoplayer.buffer;
+
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.util.Log;
+import com.google.android.exoplayer.SampleHolder;
+import java.io.File;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.channels.FileChannel;
+
+/**
+ * {@link SampleChunk} stores samples into file and makes them available for read. Stored file = {
+ * Header, Sample } * N Header = sample size : int, sample flag : int, sample PTS in micro second :
+ * long
+ */
+public class SampleChunk {
+ private static final String TAG = "SampleChunk";
+ private static final boolean DEBUG = false;
+
+ private final long mCreatedTimeMs;
+ private final long mStartPositionUs;
+ private SampleChunk mNextChunk;
+
+ // Header = sample size : int, sample flag : int, sample PTS in micro second : long
+ private static final int SAMPLE_HEADER_LENGTH = 16;
+
+ private final File mFile;
+ private final ChunkCallback mChunkCallback;
+ private final SamplePool mSamplePool;
+ private RandomAccessFile mAccessFile;
+ private long mWriteOffset;
+ private boolean mWriteFinished;
+ private boolean mIsReading;
+ private boolean mIsWriting;
+
+ /** A callback for chunks being committed to permanent storage. */
+ public abstract static class ChunkCallback {
+
+ /**
+ * Notifies when writing a SampleChunk is completed.
+ *
+ * @param chunk SampleChunk which is written completely
+ */
+ public void onChunkWrite(SampleChunk chunk) {}
+
+ /**
+ * Notifies when a SampleChunk is deleted.
+ *
+ * @param chunk SampleChunk which is deleted from storage
+ */
+ public void onChunkDelete(SampleChunk chunk) {}
+ }
+
+ /** A class for SampleChunk creation. */
+ public static class SampleChunkCreator {
+
+ /**
+ * Returns a newly created SampleChunk to read & write samples.
+ *
+ * @param samplePool sample allocator
+ * @param file filename which will be created newly
+ * @param startPositionUs the start position of the earliest sample to be stored
+ * @param chunkCallback for total storage usage change notification
+ */
+ SampleChunk createSampleChunk(
+ SamplePool samplePool,
+ File file,
+ long startPositionUs,
+ ChunkCallback chunkCallback) {
+ return new SampleChunk(
+ samplePool, file, startPositionUs, System.currentTimeMillis(), chunkCallback);
+ }
+
+ /**
+ * Returns a newly created SampleChunk which is backed by an existing file. Created
+ * SampleChunk is read-only.
+ *
+ * @param samplePool sample allocator
+ * @param bufferDir the directory where the file to read is located
+ * @param filename the filename which will be read afterwards
+ * @param startPositionUs the start position of the earliest sample in the file
+ * @param chunkCallback for total storage usage change notification
+ * @param prev the previous SampleChunk just before the newly created SampleChunk
+ * @throws IOException
+ */
+ SampleChunk loadSampleChunkFromFile(
+ SamplePool samplePool,
+ File bufferDir,
+ String filename,
+ long startPositionUs,
+ ChunkCallback chunkCallback,
+ SampleChunk prev)
+ throws IOException {
+ File file = new File(bufferDir, filename);
+ SampleChunk chunk = new SampleChunk(samplePool, file, startPositionUs, chunkCallback);
+ if (prev != null) {
+ prev.mNextChunk = chunk;
+ }
+ return chunk;
+ }
+ }
+
+ /**
+ * Handles I/O for SampleChunk. Maintains current SampleChunk and the current offset for next
+ * I/O operation.
+ */
+ static class IoState {
+ private SampleChunk mChunk;
+ private long mCurrentOffset;
+
+ private boolean equals(SampleChunk chunk, long offset) {
+ return chunk == mChunk && mCurrentOffset == offset;
+ }
+
+ /** Returns whether read I/O operation is finished. */
+ boolean isReadFinished() {
+ return mChunk == null;
+ }
+
+ /** Returns the start position of the current SampleChunk */
+ long getStartPositionUs() {
+ return mChunk == null ? 0 : mChunk.getStartPositionUs();
+ }
+
+ private void reset(@Nullable SampleChunk chunk) {
+ mChunk = chunk;
+ 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, long offset) throws IOException {
+ if (mChunk != null) {
+ mChunk.closeRead();
+ }
+ chunk.openRead();
+ reset(chunk, offset);
+ }
+
+ /**
+ * Prepares for write I/O operation to a new SampleChunk.
+ *
+ * @param chunk the new SampleChunk to write samples afterwards
+ * @throws IOException
+ */
+ void openWrite(SampleChunk chunk) throws IOException {
+ if (mChunk != null) {
+ mChunk.closeWrite(chunk);
+ }
+ chunk.openWrite();
+ reset(chunk);
+ }
+
+ /**
+ * Reads a sample if it is available.
+ *
+ * @return Returns a sample if it is available, null otherwise.
+ * @throws IOException
+ */
+ SampleHolder read() throws IOException {
+ if (mChunk != null && mChunk.isReadFinished(this)) {
+ SampleChunk next = mChunk.mNextChunk;
+ mChunk.closeRead();
+ if (next != null) {
+ next.openRead();
+ }
+ reset(next);
+ }
+ if (mChunk != null) {
+ try {
+ return mChunk.read(this);
+ } catch (IllegalStateException e) {
+ // Write is finished and there is no additional buffer to read.
+ Log.w(TAG, "Tried to read sample over EOS.");
+ return null;
+ }
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Writes a sample.
+ *
+ * @param sample to write
+ * @param nextChunk if this is {@code null} writes at the current SampleChunk, otherwise
+ * close current SampleChunk and writes at this
+ * @throws IOException
+ */
+ void write(SampleHolder sample, SampleChunk nextChunk) throws IOException {
+ if (nextChunk != null) {
+ if (mChunk == null || mChunk.mNextChunk != null) {
+ throw new IllegalStateException("Requested write for wrong SampleChunk");
+ }
+ mChunk.closeWrite(nextChunk);
+ mChunk.mChunkCallback.onChunkWrite(mChunk);
+ nextChunk.openWrite();
+ reset(nextChunk);
+ }
+ mChunk.write(sample, this);
+ }
+
+ /**
+ * Finishes write I/O operation.
+ *
+ * @throws IOException
+ */
+ void closeWrite() throws IOException {
+ if (mChunk != null) {
+ mChunk.closeWrite(null);
+ }
+ }
+
+ /** 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
+ * @param delete {@code true} when the backed file needs to be deleted, {@code false}
+ * otherwise.
+ */
+ static void release(SampleChunk chunk, boolean delete) {
+ chunk.release(delete);
+ }
+ }
+
+ @VisibleForTesting
+ protected SampleChunk(
+ SamplePool samplePool,
+ File file,
+ long startPositionUs,
+ long createdTimeMs,
+ ChunkCallback chunkCallback) {
+ mStartPositionUs = startPositionUs;
+ mCreatedTimeMs = createdTimeMs;
+ mSamplePool = samplePool;
+ mFile = file;
+ mChunkCallback = chunkCallback;
+ }
+
+ // Constructor of SampleChunk which is backed by the given existing file.
+ private SampleChunk(
+ SamplePool samplePool, File file, long startPositionUs, ChunkCallback chunkCallback)
+ throws IOException {
+ mStartPositionUs = startPositionUs;
+ mCreatedTimeMs = mStartPositionUs / 1000;
+ mSamplePool = samplePool;
+ mFile = file;
+ mChunkCallback = chunkCallback;
+ mWriteFinished = true;
+ }
+
+ private void openRead() throws IOException {
+ if (!mIsReading) {
+ if (mAccessFile == null) {
+ mAccessFile = new RandomAccessFile(mFile, "r");
+ }
+ if (mWriteFinished && mWriteOffset == 0) {
+ // Lazy loading of write offset, in order not to load
+ // all SampleChunk's write offset at start time of recorded playback.
+ mWriteOffset = mAccessFile.length();
+ }
+ mIsReading = true;
+ }
+ }
+
+ private void openWrite() throws IOException {
+ if (mWriteFinished) {
+ throw new IllegalStateException("Opened for write though write is already finished");
+ }
+ if (!mIsWriting) {
+ if (mIsReading) {
+ throw new IllegalStateException(
+ "Write is requested for " + "an already opened SampleChunk");
+ }
+ mAccessFile = new RandomAccessFile(mFile, "rw");
+ mIsWriting = true;
+ }
+ }
+
+ private void CloseAccessFileIfNeeded() throws IOException {
+ if (!mIsReading && !mIsWriting) {
+ try {
+ if (mAccessFile != null) {
+ mAccessFile.close();
+ }
+ } finally {
+ mAccessFile = null;
+ }
+ }
+ }
+
+ private void closeRead() throws IOException {
+ if (mIsReading) {
+ mIsReading = false;
+ CloseAccessFileIfNeeded();
+ }
+ }
+
+ private void closeWrite(SampleChunk nextChunk) throws IOException {
+ if (mIsWriting) {
+ mNextChunk = nextChunk;
+ mIsWriting = false;
+ mWriteFinished = true;
+ CloseAccessFileIfNeeded();
+ }
+ }
+
+ private boolean isReadFinished(IoState state) {
+ return mWriteFinished && state.equals(this, mWriteOffset);
+ }
+
+ private SampleHolder read(IoState state) throws IOException {
+ if (mAccessFile == null || state.mChunk != this) {
+ throw new IllegalStateException("Requested read for wrong SampleChunk");
+ }
+ long offset = state.mCurrentOffset;
+ if (offset >= mWriteOffset) {
+ if (mWriteFinished) {
+ throw new IllegalStateException("Requested read for wrong range");
+ } else {
+ if (offset != mWriteOffset) {
+ Log.e(TAG, "This should not happen!");
+ }
+ return null;
+ }
+ }
+ mAccessFile.seek(offset);
+ int size = mAccessFile.readInt();
+ SampleHolder sample = mSamplePool.acquireSample(size);
+ sample.size = size;
+ sample.flags = mAccessFile.readInt();
+ sample.timeUs = mAccessFile.readLong();
+ sample.clearData();
+ sample.data.put(
+ mAccessFile
+ .getChannel()
+ .map(
+ FileChannel.MapMode.READ_ONLY,
+ offset + SAMPLE_HEADER_LENGTH,
+ sample.size));
+ offset += sample.size + SAMPLE_HEADER_LENGTH;
+ state.mCurrentOffset = offset;
+ return sample;
+ }
+
+ @VisibleForTesting
+ protected void write(SampleHolder sample, IoState state) throws IOException {
+ if (mAccessFile == null || mNextChunk != null || !state.equals(this, mWriteOffset)) {
+ throw new IllegalStateException("Requested write for wrong SampleChunk");
+ }
+
+ mAccessFile.seek(mWriteOffset);
+ mAccessFile.writeInt(sample.size);
+ mAccessFile.writeInt(sample.flags);
+ mAccessFile.writeLong(sample.timeUs);
+ sample.data.position(0).limit(sample.size);
+ mAccessFile.getChannel().position(mWriteOffset + SAMPLE_HEADER_LENGTH).write(sample.data);
+ mWriteOffset += sample.size + SAMPLE_HEADER_LENGTH;
+ state.mCurrentOffset = mWriteOffset;
+ }
+
+ private void release(boolean delete) {
+ mWriteFinished = true;
+ mIsReading = mIsWriting = false;
+ try {
+ if (mAccessFile != null) {
+ mAccessFile.close();
+ }
+ } catch (IOException e) {
+ // Since the SampleChunk will not be reused, ignore exception.
+ }
+ if (delete) {
+ mFile.delete();
+ mChunkCallback.onChunkDelete(this);
+ }
+ }
+
+ /** Returns the start position. */
+ public long getStartPositionUs() {
+ return mStartPositionUs;
+ }
+
+ /** Returns the creation time. */
+ public long getCreatedTimeMs() {
+ return mCreatedTimeMs;
+ }
+
+ /** Returns the current size. */
+ public long getSize() {
+ return mWriteOffset;
+ }
+}
diff --git a/src/com/android/tv/tuner/exoplayer/buffer/SampleChunkIoHelper.java b/src/com/android/tv/tuner/exoplayer/buffer/SampleChunkIoHelper.java
new file mode 100644
index 00000000..06fd6558
--- /dev/null
+++ b/src/com/android/tv/tuner/exoplayer/buffer/SampleChunkIoHelper.java
@@ -0,0 +1,464 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.exoplayer.buffer;
+
+import android.media.MediaCodec;
+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;
+import com.android.tv.common.SoftPreconditions;
+import com.android.tv.tuner.exoplayer.buffer.RecordingSampleBuffer.BufferReason;
+import com.google.android.exoplayer.MediaFormat;
+import com.google.android.exoplayer.SampleHolder;
+import com.google.android.exoplayer.util.MimeTypes;
+import java.io.IOException;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ConcurrentLinkedQueue;
+
+/**
+ * Handles all {@link SampleChunk} I/O operations. An I/O dedicated thread handles all I/O
+ * operations for synchronization.
+ */
+public class SampleChunkIoHelper implements Handler.Callback {
+ private static final String TAG = "SampleChunkIoHelper";
+
+ private static final int MAX_READ_BUFFER_SAMPLES = 3;
+ private static final int READ_RESCHEDULING_DELAY_MS = 10;
+
+ private static final int MSG_OPEN_READ = 1;
+ private static final int MSG_OPEN_WRITE = 2;
+ 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;
+ private final @BufferReason int mBufferReason;
+ private final BufferManager mBufferManager;
+ private final SamplePool mSamplePool;
+ private final IoCallback mIoCallback;
+
+ private Handler mIoHandler;
+ private final ConcurrentLinkedQueue<SampleHolder> mReadSampleBuffers[];
+ private final ConcurrentLinkedQueue<SampleHolder> mHandlerReadSampleBuffers[];
+ 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;
+ private boolean mFinished;
+
+ /** A Callback for I/O events. */
+ public abstract static class IoCallback {
+
+ /** Called when there is no sample to read. */
+ public void onIoReachedEos() {}
+
+ /** Called when there is an irrecoverable error during I/O. */
+ public void onIoError() {}
+ }
+
+ private class IoParams {
+ private final int index;
+ private final long positionUs;
+ private final SampleHolder sample;
+ private final ConditionVariable conditionVariable;
+ private final ConcurrentLinkedQueue<SampleHolder> readSampleBuffer;
+
+ private IoParams(
+ int index,
+ long positionUs,
+ SampleHolder sample,
+ ConditionVariable conditionVariable,
+ ConcurrentLinkedQueue<SampleHolder> readSampleBuffer) {
+ this.index = index;
+ this.positionUs = positionUs;
+ this.sample = sample;
+ this.conditionVariable = conditionVariable;
+ this.readSampleBuffer = readSampleBuffer;
+ }
+ }
+
+ /**
+ * Creates {@link SampleChunk} I/O handler.
+ *
+ * @param ids track names
+ * @param mediaFormats {@link android.media.MediaFormat} for each track
+ * @param bufferReason reason to be buffered
+ * @param bufferManager manager of {@link SampleChunk} collections
+ * @param samplePool allocator for a sample
+ * @param ioCallback listeners for I/O events
+ */
+ public SampleChunkIoHelper(
+ List<String> ids,
+ List<MediaFormat> mediaFormats,
+ @BufferReason int bufferReason,
+ BufferManager bufferManager,
+ SamplePool samplePool,
+ IoCallback ioCallback) {
+ mTrackCount = ids.size();
+ mIds = ids;
+ mMediaFormats = mediaFormats;
+ mBufferReason = bufferReason;
+ mBufferManager = bufferManager;
+ mSamplePool = samplePool;
+ mIoCallback = ioCallback;
+
+ mReadSampleBuffers = new ConcurrentLinkedQueue[mTrackCount];
+ mHandlerReadSampleBuffers = new ConcurrentLinkedQueue[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) {
+ mWriteIndexEndPositionUs[i] = RecordingSampleBuffer.MIN_SEEK_DURATION_US;
+ mWriteChunkEndPositionUs[i] = mSampleChunkDurationUs;
+ mReadIoStates[i] = new SampleChunk.IoState();
+ mWriteIoStates[i] = new SampleChunk.IoState();
+ }
+ }
+
+ /**
+ * Prepares and initializes for I/O operations.
+ *
+ * @throws IOException
+ */
+ public void init() throws IOException {
+ HandlerThread handlerThread = new HandlerThread(TAG);
+ handlerThread.start();
+ mIoHandler = new Handler(handlerThread.getLooper(), this);
+ if (mBufferReason == RecordingSampleBuffer.BUFFER_REASON_RECORDED_PLAYBACK) {
+ for (int i = 0; i < mTrackCount; ++i) {
+ mBufferManager.loadTrackFromStorage(mIds.get(i), mSamplePool);
+ }
+ mWriteEnded = true;
+ } else {
+ for (int i = 0; i < mTrackCount; ++i) {
+ mIoHandler.sendMessage(mIoHandler.obtainMessage(MSG_OPEN_WRITE, i));
+ }
+ }
+ }
+
+ /**
+ * Reads a sample if it is available.
+ *
+ * @param index track index
+ * @return {@code null} if a sample is not available, otherwise returns a sample
+ */
+ public SampleHolder readSample(int index) {
+ SampleHolder sample = mReadSampleBuffers[index].poll();
+ mIoHandler.sendMessage(mIoHandler.obtainMessage(MSG_READ, index));
+ return sample;
+ }
+
+ /**
+ * Writes a sample.
+ *
+ * @param index track index
+ * @param sample to write
+ * @param conditionVariable which will be wait until the write is finished
+ * @throws IOException
+ */
+ public void writeSample(int index, SampleHolder sample, ConditionVariable conditionVariable)
+ throws IOException {
+ if (mErrorNotified) {
+ throw new IOException("Storage I/O error happened");
+ }
+ conditionVariable.close();
+ IoParams params = new IoParams(index, 0, sample, conditionVariable, null);
+ mIoHandler.sendMessage(mIoHandler.obtainMessage(MSG_WRITE, params));
+ }
+
+ /**
+ * Starts read from the specified position.
+ *
+ * @param index track index
+ * @param positionUs the specified position
+ */
+ public void openRead(int index, long positionUs) {
+ // Old mReadSampleBuffers may have a pending read.
+ mReadSampleBuffers[index] = new ConcurrentLinkedQueue<>();
+ IoParams params = new IoParams(index, positionUs, null, null, mReadSampleBuffers[index]);
+ mIoHandler.sendMessage(mIoHandler.obtainMessage(MSG_OPEN_READ, params));
+ }
+
+ /**
+ * Closes read from the specified track.
+ *
+ * @param index track index
+ */
+ public void closeRead(int index) {
+ mIoHandler.sendMessage(mIoHandler.obtainMessage(MSG_CLOSE_READ, index));
+ }
+
+ /** Notifies writes are finished. */
+ public void closeWrite() {
+ mIoHandler.sendEmptyMessage(MSG_CLOSE_WRITE);
+ }
+
+ /**
+ * Finishes I/O operations and releases all the resources.
+ *
+ * @throws IOException
+ */
+ public void release() throws IOException {
+ if (mIoHandler == null) {
+ return;
+ }
+ // Finishes all I/O operations.
+ ConditionVariable conditionVariable = new ConditionVariable();
+ mIoHandler.sendMessage(mIoHandler.obtainMessage(MSG_RELEASE, conditionVariable));
+ conditionVariable.block();
+
+ for (int i = 0; i < mTrackCount; ++i) {
+ mBufferManager.unregisterChunkEvictedListener(mIds.get(i));
+ }
+ try {
+ if (mBufferReason == RecordingSampleBuffer.BUFFER_REASON_RECORDING && mTrackCount > 0) {
+ // Saves meta information for recording.
+ 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 (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(audios, videos);
+ }
+ } finally {
+ mBufferManager.release();
+ mIoHandler.getLooper().quitSafely();
+ }
+ }
+
+ @Override
+ public boolean handleMessage(Message message) {
+ if (mFinished) {
+ return true;
+ }
+ releaseEvictedChunks();
+ try {
+ switch (message.what) {
+ case MSG_OPEN_READ:
+ doOpenRead((IoParams) message.obj);
+ return true;
+ 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;
+ case MSG_READ:
+ doRead((int) message.obj);
+ return true;
+ case MSG_WRITE:
+ doWrite((IoParams) message.obj);
+ // Since only write will increase storage, eviction will be handled here.
+ return true;
+ case MSG_RELEASE:
+ doRelease((ConditionVariable) message.obj);
+ return true;
+ }
+ } catch (IOException e) {
+ mIoCallback.onIoError();
+ mErrorNotified = true;
+ Log.e(TAG, "IoException happened", e);
+ return true;
+ }
+ return false;
+ }
+
+ private void doOpenRead(IoParams params) throws IOException {
+ int index = params.index;
+ mIoHandler.removeMessages(MSG_READ, index);
+ 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(readPosition, TAG, errorMessage);
+ throw new IOException(errorMessage);
+ }
+ mSelectedTracks.add(index);
+ mReadIoStates[index].openRead(readPosition.first, (long) readPosition.second);
+ if (mHandlerReadSampleBuffers[index] != null) {
+ SampleHolder sample;
+ while ((sample = mHandlerReadSampleBuffers[index].poll()) != null) {
+ mSamplePool.releaseSample(sample);
+ }
+ }
+ mHandlerReadSampleBuffers[index] = params.readSampleBuffer;
+ mIoHandler.sendMessage(mIoHandler.obtainMessage(MSG_READ, index));
+ }
+
+ private void doOpenWrite(int index) throws IOException {
+ 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) {
+ // If enough samples are buffered, try again few moments later hoping that
+ // buffered samples are consumed.
+ mIoHandler.sendMessageDelayed(
+ mIoHandler.obtainMessage(MSG_READ, index), READ_RESCHEDULING_DELAY_MS);
+ } else {
+ if (mReadIoStates[index].isReadFinished()) {
+ for (int i = 0; i < mTrackCount; ++i) {
+ if (!mReadIoStates[i].isReadFinished()) {
+ return;
+ }
+ }
+ mIoCallback.onIoReachedEos();
+ return;
+ }
+ SampleHolder sample = mReadIoStates[index].read();
+ if (sample != null) {
+ mHandlerReadSampleBuffers[index].offer(sample);
+ } else {
+ // Read reached write but write is not finished yet --- wait a few moments to
+ // see if another sample is written.
+ mIoHandler.sendMessageDelayed(
+ mIoHandler.obtainMessage(MSG_READ, index), READ_RESCHEDULING_DELAY_MS);
+ }
+ }
+ }
+
+ private void doWrite(IoParams params) throws IOException {
+ try {
+ if (mWriteEnded) {
+ SoftPreconditions.checkState(false);
+ return;
+ }
+ int index = params.index;
+ SampleHolder sample = params.sample;
+ SampleChunk nextChunk = null;
+ if ((sample.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0) {
+ if (sample.timeUs > mBufferDurationUs) {
+ mBufferDurationUs = sample.timeUs;
+ }
+ 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);
+ } finally {
+ params.conditionVariable.open();
+ }
+ }
+
+ private void doCloseWrite() throws IOException {
+ if (mWriteEnded) {
+ return;
+ }
+ mWriteEnded = true;
+ boolean readFinished = true;
+ for (int i = 0; i < mTrackCount; ++i) {
+ readFinished = readFinished && mReadIoStates[i].isReadFinished();
+ mWriteIoStates[i].closeWrite();
+ }
+ if (readFinished) {
+ mIoCallback.onIoReachedEos();
+ }
+ }
+
+ private void doRelease(ConditionVariable conditionVariable) {
+ mIoHandler.removeCallbacksAndMessages(null);
+ mFinished = true;
+ conditionVariable.open();
+ mSelectedTracks.clear();
+ }
+
+ private void releaseEvictedChunks() {
+ 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)), currentStartPositionUs);
+ mBufferManager.evictChunks(mIds.get(i), evictEndPositionUs);
+ }
+ }
+}
diff --git a/src/com/android/tv/tuner/exoplayer/buffer/SamplePool.java b/src/com/android/tv/tuner/exoplayer/buffer/SamplePool.java
new file mode 100644
index 00000000..b89a14db
--- /dev/null
+++ b/src/com/android/tv/tuner/exoplayer/buffer/SamplePool.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.exoplayer.buffer;
+
+import com.google.android.exoplayer.SampleHolder;
+import java.util.LinkedList;
+
+/** Pool of samples to recycle ByteBuffers as much as possible. */
+public class SamplePool {
+ private final LinkedList<SampleHolder> mSamplePool = new LinkedList<>();
+
+ /**
+ * Acquires a sample with a buffer larger than size from the pool. Allocate new one or resize an
+ * existing buffer if necessary.
+ */
+ public synchronized SampleHolder acquireSample(int size) {
+ if (mSamplePool.isEmpty()) {
+ SampleHolder sample = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL);
+ sample.ensureSpaceForWrite(size);
+ return sample;
+ }
+ SampleHolder smallestSufficientSample = null;
+ SampleHolder maxSample = mSamplePool.getFirst();
+ for (SampleHolder sample : mSamplePool) {
+ // Grab the smallest sufficient sample.
+ if (sample.data.capacity() >= size
+ && (smallestSufficientSample == null
+ || smallestSufficientSample.data.capacity() > sample.data.capacity())) {
+ smallestSufficientSample = sample;
+ }
+
+ // Grab the max size sample.
+ if (maxSample.data.capacity() < sample.data.capacity()) {
+ maxSample = sample;
+ }
+ }
+ SampleHolder sampleFromPool = smallestSufficientSample;
+
+ // If there's no sufficient sample, grab the maximum sample and resize it to size.
+ if (sampleFromPool == null) {
+ sampleFromPool = maxSample;
+ sampleFromPool.ensureSpaceForWrite(size);
+ }
+ mSamplePool.remove(sampleFromPool);
+ return sampleFromPool;
+ }
+
+ /** Releases the sample back to the pool. */
+ public synchronized void releaseSample(SampleHolder sample) {
+ sample.clearData();
+ mSamplePool.offerLast(sample);
+ }
+}
diff --git a/src/com/android/tv/tuner/exoplayer/buffer/SampleQueue.java b/src/com/android/tv/tuner/exoplayer/buffer/SampleQueue.java
new file mode 100644
index 00000000..e208f2c2
--- /dev/null
+++ b/src/com/android/tv/tuner/exoplayer/buffer/SampleQueue.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.exoplayer.buffer;
+
+import com.google.android.exoplayer.SampleHolder;
+import com.google.android.exoplayer.SampleSource;
+import java.util.LinkedList;
+
+/** A sample queue which reads from the buffer and passes to player pipeline. */
+public class SampleQueue {
+ private final LinkedList<SampleHolder> mQueue = new LinkedList<>();
+ private final SamplePool mSamplePool;
+ private Long mLastQueuedPositionUs = null;
+
+ public SampleQueue(SamplePool samplePool) {
+ mSamplePool = samplePool;
+ }
+
+ public void queueSample(SampleHolder sample) {
+ mQueue.offer(sample);
+ mLastQueuedPositionUs = sample.timeUs;
+ }
+
+ public int dequeueSample(SampleHolder sample) {
+ SampleHolder sampleFromQueue = mQueue.poll();
+ if (sampleFromQueue == null) {
+ return SampleSource.NOTHING_READ;
+ }
+ sample.ensureSpaceForWrite(sampleFromQueue.size);
+ sample.size = sampleFromQueue.size;
+ sample.flags = sampleFromQueue.flags;
+ sample.timeUs = sampleFromQueue.timeUs;
+ sample.clearData();
+ sampleFromQueue.data.position(0).limit(sample.size);
+ sample.data.put(sampleFromQueue.data);
+ mSamplePool.releaseSample(sampleFromQueue);
+ return SampleSource.SAMPLE_READ;
+ }
+
+ public void clear() {
+ while (!mQueue.isEmpty()) {
+ mSamplePool.releaseSample(mQueue.poll());
+ }
+ mLastQueuedPositionUs = null;
+ }
+
+ public Long getLastQueuedPositionUs() {
+ return mLastQueuedPositionUs;
+ }
+
+ public boolean isDurationGreaterThan(long durationUs) {
+ return !mQueue.isEmpty() && mQueue.getLast().timeUs - mQueue.getFirst().timeUs > durationUs;
+ }
+
+ public boolean isEmpty() {
+ return mQueue.isEmpty();
+ }
+}
diff --git a/src/com/android/tv/tuner/exoplayer/buffer/SimpleSampleBuffer.java b/src/com/android/tv/tuner/exoplayer/buffer/SimpleSampleBuffer.java
new file mode 100644
index 00000000..4c6260bf
--- /dev/null
+++ b/src/com/android/tv/tuner/exoplayer/buffer/SimpleSampleBuffer.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.exoplayer.buffer;
+
+import android.os.ConditionVariable;
+import android.support.annotation.NonNull;
+import com.android.tv.common.SoftPreconditions;
+import com.android.tv.tuner.exoplayer.SampleExtractor;
+import com.android.tv.tuner.tvinput.PlaybackBufferListener;
+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 java.io.IOException;
+import java.util.List;
+
+/**
+ * Handles I/O for {@link SampleExtractor} when physical storage based buffer is not used. Trickplay
+ * is disabled.
+ */
+public class SimpleSampleBuffer implements BufferManager.SampleBuffer {
+ private final SamplePool mSamplePool = new SamplePool();
+ private SampleQueue[] mPlayingSampleQueues;
+ private long mLastBufferedPositionUs = C.UNKNOWN_TIME_US;
+
+ private volatile boolean mEos;
+
+ public SimpleSampleBuffer(PlaybackBufferListener bufferListener) {
+ if (bufferListener != null) {
+ // Disables trickplay.
+ bufferListener.onBufferStateChanged(false);
+ }
+ }
+
+ @Override
+ public synchronized void init(
+ @NonNull List<String> ids, @NonNull List<MediaFormat> mediaFormats) {
+ int trackCount = ids.size();
+ mPlayingSampleQueues = new SampleQueue[trackCount];
+ for (int i = 0; i < trackCount; i++) {
+ mPlayingSampleQueues[i] = null;
+ }
+ }
+
+ @Override
+ public void setEos() {
+ mEos = true;
+ }
+
+ private boolean reachedEos() {
+ return mEos;
+ }
+
+ @Override
+ public void selectTrack(int index) {
+ synchronized (this) {
+ if (mPlayingSampleQueues[index] == null) {
+ mPlayingSampleQueues[index] = new SampleQueue(mSamplePool);
+ } else {
+ mPlayingSampleQueues[index].clear();
+ }
+ }
+ }
+
+ @Override
+ public void deselectTrack(int index) {
+ synchronized (this) {
+ if (mPlayingSampleQueues[index] != null) {
+ mPlayingSampleQueues[index].clear();
+ mPlayingSampleQueues[index] = null;
+ }
+ }
+ }
+
+ @Override
+ public synchronized long getBufferedPositionUs() {
+ Long result = null;
+ for (SampleQueue queue : mPlayingSampleQueues) {
+ if (queue == null) {
+ continue;
+ }
+ Long lastQueuedSamplePositionUs = queue.getLastQueuedPositionUs();
+ if (lastQueuedSamplePositionUs == null) {
+ // No sample has been queued.
+ result = mLastBufferedPositionUs;
+ continue;
+ }
+ if (result == null || result > lastQueuedSamplePositionUs) {
+ result = lastQueuedSamplePositionUs;
+ }
+ }
+ if (result == null) {
+ return mLastBufferedPositionUs;
+ }
+ return (mLastBufferedPositionUs = result);
+ }
+
+ @Override
+ public synchronized int readSample(int track, SampleHolder sampleHolder) {
+ SampleQueue queue = mPlayingSampleQueues[track];
+ SoftPreconditions.checkNotNull(queue);
+ int result = queue == null ? SampleSource.NOTHING_READ : queue.dequeueSample(sampleHolder);
+ if (result != SampleSource.SAMPLE_READ && reachedEos()) {
+ return SampleSource.END_OF_STREAM;
+ }
+ return result;
+ }
+
+ @Override
+ public void writeSample(int index, SampleHolder sample, ConditionVariable conditionVariable)
+ throws IOException {
+ sample.data.position(0).limit(sample.size);
+ SampleHolder sampleToQueue = mSamplePool.acquireSample(sample.size);
+ sampleToQueue.size = sample.size;
+ sampleToQueue.clearData();
+ sampleToQueue.data.put(sample.data);
+ sampleToQueue.timeUs = sample.timeUs;
+ sampleToQueue.flags = sample.flags;
+
+ synchronized (this) {
+ if (mPlayingSampleQueues[index] != null) {
+ mPlayingSampleQueues[index].queueSample(sampleToQueue);
+ }
+ }
+ }
+
+ @Override
+ public boolean isWriteSpeedSlow(int sampleSize, long durationNs) {
+ // Since SimpleSampleBuffer write samples only to memory (not to physical storage),
+ // write speed is always fine.
+ return false;
+ }
+
+ @Override
+ public void handleWriteSpeedSlow() {
+ // no-op
+ }
+
+ @Override
+ public synchronized boolean continueBuffering(long positionUs) {
+ for (SampleQueue queue : mPlayingSampleQueues) {
+ if (queue == null) {
+ continue;
+ }
+ if (queue.getLastQueuedPositionUs() == null
+ || positionUs > queue.getLastQueuedPositionUs()) {
+ // No more buffered data.
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public void seekTo(long positionUs) {
+ // Not used.
+ }
+
+ @Override
+ public void release() {
+ // Not used.
+ }
+}
diff --git a/src/com/android/tv/tuner/exoplayer/buffer/TrickplayStorageManager.java b/src/com/android/tv/tuner/exoplayer/buffer/TrickplayStorageManager.java
new file mode 100644
index 00000000..b22b8af1
--- /dev/null
+++ b/src/com/android/tv/tuner/exoplayer/buffer/TrickplayStorageManager.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.exoplayer.buffer;
+
+import android.content.Context;
+import android.os.AsyncTask;
+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)
+ private static final String SYS_STORAGE_THRESHOLD_PERCENTAGE =
+ "sys_storage_threshold_percentage";
+ private static final String SYS_STORAGE_THRESHOLD_MAX_BYTES = "sys_storage_threshold_max_bytes";
+
+ // Copied from android.os.StorageManager
+ private static final int DEFAULT_THRESHOLD_PERCENTAGE = 10;
+ private static final long DEFAULT_THRESHOLD_MAX_BYTES = 500L * 1024 * 1024;
+
+ private static AsyncTask<Void, Void, Void> sLastCacheCleanUpTask;
+ private static File sBufferDir;
+ private static long sStorageBufferBytes;
+
+ private final long mMaxBufferSize;
+
+ 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 lowPercentageToBytes = path.getTotalSpace() * lowPercentage / 100;
+ long maxLowBytes =
+ Settings.Global.getLong(
+ context.getContentResolver(),
+ SYS_STORAGE_THRESHOLD_MAX_BYTES,
+ DEFAULT_THRESHOLD_MAX_BYTES);
+ sStorageBufferBytes = Math.min(lowPercentageToBytes, maxLowBytes);
+ }
+
+ public TrickplayStorageManager(Context context, @NonNull File baseDir, long maxBufferSize) {
+ initParamsIfNeeded(context, new File(baseDir, BUFFER_DIR));
+ sBufferDir.mkdirs();
+ mMaxBufferSize = maxBufferSize;
+ clearStorage();
+ }
+
+ private void clearStorage() {
+ long now = System.currentTimeMillis();
+ if (sLastCacheCleanUpTask != null) {
+ sLastCacheCleanUpTask.cancel(true);
+ }
+ 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;
+ }
+ };
+ sLastCacheCleanUpTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+
+ @Override
+ public File getBufferDir() {
+ return sBufferDir;
+ }
+
+ @Override
+ public boolean isPersistent() {
+ return false;
+ }
+
+ @Override
+ public boolean reachedStorageMax(long bufferSize, long pendingDelete) {
+ return bufferSize - pendingDelete > mMaxBufferSize;
+ }
+
+ @Override
+ public boolean hasEnoughBuffer(long pendingDelete) {
+ return sBufferDir.getUsableSpace() + pendingDelete >= sStorageBufferBytes;
+ }
+
+ @Override
+ public List<BufferManager.TrackFormat> readTrackInfoFiles(boolean isAudio) {
+ return null;
+ }
+
+ @Override
+ public ArrayList<BufferManager.PositionHolder> readIndexFile(String trackId) {
+ return null;
+ }
+
+ @Override
+ public void writeTrackInfoFiles(List<BufferManager.TrackFormat> formatList, boolean isAudio) {}
+
+ @Override
+ 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..421192f1
--- /dev/null
+++ b/src/com/android/tv/tuner/exoplayer/ffmpeg/FfmpegDecoderClient.java
@@ -0,0 +1,247 @@
+/*
+ * 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.VisibleForTesting;
+import android.support.annotation.WorkerThread;
+import com.android.tv.Features;
+import com.android.tv.tuner.exoplayer.audio.AudioDecoder;
+import com.google.android.exoplayer.SampleHolder;
+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 static synchronized 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 static synchronized 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 static synchronized boolean isAvailable() {
+ if (sInstance != null) {
+ return sInstance.available();
+ }
+ return false;
+ }
+
+ /** Returns an client instance. */
+ public static synchronized 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..0172d817
--- /dev/null
+++ b/src/com/android/tv/tuner/exoplayer/ffmpeg/FfmpegDecoderService.java
@@ -0,0 +1,202 @@
+/*
+ * 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/layout/ScaledLayout.java b/src/com/android/tv/tuner/layout/ScaledLayout.java
new file mode 100644
index 00000000..dd92b641
--- /dev/null
+++ b/src/com/android/tv/tuner/layout/ScaledLayout.java
@@ -0,0 +1,290 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.layout;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.hardware.display.DisplayManager;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.Display;
+import android.view.View;
+import android.view.ViewGroup;
+import com.android.tv.tuner.R;
+import java.util.Arrays;
+import java.util.Comparator;
+
+/** A layout that scales its children using the given percentage value. */
+public class ScaledLayout extends ViewGroup {
+ private static final String TAG = "ScaledLayout";
+ private static final boolean DEBUG = false;
+ private static final Comparator<Rect> mRectTopLeftSorter =
+ new Comparator<Rect>() {
+ @Override
+ public int compare(Rect lhs, Rect rhs) {
+ if (lhs.top != rhs.top) {
+ return lhs.top - rhs.top;
+ } else {
+ return lhs.left - rhs.left;
+ }
+ }
+ };
+
+ private Rect[] mRectArray;
+ private final int mMaxWidth;
+ private final int mMaxHeight;
+
+ public ScaledLayout(Context context) {
+ this(context, null);
+ }
+
+ public ScaledLayout(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public ScaledLayout(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ Point size = new Point();
+ DisplayManager displayManager =
+ (DisplayManager) getContext().getSystemService(Context.DISPLAY_SERVICE);
+ Display display = displayManager.getDisplay(Display.DEFAULT_DISPLAY);
+ display.getRealSize(size);
+ mMaxWidth = size.x;
+ mMaxHeight = size.y;
+ }
+
+ /**
+ * ScaledLayoutParams stores the four scale factors. <br>
+ * Vertical coordinate system: ({@code scaleStartRow} * 100) % ~ ({@code scaleEndRow} * 100) %
+ * Horizontal coordinate system: ({@code scaleStartCol} * 100) % ~ ({@code scaleEndCol} * 100) %
+ * <br>
+ * In XML, for example,
+ *
+ * <pre>{@code
+ * <View
+ * app:layout_scaleStartRow="0.1"
+ * app:layout_scaleEndRow="0.5"
+ * app:layout_scaleStartCol="0.4"
+ * app:layout_scaleEndCol="1" />
+ * }</pre>
+ */
+ public static class ScaledLayoutParams extends ViewGroup.LayoutParams {
+ public static final float SCALE_UNSPECIFIED = -1;
+ public final float scaleStartRow;
+ public final float scaleEndRow;
+ public final float scaleStartCol;
+ public final float scaleEndCol;
+
+ public ScaledLayoutParams(
+ float scaleStartRow, float scaleEndRow, float scaleStartCol, float scaleEndCol) {
+ super(MATCH_PARENT, MATCH_PARENT);
+ this.scaleStartRow = scaleStartRow;
+ this.scaleEndRow = scaleEndRow;
+ this.scaleStartCol = scaleStartCol;
+ this.scaleEndCol = scaleEndCol;
+ }
+
+ public ScaledLayoutParams(Context context, AttributeSet attrs) {
+ super(MATCH_PARENT, MATCH_PARENT);
+ TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.utScaledLayout);
+ scaleStartRow =
+ array.getFloat(
+ R.styleable.utScaledLayout_layout_scaleStartRow, SCALE_UNSPECIFIED);
+ scaleEndRow =
+ array.getFloat(
+ R.styleable.utScaledLayout_layout_scaleEndRow, SCALE_UNSPECIFIED);
+ scaleStartCol =
+ array.getFloat(
+ R.styleable.utScaledLayout_layout_scaleStartCol, SCALE_UNSPECIFIED);
+ scaleEndCol =
+ array.getFloat(
+ R.styleable.utScaledLayout_layout_scaleEndCol, SCALE_UNSPECIFIED);
+ array.recycle();
+ }
+ }
+
+ @Override
+ public LayoutParams generateLayoutParams(AttributeSet attrs) {
+ return new ScaledLayoutParams(getContext(), attrs);
+ }
+
+ @Override
+ protected boolean checkLayoutParams(LayoutParams p) {
+ return (p instanceof ScaledLayoutParams);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
+ int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
+ int width = widthSpecSize - getPaddingLeft() - getPaddingRight();
+ int height = heightSpecSize - getPaddingTop() - getPaddingBottom();
+ if (DEBUG) {
+ Log.d(TAG, String.format("onMeasure width: %d, height: %d", width, height));
+ }
+ int count = getChildCount();
+ mRectArray = new Rect[count];
+ for (int i = 0; i < count; ++i) {
+ View child = getChildAt(i);
+ ViewGroup.LayoutParams params = child.getLayoutParams();
+ float scaleStartRow, scaleEndRow, scaleStartCol, scaleEndCol;
+ if (!(params instanceof ScaledLayoutParams)) {
+ throw new RuntimeException(
+ "A child of ScaledLayout cannot have the UNSPECIFIED scale factors");
+ }
+ scaleStartRow = ((ScaledLayoutParams) params).scaleStartRow;
+ scaleEndRow = ((ScaledLayoutParams) params).scaleEndRow;
+ scaleStartCol = ((ScaledLayoutParams) params).scaleStartCol;
+ scaleEndCol = ((ScaledLayoutParams) params).scaleEndCol;
+ if (scaleStartRow < 0 || scaleStartRow > 1) {
+ throw new RuntimeException(
+ "A child of ScaledLayout should have a range of "
+ + "scaleStartRow between 0 and 1");
+ }
+ if (scaleEndRow < scaleStartRow || scaleStartRow > 1) {
+ throw new RuntimeException(
+ "A child of ScaledLayout should have a range of "
+ + "scaleEndRow between scaleStartRow and 1");
+ }
+ if (scaleEndCol < 0 || scaleEndCol > 1) {
+ throw new RuntimeException(
+ "A child of ScaledLayout should have a range of "
+ + "scaleStartCol between 0 and 1");
+ }
+ if (scaleEndCol < scaleStartCol || scaleEndCol > 1) {
+ throw new RuntimeException(
+ "A child of ScaledLayout should have a range of "
+ + "scaleEndCol between scaleStartCol and 1");
+ }
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ String.format(
+ "onMeasure child scaleStartRow: %f scaleEndRow: %f "
+ + "scaleStartCol: %f scaleEndCol: %f",
+ scaleStartRow, scaleEndRow, scaleStartCol, scaleEndCol));
+ }
+ mRectArray[i] =
+ new Rect(
+ (int) (scaleStartCol * width),
+ (int) (scaleStartRow * height),
+ (int) (scaleEndCol * width),
+ (int) (scaleEndRow * height));
+ int scaleWidth = (int) (width * (scaleEndCol - scaleStartCol));
+ int childWidthSpec =
+ MeasureSpec.makeMeasureSpec(
+ scaleWidth > mMaxWidth ? mMaxWidth : scaleWidth, MeasureSpec.EXACTLY);
+ int childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+ child.measure(childWidthSpec, childHeightSpec);
+
+ // If the height of the measured child view is bigger than the height of the calculated
+ // region by the given ScaleLayoutParams, the height of the region should be increased
+ // to fit the size of the child view.
+ if (child.getMeasuredHeight() > mRectArray[i].height()) {
+ int overflowedHeight = child.getMeasuredHeight() - mRectArray[i].height();
+ overflowedHeight = (overflowedHeight + 1) / 2;
+ mRectArray[i].bottom += overflowedHeight;
+ mRectArray[i].top -= overflowedHeight;
+ if (mRectArray[i].top < 0) {
+ mRectArray[i].bottom -= mRectArray[i].top;
+ mRectArray[i].top = 0;
+ }
+ if (mRectArray[i].bottom > height) {
+ mRectArray[i].top -= mRectArray[i].bottom - height;
+ mRectArray[i].bottom = height;
+ }
+ }
+ int scaleHeight = (int) (height * (scaleEndRow - scaleStartRow));
+ childHeightSpec =
+ MeasureSpec.makeMeasureSpec(
+ scaleHeight > mMaxHeight ? mMaxHeight : scaleHeight,
+ MeasureSpec.EXACTLY);
+ child.measure(childWidthSpec, childHeightSpec);
+ }
+
+ // Avoid overlapping rectangles.
+ // Step 1. Sort rectangles by position (top-left).
+ int visibleRectCount = 0;
+ int[] visibleRectGroup = new int[count];
+ Rect[] visibleRectArray = new Rect[count];
+ for (int i = 0; i < count; ++i) {
+ if (getChildAt(i).getVisibility() == View.VISIBLE) {
+ visibleRectGroup[visibleRectCount] = visibleRectCount;
+ visibleRectArray[visibleRectCount] = mRectArray[i];
+ ++visibleRectCount;
+ }
+ }
+ Arrays.sort(visibleRectArray, 0, visibleRectCount, mRectTopLeftSorter);
+
+ // Step 2. Move down if there are overlapping rectangles.
+ for (int i = 0; i < visibleRectCount - 1; ++i) {
+ for (int j = i + 1; j < visibleRectCount; ++j) {
+ if (Rect.intersects(visibleRectArray[i], visibleRectArray[j])) {
+ visibleRectGroup[j] = visibleRectGroup[i];
+ visibleRectArray[j].set(
+ visibleRectArray[j].left,
+ visibleRectArray[i].bottom,
+ visibleRectArray[j].right,
+ visibleRectArray[i].bottom + visibleRectArray[j].height());
+ }
+ }
+ }
+
+ // Step 3. Move up if there is any overflowed rectangle.
+ for (int i = visibleRectCount - 1; i >= 0; --i) {
+ if (visibleRectArray[i].bottom > height) {
+ int overflowedHeight = visibleRectArray[i].bottom - height;
+ for (int j = 0; j <= i; ++j) {
+ if (visibleRectGroup[i] == visibleRectGroup[j]) {
+ visibleRectArray[j].set(
+ visibleRectArray[j].left,
+ visibleRectArray[j].top - overflowedHeight,
+ visibleRectArray[j].right,
+ visibleRectArray[j].bottom - overflowedHeight);
+ }
+ }
+ }
+ }
+ setMeasuredDimension(widthSpecSize, heightSpecSize);
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ int paddingLeft = getPaddingLeft();
+ int paddingTop = getPaddingTop();
+ int count = getChildCount();
+ for (int i = 0; i < count; ++i) {
+ View child = getChildAt(i);
+ if (child.getVisibility() != GONE) {
+ int childLeft = paddingLeft + mRectArray[i].left;
+ int childTop = paddingTop + mRectArray[i].top;
+ int childBottom = paddingLeft + mRectArray[i].bottom;
+ int childRight = paddingTop + mRectArray[i].right;
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ String.format(
+ "layoutChild bottom: %d left: %d right: %d top: %d",
+ childBottom, childLeft, childRight, childTop));
+ }
+ child.layout(childLeft, childTop, childRight, childBottom);
+ }
+ }
+ }
+}
diff --git a/src/com/android/tv/tuner/setup/ConnectionTypeFragment.java b/src/com/android/tv/tuner/setup/ConnectionTypeFragment.java
new file mode 100644
index 00000000..75d1c34c
--- /dev/null
+++ b/src/com/android/tv/tuner/setup/ConnectionTypeFragment.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.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 com.android.tv.common.ui.setup.SetupGuidedStepFragment;
+import com.android.tv.common.ui.setup.SetupMultiPaneFragment;
+import com.android.tv.tuner.R;
+import java.util.List;
+
+/** A fragment for connection type selection. */
+public class ConnectionTypeFragment extends SetupMultiPaneFragment {
+ public static final String ACTION_CATEGORY =
+ "com.android.tv.tuner.setup.ConnectionTypeFragment";
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ ((TunerSetupActivity) getActivity()).generateTunerHal();
+ super.onCreate(savedInstanceState);
+ }
+
+ @Override
+ public void onResume() {
+ ((TunerSetupActivity) getActivity()).generateTunerHal();
+ super.onResume();
+ }
+
+ @Override
+ public void onDestroy() {
+ ((TunerSetupActivity) getActivity()).clearTunerHal();
+ super.onDestroy();
+ }
+
+ @Override
+ protected SetupGuidedStepFragment onCreateContentFragment() {
+ return new ContentFragment();
+ }
+
+ @Override
+ protected String getActionCategory() {
+ return ACTION_CATEGORY;
+ }
+
+ @Override
+ protected boolean needsDoneButton() {
+ return false;
+ }
+
+ public static class ContentFragment extends SetupGuidedStepFragment {
+
+ @NonNull
+ @Override
+ public Guidance onCreateGuidance(Bundle savedInstanceState) {
+ return new Guidance(
+ getString(R.string.ut_connection_title),
+ getString(R.string.ut_connection_description),
+ getString(R.string.ut_setup_breadcrumb),
+ null);
+ }
+
+ @Override
+ public void onCreateActions(
+ @NonNull List<GuidedAction> actions, Bundle savedInstanceState) {
+ String[] choices = getResources().getStringArray(R.array.ut_connection_choices);
+ int length = choices.length - 1;
+ int startOffset = 0;
+ for (int i = 0; i < length; ++i) {
+ actions.add(
+ new GuidedAction.Builder(getActivity())
+ .id(startOffset + i)
+ .title(choices[i])
+ .build());
+ }
+ }
+
+ @Override
+ protected String getActionCategory() {
+ return ACTION_CATEGORY;
+ }
+ }
+}
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..fbf03909
--- /dev/null
+++ b/src/com/android/tv/tuner/setup/PostalCodeFragment.java
@@ -0,0 +1,179 @@
+/*
+ * 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);
+ }
+ };
+ }
+ }
+}
diff --git a/src/com/android/tv/tuner/setup/ScanFragment.java b/src/com/android/tv/tuner/setup/ScanFragment.java
new file mode 100644
index 00000000..044b0d26
--- /dev/null
+++ b/src/com/android/tv/tuner/setup/ScanFragment.java
@@ -0,0 +1,542 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.setup;
+
+import android.animation.LayoutTransition;
+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;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.Button;
+import android.widget.ListView;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+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.R;
+import com.android.tv.tuner.TunerHal;
+import com.android.tv.tuner.TunerPreferences;
+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 java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/** A fragment for scanning channels. */
+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;
+
+ private static final String VCTLESS_CHANNEL_NAME_FORMAT = "RF%d-%d";
+
+ public static final String ACTION_CATEGORY = "com.android.tv.tuner.setup.ScanFragment";
+ public static final int ACTION_CANCEL = 1;
+ public static final int ACTION_FINISH = 2;
+
+ public static final String EXTRA_FOR_CHANNEL_SCAN_FILE = "scan_file_choice";
+
+ private static final long CHANNEL_SCAN_SHOW_DELAY_MS = 10000;
+ private static final long CHANNEL_SCAN_PERIOD_MS = 4000;
+ private static final long SHOW_PROGRESS_DIALOG_DELAY_MS = 300;
+
+ // Build channels out of the locally stored TS streams.
+ private static final boolean SCAN_LOCAL_STREAMS = true;
+
+ private ChannelDataManager mChannelDataManager;
+ private ChannelScanTask mChannelScanTask;
+ private ProgressBar mProgressBar;
+ private TextView mScanningMessage;
+ private View mChannelHolder;
+ private ChannelAdapter mAdapter;
+ private volatile boolean mChannelListVisible;
+ private Button mCancelButton;
+
+ @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());
+ mAdapter = new ChannelAdapter();
+ mProgressBar = (ProgressBar) view.findViewById(R.id.tune_progress);
+ mScanningMessage = (TextView) view.findViewById(R.id.tune_description);
+ ListView channelList = (ListView) view.findViewById(R.id.channel_list);
+ channelList.setAdapter(mAdapter);
+ channelList.setOnItemClickListener(null);
+ ViewGroup progressHolder = (ViewGroup) view.findViewById(R.id.progress_holder);
+ LayoutTransition transition = new LayoutTransition();
+ transition.enableTransitionType(LayoutTransition.CHANGING);
+ progressHolder.setLayoutTransition(transition);
+ mChannelHolder = view.findViewById(R.id.channel_holder);
+ mCancelButton = (Button) view.findViewById(R.id.tune_cancel);
+ mCancelButton.setOnClickListener(
+ new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ finishScan(false);
+ }
+ });
+ 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);
+ 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;
+ }
+
+ @Override
+ protected int getLayoutResourceId() {
+ return R.layout.ut_channel_scan;
+ }
+
+ @Override
+ protected int[] getParentIdsForDelay() {
+ return new int[] {R.id.progress_holder};
+ }
+
+ private void startScan(int channelMapId) {
+ mChannelScanTask = new ChannelScanTask(channelMapId);
+ mChannelScanTask.execute();
+ }
+
+ @Override
+ 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.onPause();
+ }
+
+ /**
+ * Finishes the current scan thread. This fragment will be popped after the scan thread ends.
+ *
+ * @param cancel a flag which indicates the scan is canceled or not.
+ */
+ public void finishScan(boolean cancel) {
+ if (mChannelScanTask != null) {
+ mChannelScanTask.cancelScan(cancel);
+
+ // Notifies a user of waiting to finish the scanning process.
+ new Handler()
+ .postDelayed(
+ new Runnable() {
+ @Override
+ public void run() {
+ if (mChannelScanTask != null) {
+ mChannelScanTask.showFinishingProgressDialog();
+ }
+ }
+ },
+ SHOW_PROGRESS_DIALOG_DELAY_MS);
+
+ // Hides the cancel button.
+ mCancelButton.setEnabled(false);
+ }
+ }
+
+ private class ChannelAdapter extends BaseAdapter {
+ private final ArrayList<TunerChannel> mChannels;
+
+ public ChannelAdapter() {
+ mChannels = new ArrayList<>();
+ }
+
+ @Override
+ public boolean areAllItemsEnabled() {
+ return false;
+ }
+
+ @Override
+ public boolean isEnabled(int pos) {
+ return false;
+ }
+
+ @Override
+ public int getCount() {
+ return mChannels.size();
+ }
+
+ @Override
+ public Object getItem(int pos) {
+ return pos;
+ }
+
+ @Override
+ public long getItemId(int pos) {
+ return pos;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ final Context context = parent.getContext();
+
+ if (convertView == null) {
+ LayoutInflater inflater =
+ (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ convertView = inflater.inflate(R.layout.ut_channel_list, parent, false);
+ }
+
+ TextView channelNum = (TextView) convertView.findViewById(R.id.channel_num);
+ channelNum.setText(mChannels.get(position).getDisplayNumber());
+
+ TextView channelName = (TextView) convertView.findViewById(R.id.channel_name);
+ channelName.setText(mChannels.get(position).getName());
+ return convertView;
+ }
+
+ public void add(TunerChannel channel) {
+ mChannels.add(channel);
+ notifyDataSetChanged();
+ }
+ }
+
+ private class ChannelScanTask extends AsyncTask<Void, Integer, Void>
+ implements EventDetector.EventListener, ChannelDataManager.ChannelScanListener {
+ private static final int MAX_PROGRESS = 100;
+
+ private final Activity mActivity;
+ private final int mChannelMapId;
+ private final TsStreamer mScanTsStreamer;
+ private final TsStreamer mFileTsStreamer;
+ private final ConditionVariable mConditionStopped;
+
+ private final List<ChannelScanFileParser.ScanChannel> mScanChannelList = new ArrayList<>();
+ private boolean mIsCanceled;
+ private boolean mIsFinished;
+ private ProgressDialog mFinishingProgressDialog;
+ private CountDownLatch mLatch;
+
+ public ChannelScanTask(int channelMapId) {
+ mActivity = getActivity();
+ mChannelMapId = channelMapId;
+ if (FAKE_MODE) {
+ mScanTsStreamer = new FakeTsStreamer(this);
+ } else {
+ 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, mActivity) : null;
+ mConditionStopped = new ConditionVariable();
+ mChannelDataManager.setChannelScanListener(this, new Handler());
+ }
+
+ private void maybeSetChannelListVisible() {
+ mActivity.runOnUiThread(
+ new Runnable() {
+ @Override
+ public void run() {
+ int channelsFound = mAdapter.getCount();
+ if (!mChannelListVisible && channelsFound > 0) {
+ String format =
+ getResources()
+ .getQuantityString(
+ R.plurals.ut_channel_scan_message,
+ channelsFound,
+ channelsFound);
+ mScanningMessage.setText(String.format(format, channelsFound));
+ mChannelHolder.setVisibility(View.VISIBLE);
+ mChannelListVisible = true;
+ }
+ }
+ });
+ }
+
+ private void addChannel(final TunerChannel channel) {
+ mActivity.runOnUiThread(
+ new Runnable() {
+ @Override
+ public void run() {
+ mAdapter.add(channel);
+ if (mChannelListVisible) {
+ int channelsFound = mAdapter.getCount();
+ String format =
+ getResources()
+ .getQuantityString(
+ R.plurals.ut_channel_scan_message,
+ channelsFound,
+ channelsFound);
+ mScanningMessage.setText(String.format(format, channelsFound));
+ }
+ }
+ });
+ }
+
+ @Override
+ protected Void doInBackground(Void... params) {
+ mScanChannelList.clear();
+ if (SCAN_LOCAL_STREAMS) {
+ FileTsStreamer.addLocalStreamFiles(mScanChannelList);
+ }
+ mScanChannelList.addAll(
+ ChannelScanFileParser.parseScanFile(
+ getResources().openRawResource(mChannelMapId)));
+ scanChannels();
+ return null;
+ }
+
+ @Override
+ protected void onCancelled() {
+ SoftPreconditions.checkState(false, TAG, "call cancelScan instead of cancel");
+ }
+
+ @Override
+ protected void onProgressUpdate(Integer... values) {
+ 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();
+ }
+
+ private void cancelScan(boolean cancel) {
+ mIsCanceled = cancel;
+ stopScan();
+ }
+
+ private void scanChannels() {
+ if (DEBUG) Log.i(TAG, "Channel scan starting");
+ mChannelDataManager.notifyScanStarted();
+
+ long startMs = System.currentTimeMillis();
+ int i = 1;
+ for (ChannelScanFileParser.ScanChannel scanChannel : mScanChannelList) {
+ int frequency = scanChannel.frequency;
+ String modulation = scanChannel.modulation;
+ Log.i(TAG, "Tuning to " + frequency + " " + modulation);
+
+ TsStreamer streamer = getStreamer(scanChannel.type);
+ SoftPreconditions.checkNotNull(streamer);
+ if (streamer != null && streamer.startStream(scanChannel)) {
+ mLatch = new CountDownLatch(1);
+ try {
+ mLatch.await(CHANNEL_SCAN_PERIOD_MS, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException e) {
+ Log.e(
+ TAG,
+ "The current thread is interrupted during scanChannels(). "
+ + "The TS stream is stopped earlier than expected.",
+ e);
+ }
+ streamer.stopStream();
+
+ addChannelsWithoutVct(scanChannel);
+ if (System.currentTimeMillis() > startMs + CHANNEL_SCAN_SHOW_DELAY_MS
+ && !mChannelListVisible) {
+ maybeSetChannelListVisible();
+ }
+ }
+ if (mConditionStopped.block(-1)) {
+ break;
+ }
+ publishProgress(MAX_PROGRESS * i++ / mScanChannelList.size());
+ }
+ mChannelDataManager.notifyScanCompleted();
+ if (!mConditionStopped.block(-1)) {
+ publishProgress(MAX_PROGRESS);
+ }
+ if (DEBUG) Log.i(TAG, "Channel scan ended");
+ }
+
+ private void addChannelsWithoutVct(ChannelScanFileParser.ScanChannel scanChannel) {
+ if (scanChannel.radioFrequencyNumber == null
+ || !(mScanTsStreamer instanceof TunerTsStreamer)) {
+ return;
+ }
+ for (TunerChannel tunerChannel :
+ ((TunerTsStreamer) mScanTsStreamer).getMalFormedChannels()) {
+ if ((tunerChannel.getVideoPid() != TunerChannel.INVALID_PID)
+ && (tunerChannel.getAudioPid() != TunerChannel.INVALID_PID)) {
+ tunerChannel.setFrequency(scanChannel.frequency);
+ tunerChannel.setModulation(scanChannel.modulation);
+ tunerChannel.setShortName(
+ String.format(
+ Locale.US,
+ VCTLESS_CHANNEL_NAME_FORMAT,
+ scanChannel.radioFrequencyNumber,
+ tunerChannel.getProgramNumber()));
+ tunerChannel.setVirtualMajor(scanChannel.radioFrequencyNumber);
+ tunerChannel.setVirtualMinor(tunerChannel.getProgramNumber());
+ onChannelDetected(tunerChannel, true);
+ }
+ }
+ }
+
+ private TsStreamer getStreamer(int type) {
+ switch (type) {
+ case Channel.TYPE_TUNER:
+ return mScanTsStreamer;
+ case Channel.TYPE_FILE:
+ return mFileTsStreamer;
+ default:
+ return null;
+ }
+ }
+
+ @Override
+ public void onEventDetected(TunerChannel channel, List<PsipData.EitItem> items) {
+ mChannelDataManager.notifyEventDetected(channel, items);
+ }
+
+ @Override
+ public void onChannelScanDone() {
+ if (mLatch != null) {
+ mLatch.countDown();
+ }
+ }
+
+ @Override
+ public void onChannelDetected(TunerChannel channel, boolean channelArrivedAtFirstTime) {
+ if (channelArrivedAtFirstTime) {
+ Log.i(TAG, "Found channel " + channel);
+ }
+ if (channelArrivedAtFirstTime && channel.hasAudio()) {
+ // Playbacks with video-only stream have not been tested yet.
+ // No video-only channel has been found.
+ addChannel(channel);
+ mChannelDataManager.notifyChannelDetected(channel, channelArrivedAtFirstTime);
+ }
+ }
+
+ public void showFinishingProgressDialog() {
+ // Show a progress dialog to wait for the scanning process if it's not done yet.
+ if (!mIsFinished && mFinishingProgressDialog == null) {
+ mFinishingProgressDialog =
+ ProgressDialog.show(
+ mActivity, "", getString(R.string.ut_setup_cancel), true, false);
+ }
+ }
+
+ @Override
+ public void onChannelHandlingDone() {
+ mChannelDataManager.setCurrentVersion(mActivity);
+ mChannelDataManager.releaseSafely();
+ mIsFinished = true;
+ TunerPreferences.setScannedChannelCount(
+ mActivity.getApplicationContext(),
+ mChannelDataManager.getScannedChannelCount());
+ // Cancel a previously shown notification.
+ TunerSetupActivity.cancelNotification(mActivity.getApplicationContext());
+ // Mark scan as done
+ TunerPreferences.setScanDone(mActivity.getApplicationContext());
+ // finishing will be done manually.
+ if (mFinishingProgressDialog != null) {
+ mFinishingProgressDialog.dismiss();
+ }
+ // 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;
+ }
+ }
+
+ private static class FakeTsStreamer implements TsStreamer {
+ private final EventDetector.EventListener mEventListener;
+ private int mProgramNumber = 0;
+
+ FakeTsStreamer(EventDetector.EventListener eventListener) {
+ mEventListener = eventListener;
+ }
+
+ @Override
+ public boolean startStream(ChannelScanFileParser.ScanChannel channel) {
+ if (++mProgramNumber % 2 == 1) {
+ return true;
+ }
+ final String displayNumber = Integer.toString(mProgramNumber);
+ final String name = "Channel-" + mProgramNumber;
+ mEventListener.onChannelDetected(
+ new TunerChannel(mProgramNumber, new ArrayList<>()) {
+ @Override
+ public String getDisplayNumber() {
+ return displayNumber;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+ },
+ true);
+ return true;
+ }
+
+ @Override
+ public boolean startStream(TunerChannel channel) {
+ return false;
+ }
+
+ @Override
+ public void stopStream() {}
+
+ @Override
+ public TsDataSource createDataSource() {
+ return null;
+ }
+ }
+}
diff --git a/src/com/android/tv/tuner/setup/ScanResultFragment.java b/src/com/android/tv/tuner/setup/ScanResultFragment.java
new file mode 100644
index 00000000..a6160ef1
--- /dev/null
+++ b/src/com/android/tv/tuner/setup/ScanResultFragment.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.setup;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
+import android.support.v17.leanback.widget.GuidedAction;
+import com.android.tv.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 java.util.List;
+
+/** A fragment for initial screen. */
+public class ScanResultFragment extends SetupMultiPaneFragment {
+ public static final String ACTION_CATEGORY = "com.android.tv.tuner.setup.ScanResultFragment";
+
+ @Override
+ protected SetupGuidedStepFragment onCreateContentFragment() {
+ return new ContentFragment();
+ }
+
+ @Override
+ protected String getActionCategory() {
+ return ACTION_CATEGORY;
+ }
+
+ @Override
+ protected boolean needsDoneButton() {
+ return false;
+ }
+
+ public static class ContentFragment extends SetupGuidedStepFragment {
+ private int mChannelCountOnPreference;
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ mChannelCountOnPreference = TunerPreferences.getScannedChannelCount(context);
+ }
+
+ @NonNull
+ @Override
+ public Guidance onCreateGuidance(Bundle savedInstanceState) {
+ String title;
+ String description;
+ String breadcrumb;
+ if (mChannelCountOnPreference > 0) {
+ Resources res = getResources();
+ title =
+ res.getQuantityString(
+ R.plurals.ut_result_found_title,
+ mChannelCountOnPreference,
+ mChannelCountOnPreference);
+ description =
+ res.getQuantityString(
+ R.plurals.ut_result_found_description,
+ 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);
+ 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);
+ }
+ return new Guidance(title, description, breadcrumb, null);
+ }
+
+ @Override
+ public void onCreateActions(
+ @NonNull List<GuidedAction> actions, Bundle savedInstanceState) {
+ String[] choices;
+ int doneActionIndex;
+ if (mChannelCountOnPreference > 0) {
+ choices = getResources().getStringArray(R.array.ut_result_found_choices);
+ doneActionIndex = 0;
+ } else {
+ choices = getResources().getStringArray(R.array.ut_result_not_found_choices);
+ doneActionIndex = 1;
+ }
+ for (int i = 0; i < choices.length; ++i) {
+ if (i == doneActionIndex) {
+ actions.add(
+ new GuidedAction.Builder(getActivity())
+ .id(ACTION_DONE)
+ .title(choices[i])
+ .build());
+ } else {
+ actions.add(
+ new GuidedAction.Builder(getActivity())
+ .id(i)
+ .title(choices[i])
+ .build());
+ }
+ }
+ }
+
+ @Override
+ protected String getActionCategory() {
+ return ACTION_CATEGORY;
+ }
+ }
+}
diff --git a/src/com/android/tv/tuner/setup/TunerSetupActivity.java b/src/com/android/tv/tuner/setup/TunerSetupActivity.java
new file mode 100644
index 00000000..58cfc927
--- /dev/null
+++ b/src/com/android/tv/tuner/setup/TunerSetupActivity.java
@@ -0,0 +1,548 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.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;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+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.PostalCodeUtils;
+import java.util.concurrent.Executor;
+
+/** An activity that serves tuner setup process. */
+public class TunerSetupActivity extends SetupActivity {
+ 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_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}.
+ if (checkSelfPermission(android.Manifest.permission.ACCESS_COARSE_LOCATION)
+ != PackageManager.PERMISSION_GRANTED) {
+ // No need to check the request result.
+ requestPermissions(
+ new String[] {android.Manifest.permission.ACCESS_COARSE_LOCATION},
+ 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() {
+ 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
+ protected boolean executeAction(String category, int actionId, Bundle params) {
+ switch (category) {
+ case WelcomeFragment.ACTION_CATEGORY:
+ switch (actionId) {
+ case SetupMultiPaneFragment.ACTION_DONE:
+ // If the scan was performed, then the result should be OK.
+ setResult(mLastScanFragment == null ? RESULT_CANCELED : RESULT_OK);
+ finish();
+ break;
+ 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:
+ if (mTunerHalFactory.getOrCreate() == null) {
+ finish();
+ Toast.makeText(
+ getApplicationContext(),
+ R.string.ut_channel_scan_tuner_unavailable,
+ Toast.LENGTH_LONG)
+ .show();
+ return true;
+ }
+ mLastScanFragment = new ScanFragment();
+ Bundle args1 = new Bundle();
+ args1.putInt(
+ ScanFragment.EXTRA_FOR_CHANNEL_SCAN_FILE, CHANNEL_MAP_SCAN_FILE[actionId]);
+ args1.putInt(KEY_TUNER_TYPE, mTunerType);
+ mLastScanFragment.setArguments(args1);
+ showFragment(mLastScanFragment, true);
+ return true;
+ case ScanFragment.ACTION_CATEGORY:
+ switch (actionId) {
+ case ScanFragment.ACTION_CANCEL:
+ 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);
+ return true;
+ }
+ break;
+ case ScanResultFragment.ACTION_CATEGORY:
+ switch (actionId) {
+ case SetupMultiPaneFragment.ACTION_DONE:
+ setResult(RESULT_OK);
+ finish();
+ break;
+ default:
+ SetupFragment fragment = new ConnectionTypeFragment();
+ fragment.setShortDistance(
+ SetupFragment.FRAGMENT_ENTER_TRANSITION
+ | SetupFragment.FRAGMENT_RETURN_TRANSITION);
+ showFragment(fragment, true);
+ break;
+ }
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_BACK) {
+ FragmentManager manager = getFragmentManager();
+ int count = manager.getBackStackEntryCount();
+ if (count > 0) {
+ String lastTag = manager.getBackStackEntryAt(count - 1).getName();
+ if (ScanResultFragment.class.getCanonicalName().equals(lastTag) && count >= 2) {
+ // Pops fragment including ScanFragment.
+ manager.popBackStack(
+ manager.getBackStackEntryAt(count - 2).getName(),
+ FragmentManager.POP_BACK_STACK_INCLUSIVE);
+ return true;
+ } else if (ScanFragment.class.getCanonicalName().equals(lastTag)) {
+ mLastScanFragment.finishScan(true);
+ return true;
+ }
+ }
+ }
+ 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.
+ *
+ * @param context a {@link Context} instance
+ * @param enabled {@code true} for the {@link TunerTvInputService} to be enabled; otherwise
+ * {@code false}
+ */
+ 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);
+ sendNotification(context, tunerType);
+ } else {
+ TunerPreferences.setShouldShowSetupActivity(context, false);
+ cancelNotification(context);
+ }
+ }
+
+ /**
+ * Returns a {@link Intent} to launch the tuner TV input service.
+ *
+ * @param context a {@link Context} instance
+ */
+ public static Intent createSetupActivity(Context context) {
+ String inputId =
+ TvContract.buildInputId(
+ new ComponentName(
+ context.getPackageName(), TunerTvInputService.class.getName()));
+
+ // 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);
+ Intent tvActivityIntent = new Intent();
+ tvActivityIntent.setComponent(new ComponentName(context, TV_ACTIVITY_CLASS_NAME));
+ intent.putExtra(TvCommonConstants.EXTRA_ACTIVITY_AFTER_COMPLETION, tvActivityIntent);
+ return intent;
+ }
+
+ /** 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
+ */
+ private static PendingIntent createPendingIntentForSetupActivity(Context context) {
+ return PendingIntent.getActivity(
+ context, 0, createSetupActivity(context), 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, String contentTitle, String contentText, Bitmap largeIcon) {
+ // Build and send the notification.
+ Notification notification =
+ new NotificationCompat.BigPictureStyle(
+ new NotificationCompat.Builder(context)
+ .setAutoCancel(false)
+ .setContentTitle(contentTitle)
+ .setContentText(contentText)
+ .setContentInfo(contentText)
+ .setCategory(Notification.CATEGORY_RECOMMENDATION)
+ .setLargeIcon(largeIcon)
+ .setSmallIcon(
+ context.getResources()
+ .getIdentifier(
+ TAG_ICON,
+ TAG_DRAWABLE,
+ context.getPackageName()))
+ .setContentIntent(
+ createPendingIntentForSetupActivity(context)))
+ .build();
+ NotificationManager notificationManager =
+ (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+ 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 notification.
+ *
+ * @param context a {@link Context} instance
+ */
+ 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;
+ }
+ }
+ }
+}
diff --git a/src/com/android/tv/tuner/setup/WelcomeFragment.java b/src/com/android/tv/tuner/setup/WelcomeFragment.java
new file mode 100644
index 00000000..326fe126
--- /dev/null
+++ b/src/com/android/tv/tuner/setup/WelcomeFragment.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.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 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 java.util.List;
+
+/** A fragment for initial screen. */
+public class WelcomeFragment extends SetupMultiPaneFragment {
+ public static final String ACTION_CATEGORY = "com.android.tv.tuner.setup.WelcomeFragment";
+
+ @Override
+ protected SetupGuidedStepFragment onCreateContentFragment() {
+ ContentFragment fragment = new ContentFragment();
+ fragment.setArguments(getArguments());
+ return fragment;
+ }
+
+ @Override
+ protected String getActionCategory() {
+ return ACTION_CATEGORY;
+ }
+
+ @Override
+ protected boolean needsDoneButton() {
+ return false;
+ }
+
+ public static class ContentFragment extends SetupGuidedStepFragment {
+ private int mChannelCountOnPreference;
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ mChannelCountOnPreference =
+ TunerPreferences.getScannedChannelCount(getActivity().getApplicationContext());
+ super.onCreate(savedInstanceState);
+ }
+
+ @NonNull
+ @Override
+ 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) {
+ 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);
+ 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);
+ }
+
+ @Override
+ public void onCreateActions(
+ @NonNull List<GuidedAction> actions, Bundle savedInstanceState) {
+ String[] choices =
+ getResources()
+ .getStringArray(
+ mChannelCountOnPreference == 0
+ ? R.array.ut_setup_new_choices
+ : R.array.ut_setup_again_choices);
+ for (int i = 0; i < choices.length - 1; ++i) {
+ actions.add(
+ new GuidedAction.Builder(getActivity()).id(i).title(choices[i]).build());
+ }
+ actions.add(
+ new GuidedAction.Builder(getActivity())
+ .id(ACTION_DONE)
+ .title(choices[choices.length - 1])
+ .build());
+ }
+
+ @Override
+ protected String getActionCategory() {
+ return ACTION_CATEGORY;
+ }
+ }
+}
diff --git a/src/com/android/tv/tuner/source/FileTsStreamer.java b/src/com/android/tv/tuner/source/FileTsStreamer.java
new file mode 100644
index 00000000..f74274f4
--- /dev/null
+++ b/src/com/android/tv/tuner/source/FileTsStreamer.java
@@ -0,0 +1,487 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.source;
+
+import android.content.Context;
+import android.os.Environment;
+import android.util.Log;
+import android.util.SparseBooleanArray;
+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;
+import com.android.tv.tuner.ts.TsParser;
+import com.android.tv.tuner.tvinput.EventDetector;
+import com.android.tv.tuner.tvinput.FileSourceEventDetector;
+import com.google.android.exoplayer.C;
+import com.google.android.exoplayer.upstream.DataSpec;
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * Provides MPEG-2 TS stream sources for both channel scanning and channel playing from a local file
+ * generated by capturing TV signal.
+ */
+public class FileTsStreamer implements TsStreamer {
+ private static final String TAG = "FileTsStreamer";
+
+ private static final int TS_PACKET_SIZE = 188;
+ private static final int TS_SYNC_BYTE = 0x47;
+ private static final int MIN_READ_UNIT = TS_PACKET_SIZE * 10;
+ private static final int READ_BUFFER_SIZE = MIN_READ_UNIT * 10; // ~20KB
+ private static final int CIRCULAR_BUFFER_SIZE = MIN_READ_UNIT * 4000; // ~ 8MB
+ private static final int PADDING_SIZE = MIN_READ_UNIT * 1000; // ~2MB
+ private static final int READ_TIMEOUT_MS = 10000; // 10 secs.
+ private static final int BUFFER_UNDERRUN_SLEEP_MS = 10;
+ private static final String FILE_DIR =
+ new File(Environment.getExternalStorageDirectory(), "Streams").getAbsolutePath();
+
+ // Virtual frequency base used for file-based source
+ public static final int FREQ_BASE = 100;
+
+ 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;
+ private boolean mStreaming;
+
+ private Thread mStreamingThread;
+ private StreamProvider mSource;
+
+ public static class FileDataSource extends TsDataSource {
+ private final FileTsStreamer mTsStreamer;
+ private final AtomicLong mLastReadPosition = new AtomicLong(0);
+ private long mStartBufferedPosition;
+
+ private FileDataSource(FileTsStreamer tsStreamer) {
+ mTsStreamer = tsStreamer;
+ mStartBufferedPosition = tsStreamer.getBufferedPosition();
+ }
+
+ @Override
+ public long getBufferedPosition() {
+ return mTsStreamer.getBufferedPosition() - mStartBufferedPosition;
+ }
+
+ @Override
+ public long getLastReadPosition() {
+ return mLastReadPosition.get();
+ }
+
+ @Override
+ public void shiftStartPosition(long offset) {
+ SoftPreconditions.checkState(mLastReadPosition.get() == 0);
+ SoftPreconditions.checkArgument(0 <= offset && offset <= getBufferedPosition());
+ mStartBufferedPosition += offset;
+ }
+
+ @Override
+ public long open(DataSpec dataSpec) throws IOException {
+ mLastReadPosition.set(0);
+ return C.LENGTH_UNBOUNDED;
+ }
+
+ @Override
+ public void close() {}
+
+ @Override
+ public int read(byte[] buffer, int offset, int readLength) throws IOException {
+ int ret =
+ mTsStreamer.readAt(
+ mStartBufferedPosition + mLastReadPosition.get(),
+ buffer,
+ offset,
+ readLength);
+ if (ret > 0) {
+ mLastReadPosition.addAndGet(ret);
+ }
+ return ret;
+ }
+ }
+
+ /**
+ * Creates {@link TsStreamer} for scanning & playing MPEG-2 TS file.
+ *
+ * @param eventListener the listener for channel & program information
+ */
+ public FileTsStreamer(EventDetector.EventListener eventListener, Context context) {
+ mEventDetector =
+ new FileSourceEventDetector(
+ eventListener, Features.ENABLE_FILE_DVB.isEnabled(context));
+ mContext = context;
+ }
+
+ @Override
+ public boolean startStream(ScanChannel channel) {
+ String filepath = new File(FILE_DIR, channel.filename).getAbsolutePath();
+ mSource = new StreamProvider(filepath);
+ if (!mSource.isReady()) {
+ return false;
+ }
+ mEventDetector.start(mSource, FileSourceEventDetector.ALL_PROGRAM_NUMBERS);
+ 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;
+ }
+ mStreaming = true;
+ }
+
+ mStreamingThread = new StreamingThread();
+ mStreamingThread.start();
+ Log.i(TAG, "Streaming started");
+ return true;
+ }
+
+ @Override
+ public boolean startStream(TunerChannel channel) {
+ Log.i(TAG, "tuneToChannel with: " + channel.getFilepath());
+ mSource = new StreamProvider(channel.getFilepath());
+ if (!mSource.isReady()) {
+ return false;
+ }
+ mEventDetector.start(mSource, channel.getProgramNumber());
+ mSource.addPidFilter(channel.getVideoPid());
+ for (Integer i : channel.getAudioPids()) {
+ mSource.addPidFilter(i);
+ }
+ mSource.addPidFilter(channel.getPcrPid());
+ 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;
+ }
+ mStreaming = true;
+ }
+
+ mStreamingThread = new StreamingThread();
+ mStreamingThread.start();
+ Log.i(TAG, "Streaming started");
+ return true;
+ }
+
+ /**
+ * Blocks the current thread until the streaming thread stops. In rare cases when the tuner
+ * device is overloaded this can take a while, but usually it returns pretty quickly.
+ */
+ @Override
+ public void stopStream() {
+ synchronized (mCircularBufferMonitor) {
+ mStreaming = false;
+ mCircularBufferMonitor.notify();
+ }
+
+ try {
+ if (mStreamingThread != null) {
+ mStreamingThread.join();
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ @Override
+ public TsDataSource createDataSource() {
+ return new FileDataSource(this);
+ }
+
+ /**
+ * Returns the current buffered position from the file.
+ *
+ * @return the current buffered position
+ */
+ public long getBufferedPosition() {
+ synchronized (mCircularBufferMonitor) {
+ return mBytesFetched;
+ }
+ }
+
+ /** Provides MPEG-2 transport stream from a local file. Stream can be filtered by PID. */
+ public static class StreamProvider {
+ private final String mFilepath;
+ private final SparseBooleanArray mPids = new SparseBooleanArray();
+ private final byte[] mPreBuffer = new byte[READ_BUFFER_SIZE];
+
+ private BufferedInputStream mInputStream;
+
+ private StreamProvider(String filepath) {
+ mFilepath = filepath;
+ open(filepath);
+ }
+
+ private void open(String filepath) {
+ try {
+ mInputStream = new BufferedInputStream(new FileInputStream(filepath));
+ } catch (IOException e) {
+ Log.e(TAG, "Error opening input stream", e);
+ mInputStream = null;
+ }
+ }
+
+ private boolean isReady() {
+ return mInputStream != null;
+ }
+
+ /** Returns the file path of the MPEG-2 TS file. */
+ public String getFilepath() {
+ return mFilepath;
+ }
+
+ /** Adds a pid for filtering from the MPEG-2 TS file. */
+ public void addPidFilter(int pid) {
+ mPids.put(pid, true);
+ }
+
+ /** Returns whether the current pid filter is empty or not. */
+ public boolean isFilterEmpty() {
+ return mPids.size() == 0;
+ }
+
+ /** Clears the current pid filter. */
+ public void clearPidFilter() {
+ mPids.clear();
+ }
+
+ /**
+ * Returns whether a pid is in the pid filter or not.
+ *
+ * @param pid the pid to check
+ */
+ public boolean isInFilter(int pid) {
+ return mPids.get(pid);
+ }
+
+ /**
+ * Reads from the MPEG-2 TS file to buffer.
+ *
+ * @param inputBuffer to read
+ * @return the number of read bytes
+ */
+ private int read(byte[] inputBuffer) {
+ int readSize = readInternal();
+ if (readSize <= 0) {
+ // Reached the end of stream. Restart from the beginning.
+ close();
+ open(mFilepath);
+ if (mInputStream == null) {
+ return -1;
+ }
+ readSize = readInternal();
+ }
+
+ if (mPreBuffer[0] != TS_SYNC_BYTE) {
+ Log.e(TAG, "Error reading input stream - no TS sync found");
+ return -1;
+ }
+ int filteredSize = 0;
+ for (int i = 0, destPos = 0; i < readSize; i += TS_PACKET_SIZE) {
+ if (mPreBuffer[i] == TS_SYNC_BYTE) {
+ int pid = ((mPreBuffer[i + 1] & 0x1f) << 8) + (mPreBuffer[i + 2] & 0xff);
+ if (mPids.get(pid)) {
+ System.arraycopy(mPreBuffer, i, inputBuffer, destPos, TS_PACKET_SIZE);
+ destPos += TS_PACKET_SIZE;
+ filteredSize += TS_PACKET_SIZE;
+ }
+ }
+ }
+ return filteredSize;
+ }
+
+ private int readInternal() {
+ int readSize;
+ try {
+ readSize = mInputStream.read(mPreBuffer, 0, mPreBuffer.length);
+ } catch (IOException e) {
+ Log.e(TAG, "Error reading input stream", e);
+ return -1;
+ }
+ return readSize;
+ }
+
+ private void close() {
+ try {
+ mInputStream.close();
+ } catch (IOException e) {
+ Log.e(TAG, "Error closing input stream:", e);
+ }
+ mInputStream = null;
+ }
+ }
+
+ /**
+ * Reads data from internal buffer.
+ *
+ * @param pos the position to read from
+ * @param buffer to read
+ * @param offset start position of the read buffer
+ * @param amount number of bytes to read
+ * @return number of read bytes when successful, {@code -1} otherwise
+ * @throws IOException
+ */
+ public int readAt(long pos, byte[] buffer, int offset, int amount) throws IOException {
+ synchronized (mCircularBufferMonitor) {
+ long initialBytesFetched = mBytesFetched;
+ while (mBytesFetched < pos + amount && mStreaming) {
+ try {
+ mCircularBufferMonitor.wait(READ_TIMEOUT_MS);
+ } catch (InterruptedException e) {
+ // Wait again.
+ Thread.currentThread().interrupt();
+ }
+ if (initialBytesFetched == mBytesFetched) {
+ Log.w(TAG, "No data update for " + READ_TIMEOUT_MS + "ms. returning -1.");
+
+ // Returning -1 will make demux report EOS so that the input service can retry
+ // the playback.
+ return -1;
+ }
+ }
+ if (!mStreaming) {
+ Log.w(TAG, "Stream is already stopped.");
+ return -1;
+ }
+ if (mBytesFetched - CIRCULAR_BUFFER_SIZE > pos) {
+ Log.e(TAG, "Demux is requesting the data which is already overwritten.");
+ return -1;
+ }
+ int posInBuffer = (int) (pos % CIRCULAR_BUFFER_SIZE);
+ int bytesToCopyInFirstPass = amount;
+ if (posInBuffer + bytesToCopyInFirstPass > mCircularBuffer.length) {
+ bytesToCopyInFirstPass = mCircularBuffer.length - posInBuffer;
+ }
+ System.arraycopy(mCircularBuffer, posInBuffer, buffer, offset, bytesToCopyInFirstPass);
+ if (bytesToCopyInFirstPass < amount) {
+ System.arraycopy(
+ mCircularBuffer,
+ 0,
+ buffer,
+ offset + bytesToCopyInFirstPass,
+ amount - bytesToCopyInFirstPass);
+ }
+ mLastReadPosition = pos + amount;
+ mCircularBufferMonitor.notify();
+ return amount;
+ }
+ }
+
+ /**
+ * Adds {@link ScanChannel} instance for local files.
+ *
+ * @param output a list of channels where the results will be placed in
+ */
+ public static void addLocalStreamFiles(List<ScanChannel> output) {
+ File dir = new File(FILE_DIR);
+ if (!dir.exists()) return;
+
+ File[] tsFiles = dir.listFiles();
+ if (tsFiles == null) return;
+ int freq = FileTsStreamer.FREQ_BASE;
+ for (File file : tsFiles) {
+ if (!file.isFile()) continue;
+ output.add(ScanChannel.forFile(freq, file.getName()));
+ freq += 100;
+ }
+ }
+
+ /**
+ * A thread managing a circular buffer that holds stream data to be consumed by player. Keeps
+ * reading data in from a {@link StreamProvider} to hold enough amount for buffering. Started
+ * and stopped by {@link #startStream()} and {@link #stopStream()}, respectively.
+ */
+ private class StreamingThread extends Thread {
+ @Override
+ public void run() {
+ byte[] dataBuffer = new byte[READ_BUFFER_SIZE];
+
+ synchronized (mCircularBufferMonitor) {
+ mBytesFetched = 0;
+ mLastReadPosition = 0;
+ }
+
+ while (true) {
+ synchronized (mCircularBufferMonitor) {
+ while ((mBytesFetched - mLastReadPosition + PADDING_SIZE) > CIRCULAR_BUFFER_SIZE
+ && mStreaming) {
+ try {
+ mCircularBufferMonitor.wait();
+ } catch (InterruptedException e) {
+ // Wait again.
+ Thread.currentThread().interrupt();
+ }
+ }
+ if (!mStreaming) {
+ break;
+ }
+ }
+
+ int bytesWritten = mSource.read(dataBuffer);
+ if (bytesWritten <= 0) {
+ try {
+ // When buffer is underrun, we sleep for short time to prevent
+ // unnecessary CPU draining.
+ sleep(BUFFER_UNDERRUN_SLEEP_MS);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ continue;
+ }
+
+ mEventDetector.feedTSStream(dataBuffer, 0, bytesWritten);
+
+ synchronized (mCircularBufferMonitor) {
+ int posInBuffer = (int) (mBytesFetched % CIRCULAR_BUFFER_SIZE);
+ int bytesToCopyInFirstPass = bytesWritten;
+ if (posInBuffer + bytesToCopyInFirstPass > mCircularBuffer.length) {
+ bytesToCopyInFirstPass = mCircularBuffer.length - posInBuffer;
+ }
+ System.arraycopy(
+ dataBuffer, 0, mCircularBuffer, posInBuffer, bytesToCopyInFirstPass);
+ if (bytesToCopyInFirstPass < bytesWritten) {
+ System.arraycopy(
+ dataBuffer,
+ bytesToCopyInFirstPass,
+ mCircularBuffer,
+ 0,
+ bytesWritten - bytesToCopyInFirstPass);
+ }
+ mBytesFetched += bytesWritten;
+ mCircularBufferMonitor.notify();
+ }
+ }
+
+ Log.i(TAG, "Streaming stopped");
+ mSource.close();
+ }
+ }
+}
diff --git a/src/com/android/tv/tuner/source/TsDataSource.java b/src/com/android/tv/tuner/source/TsDataSource.java
new file mode 100644
index 00000000..be902944
--- /dev/null
+++ b/src/com/android/tv/tuner/source/TsDataSource.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.source;
+
+import com.google.android.exoplayer.upstream.DataSource;
+
+/** {@link DataSource} for MPEG-TS stream, which will be used by {@link TsExtractor}. */
+public abstract class TsDataSource implements DataSource {
+
+ /**
+ * Returns the number of bytes being buffered by {@link TsStreamer} so far.
+ *
+ * @return the buffered position
+ */
+ public long getBufferedPosition() {
+ return 0;
+ }
+
+ /**
+ * Returns the offset position where the last {@link DataSource#read} read.
+ *
+ * @return the last read position
+ */
+ public long getLastReadPosition() {
+ return 0;
+ }
+
+ /**
+ * Shifts start position by the specified offset. Do not call this method when the class already
+ * provided MPEG-TS stream to the extractor.
+ *
+ * @param offset 0 <= offset <= buffered position
+ */
+ public void shiftStartPosition(long offset) {}
+}
diff --git a/src/com/android/tv/tuner/source/TsDataSourceManager.java b/src/com/android/tv/tuner/source/TsDataSourceManager.java
new file mode 100644
index 00000000..fc8a8327
--- /dev/null
+++ b/src/com/android/tv/tuner/source/TsDataSourceManager.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.source;
+
+import android.content.Context;
+import android.support.annotation.VisibleForTesting;
+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;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Manages {@link DataSource} for playback and recording. The class hides handling of {@link
+ * TunerHal} and {@link TsStreamer} from other classes. One TsDataSourceManager should be created
+ * for per session.
+ */
+public class TsDataSourceManager {
+ private static final Object sLock = new Object();
+ private static final Map<TsDataSource, TsStreamer> sTsStreamers = new ConcurrentHashMap<>();
+
+ private static int sSequenceId;
+
+ private final int mId;
+ private final boolean mIsRecording;
+ private final TunerTsStreamerManager mTunerStreamerManager =
+ TunerTsStreamerManager.getInstance();
+
+ private boolean mKeepTuneStatus;
+
+ /**
+ * Creates TsDataSourceManager to create and release {@link DataSource} which will be used for
+ * playing and recording.
+ *
+ * @param isRecording {@code true} when for recording, {@code false} otherwise
+ * @return {@link TsDataSourceManager}
+ */
+ public static TsDataSourceManager createSourceManager(boolean isRecording) {
+ int id;
+ synchronized (sLock) {
+ id = ++sSequenceId;
+ }
+ return new TsDataSourceManager(id, isRecording);
+ }
+
+ private TsDataSourceManager(int id, boolean isRecording) {
+ mId = id;
+ mIsRecording = isRecording;
+ mKeepTuneStatus = true;
+ }
+
+ /**
+ * Creates or retrieves {@link TsDataSource} for playing or recording
+ *
+ * @param context a {@link Context} instance
+ * @param channel to play or record
+ * @param eventListener for program information which will be scanned from MPEG2-TS stream
+ * @return {@link TsDataSource} which will provide the specified channel stream
+ */
+ public TsDataSource createDataSource(
+ Context context, TunerChannel channel, EventDetector.EventListener eventListener) {
+ if (channel.getType() == Channel.TYPE_FILE) {
+ // MPEG2 TS captured stream file recording is not supported.
+ if (mIsRecording) {
+ return null;
+ }
+ FileTsStreamer streamer = new FileTsStreamer(eventListener, context);
+ if (streamer.startStream(channel)) {
+ TsDataSource source = streamer.createDataSource();
+ sTsStreamers.put(source, streamer);
+ return source;
+ }
+ return null;
+ }
+ return mTunerStreamerManager.createDataSource(
+ context, channel, eventListener, mId, !mIsRecording && mKeepTuneStatus);
+ }
+
+ /**
+ * Releases the specified {@link TsDataSource} and underlying {@link TunerHal}.
+ *
+ * @param source to release
+ */
+ public void releaseDataSource(TsDataSource source) {
+ if (source instanceof TunerTsStreamer.TunerDataSource) {
+ mTunerStreamerManager.releaseDataSource(source, mId, !mIsRecording && mKeepTuneStatus);
+ } else if (source instanceof FileTsStreamer.FileDataSource) {
+ FileTsStreamer streamer = (FileTsStreamer) sTsStreamers.get(source);
+ if (streamer != null) {
+ sTsStreamers.remove(source);
+ streamer.stopStream();
+ }
+ }
+ }
+
+ /** Indicates that the current session has pending tunes. */
+ public void setHasPendingTune() {
+ mTunerStreamerManager.setHasPendingTune(mId);
+ }
+
+ /**
+ * Indicates whether the underlying {@link TunerHal} should be kept or not when data source is
+ * being released. TODO: If b/30750953 is fixed, we can remove this function.
+ *
+ * @param keepTuneStatus underlying {@link TunerHal} will be reused when data source releasing.
+ */
+ public void setKeepTuneStatus(boolean keepTuneStatus) {
+ mKeepTuneStatus = keepTuneStatus;
+ }
+
+ /** Add tuner hal into TunerTsStreamerManager for test. */
+ @VisibleForTesting
+ public void addTunerHalForTest(TunerHal tunerHal) {
+ mTunerStreamerManager.addTunerHal(tunerHal, mId);
+ }
+
+ /** Releases persistent resources. */
+ public void release() {
+ mTunerStreamerManager.release(mId);
+ }
+}
diff --git a/src/com/android/tv/tuner/source/TsStreamWriter.java b/src/com/android/tv/tuner/source/TsStreamWriter.java
new file mode 100644
index 00000000..f90136bf
--- /dev/null
+++ b/src/com/android/tv/tuner/source/TsStreamWriter.java
@@ -0,0 +1,238 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.source;
+
+import android.content.Context;
+import android.util.Log;
+import com.android.tv.tuner.data.TunerChannel;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.Set;
+
+/** Stores TS files to the disk for debugging. */
+public class TsStreamWriter {
+ private static final String TAG = "TsStreamWriter";
+ private static final boolean DEBUG = false;
+
+ private static final long TIME_LIMIT_MS = 10000; // 10s
+ private static final int NO_INSTANCE_ID = 0;
+ private static final int MAX_GET_ID_RETRY_COUNT = 5;
+ private static final int MAX_INSTANCE_ID = 10000;
+ private static final String SEPARATOR = "_";
+
+ private FileOutputStream mFileOutputStream;
+ private long mFileStartTimeMs;
+ private String mFileName = null;
+ private final String mDirectoryPath;
+ private final File mDirectory;
+ private final int mInstanceId;
+ private TunerChannel mChannel;
+
+ public TsStreamWriter(Context context) {
+ File externalFilesDir = context.getExternalFilesDir(null);
+ if (externalFilesDir == null || !externalFilesDir.isDirectory()) {
+ mDirectoryPath = null;
+ mDirectory = null;
+ mInstanceId = NO_INSTANCE_ID;
+ if (DEBUG) {
+ Log.w(TAG, "Fail to get external files dir!");
+ }
+ } else {
+ mDirectoryPath = externalFilesDir.getPath() + "/EngTsStream";
+ mDirectory = new File(mDirectoryPath);
+ if (!mDirectory.exists()) {
+ boolean madeDir = mDirectory.mkdir();
+ if (!madeDir) {
+ Log.w(TAG, "Error. Fail to create folder!");
+ }
+ }
+ mInstanceId = generateInstanceId();
+ }
+ }
+
+ /**
+ * Sets the current channel.
+ *
+ * @param channel curren channel of the stream
+ */
+ public void setChannel(TunerChannel channel) {
+ mChannel = channel;
+ }
+
+ /** Opens a file to store TS data. */
+ public void openFile() {
+ if (mChannel == null || mDirectoryPath == null) {
+ return;
+ }
+ mFileStartTimeMs = System.currentTimeMillis();
+ mFileName =
+ mChannel.getDisplayNumber()
+ + SEPARATOR
+ + mFileStartTimeMs
+ + SEPARATOR
+ + mInstanceId
+ + ".ts";
+ String filePath = mDirectoryPath + "/" + mFileName;
+ try {
+ mFileOutputStream = new FileOutputStream(filePath, false);
+ } catch (FileNotFoundException e) {
+ Log.w(TAG, "Cannot open file: " + filePath, e);
+ }
+ }
+
+ /**
+ * Closes the file and stops storing TS data.
+ *
+ * @param calledWhenStopStream {@code true} if this method is called when the stream is stopped
+ * {@code false} otherwise
+ */
+ public void closeFile(boolean calledWhenStopStream) {
+ if (mFileOutputStream == null) {
+ return;
+ }
+ try {
+ mFileOutputStream.close();
+ deleteOutdatedFiles(calledWhenStopStream);
+ mFileName = null;
+ mFileOutputStream = null;
+ } catch (IOException e) {
+ Log.w(TAG, "Error on closing file.", e);
+ }
+ }
+
+ /**
+ * Writes the data to the file.
+ *
+ * @param buffer the data to be written
+ * @param bytesWritten number of bytes written
+ */
+ public void writeToFile(byte[] buffer, int bytesWritten) {
+ if (mFileOutputStream == null) {
+ return;
+ }
+ if (System.currentTimeMillis() - mFileStartTimeMs > TIME_LIMIT_MS) {
+ closeFile(false);
+ openFile();
+ }
+ try {
+ mFileOutputStream.write(buffer, 0, bytesWritten);
+ } catch (IOException e) {
+ Log.w(TAG, "Error on writing TS stream.", e);
+ }
+ }
+
+ /**
+ * Deletes outdated files to save storage.
+ *
+ * @param deleteAll {@code true} if all the files with the relative ID should be deleted {@code
+ * false} if the most recent file should not be deleted
+ */
+ private void deleteOutdatedFiles(boolean deleteAll) {
+ if (mFileName == null) {
+ return;
+ }
+ if (mDirectory == null || !mDirectory.isDirectory()) {
+ Log.e(TAG, "Error. The folder doesn't exist!");
+ return;
+ }
+ if (mFileName == null) {
+ Log.e(TAG, "Error. The current file name is null!");
+ return;
+ }
+ for (File file : mDirectory.listFiles()) {
+ if (file.isFile()
+ && getFileId(file) == mInstanceId
+ && (deleteAll || !mFileName.equals(file.getName()))) {
+ boolean deleted = file.delete();
+ if (DEBUG && !deleted) {
+ Log.w(TAG, "Failed to delete " + file.getName());
+ }
+ }
+ }
+ }
+
+ /**
+ * Generates a unique instance ID.
+ *
+ * @return a unique instance ID
+ */
+ private int generateInstanceId() {
+ if (mDirectory == null) {
+ return NO_INSTANCE_ID;
+ }
+ Set<Integer> idSet = getExistingIds();
+ if (idSet == null) {
+ return NO_INSTANCE_ID;
+ }
+ for (int i = 0; i < MAX_GET_ID_RETRY_COUNT; i++) {
+ // Range [1, MAX_INSTANCE_ID]
+ int id = (int) Math.floor(Math.random() * MAX_INSTANCE_ID) + 1;
+ if (!idSet.contains(id)) {
+ return id;
+ }
+ }
+ return NO_INSTANCE_ID;
+ }
+
+ /**
+ * Gets all existing instance IDs.
+ *
+ * @return a set of all existing instance IDs
+ */
+ private Set<Integer> getExistingIds() {
+ if (mDirectory == null || !mDirectory.isDirectory()) {
+ return null;
+ }
+
+ Set<Integer> idSet = new HashSet<>();
+ for (File file : mDirectory.listFiles()) {
+ int id = getFileId(file);
+ if (id != NO_INSTANCE_ID) {
+ idSet.add(id);
+ }
+ }
+ return idSet;
+ }
+
+ /**
+ * Gets the instance ID of a given file.
+ *
+ * @param file the file whose TsStreamWriter ID is returned
+ * @return the TsStreamWriter ID of the file or NO_INSTANCE_ID if not available
+ */
+ private static int getFileId(File file) {
+ if (file == null || !file.isFile()) {
+ return NO_INSTANCE_ID;
+ }
+ String fileName = file.getName();
+ int lastSeparator = fileName.lastIndexOf(SEPARATOR);
+ if (!fileName.endsWith(".ts") || lastSeparator == -1) {
+ return NO_INSTANCE_ID;
+ }
+ try {
+ return Integer.parseInt(fileName.substring(lastSeparator + 1, fileName.length() - 3));
+ } catch (NumberFormatException e) {
+ if (DEBUG) {
+ Log.e(TAG, fileName + " is not a valid file name.");
+ }
+ }
+ return NO_INSTANCE_ID;
+ }
+}
diff --git a/src/com/android/tv/tuner/source/TsStreamer.java b/src/com/android/tv/tuner/source/TsStreamer.java
new file mode 100644
index 00000000..3dbba7e7
--- /dev/null
+++ b/src/com/android/tv/tuner/source/TsStreamer.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.source;
+
+import com.android.tv.tuner.ChannelScanFileParser;
+import com.android.tv.tuner.data.TunerChannel;
+
+/**
+ * Interface definition for a stream generator. The interface will provide streams for scanning
+ * channels and/or playback.
+ */
+public interface TsStreamer {
+ /**
+ * Starts streaming the data for channel scanning process.
+ *
+ * @param channel {@link ChannelScanFileParser.ScanChannel} to be scanned
+ * @return {@code true} if ready to stream, otherwise {@code false}
+ */
+ boolean startStream(ChannelScanFileParser.ScanChannel channel);
+
+ /**
+ * Starts streaming the data for channel playing or recording.
+ *
+ * @param channel {@link TunerChannel} to tune
+ * @return {@code true} if ready to stream, otherwise {@code false}
+ */
+ boolean startStream(TunerChannel channel);
+
+ /** Stops streaming the data. */
+ void stopStream();
+
+ /**
+ * Creates {@link TsDataSource} which will provide MPEG-2 TS stream for {@link
+ * android.media.MediaExtractor}. The source will start from the position where it is created.
+ *
+ * @return {@link TsDataSource}
+ */
+ TsDataSource createDataSource();
+}
diff --git a/src/com/android/tv/tuner/source/TunerTsStreamer.java b/src/com/android/tv/tuner/source/TunerTsStreamer.java
new file mode 100644
index 00000000..21b7a1f8
--- /dev/null
+++ b/src/com/android/tv/tuner/source/TunerTsStreamer.java
@@ -0,0 +1,420 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.source;
+
+import android.content.Context;
+import android.util.Log;
+import android.util.Pair;
+import com.android.tv.common.SoftPreconditions;
+import com.android.tv.tuner.ChannelScanFileParser;
+import com.android.tv.tuner.TunerHal;
+import com.android.tv.tuner.TunerPreferences;
+import com.android.tv.tuner.data.TunerChannel;
+import com.android.tv.tuner.tvinput.EventDetector;
+import com.android.tv.tuner.tvinput.EventDetector.EventListener;
+import com.google.android.exoplayer.C;
+import com.google.android.exoplayer.upstream.DataSpec;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicLong;
+
+/** Provides MPEG-2 TS stream sources for channel playing from an underlying tuner device. */
+public class TunerTsStreamer implements TsStreamer {
+ private static final String TAG = "TunerTsStreamer";
+
+ 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 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;
+ private final AtomicLong mLastReadPosition = new AtomicLong(0);
+ private long mStartBufferedPosition;
+
+ private TunerDataSource(TunerTsStreamer tsStreamer) {
+ mTsStreamer = tsStreamer;
+ mStartBufferedPosition = tsStreamer.getBufferedPosition();
+ }
+
+ @Override
+ public long getBufferedPosition() {
+ return mTsStreamer.getBufferedPosition() - mStartBufferedPosition;
+ }
+
+ @Override
+ public long getLastReadPosition() {
+ return mLastReadPosition.get();
+ }
+
+ @Override
+ public void shiftStartPosition(long offset) {
+ SoftPreconditions.checkState(mLastReadPosition.get() == 0);
+ SoftPreconditions.checkArgument(0 <= offset && offset <= getBufferedPosition());
+ mStartBufferedPosition += offset;
+ }
+
+ @Override
+ public long open(DataSpec dataSpec) throws IOException {
+ mLastReadPosition.set(0);
+ return C.LENGTH_UNBOUNDED;
+ }
+
+ @Override
+ public void close() {}
+
+ @Override
+ public int read(byte[] buffer, int offset, int readLength) throws IOException {
+ int ret =
+ mTsStreamer.readAt(
+ mStartBufferedPosition + mLastReadPosition.get(),
+ buffer,
+ offset,
+ readLength);
+ if (ret > 0) {
+ mLastReadPosition.addAndGet(ret);
+ } 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;
+ }
+ }
+ /**
+ * Creates {@link TsStreamer} for playing or recording the specified channel.
+ *
+ * @param tunerHal the HAL for tuner device
+ * @param eventListener the listener for channel & program information
+ */
+ public TunerTsStreamer(TunerHal tunerHal, EventListener eventListener, Context context) {
+ mTunerHal = tunerHal;
+ mEventDetector = new EventDetector(mTunerHal);
+ if (eventListener != null) {
+ mEventDetector.registerListener(eventListener);
+ }
+ mTsStreamWriter =
+ context != null && TunerPreferences.getStoreTsStream(context)
+ ? new TsStreamWriter(context)
+ : null;
+ }
+
+ public TunerTsStreamer(TunerHal tunerHal, EventListener eventListener) {
+ this(tunerHal, eventListener, null);
+ }
+
+ @Override
+ public boolean startStream(TunerChannel channel) {
+ if (mTunerHal.tune(
+ channel.getFrequency(), channel.getModulation(), channel.getDisplayNumber(false))) {
+ if (channel.hasVideo()) {
+ mTunerHal.addPidFilter(channel.getVideoPid(), TunerHal.FILTER_TYPE_VIDEO);
+ }
+ boolean audioFilterSet = false;
+ for (Integer audioPid : channel.getAudioPids()) {
+ if (!audioFilterSet) {
+ mTunerHal.addPidFilter(audioPid, TunerHal.FILTER_TYPE_AUDIO);
+ audioFilterSet = true;
+ } else {
+ // FILTER_TYPE_AUDIO overrides the previous filter for audio. We use
+ // FILTER_TYPE_OTHER from the secondary one to get the all audio tracks.
+ mTunerHal.addPidFilter(audioPid, TunerHal.FILTER_TYPE_OTHER);
+ }
+ }
+ mTunerHal.addPidFilter(channel.getPcrPid(), TunerHal.FILTER_TYPE_PCR);
+ if (mEventDetector != null) {
+ mEventDetector.startDetecting(
+ channel.getFrequency(),
+ channel.getModulation(),
+ channel.getProgramNumber());
+ }
+ mChannel = channel;
+ mChannelNumber = channel.getDisplayNumber();
+ synchronized (mCircularBufferMonitor) {
+ if (mStreaming) {
+ Log.w(TAG, "Streaming should be stopped before start streaming");
+ return true;
+ }
+ mStreaming = true;
+ mBytesFetched = 0;
+ mLastReadPosition.set(0L);
+ }
+ if (mTsStreamWriter != null) {
+ mTsStreamWriter.setChannel(mChannel);
+ mTsStreamWriter.openFile();
+ }
+ mStreamingThread = new StreamingThread();
+ mStreamingThread.start();
+ Log.i(TAG, "Streaming started");
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean startStream(ChannelScanFileParser.ScanChannel channel) {
+ if (mTunerHal.tune(channel.frequency, channel.modulation, null)) {
+ mEventDetector.startDetecting(
+ channel.frequency, channel.modulation, EventDetector.ALL_PROGRAM_NUMBERS);
+ synchronized (mCircularBufferMonitor) {
+ if (mStreaming) {
+ Log.w(TAG, "Streaming should be stopped before start streaming");
+ return true;
+ }
+ mStreaming = true;
+ mBytesFetched = 0;
+ mLastReadPosition.set(0L);
+ }
+ mStreamingThread = new StreamingThread();
+ mStreamingThread.start();
+ Log.i(TAG, "Streaming started");
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Blocks the current thread until the streaming thread stops. In rare cases when the tuner
+ * device is overloaded this can take a while, but usually it returns pretty quickly.
+ */
+ @Override
+ public void stopStream() {
+ mChannel = null;
+ synchronized (mCircularBufferMonitor) {
+ mStreaming = false;
+ mCircularBufferMonitor.notifyAll();
+ }
+
+ try {
+ if (mStreamingThread != null) {
+ mStreamingThread.join();
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ if (mTsStreamWriter != null) {
+ mTsStreamWriter.closeFile(true);
+ mTsStreamWriter.setChannel(null);
+ }
+ }
+
+ @Override
+ public TsDataSource createDataSource() {
+ return new TunerDataSource(this);
+ }
+
+ /**
+ * Returns incomplete channel lists which was scanned so far. Incomplete channel means the
+ * channel whose channel information is not complete or is not well-formed.
+ *
+ * @return {@link List} of {@link TunerChannel}
+ */
+ public List<TunerChannel> getMalFormedChannels() {
+ return mEventDetector.getMalFormedChannels();
+ }
+
+ /**
+ * Returns the current {@link TunerHal} which provides MPEG-TS stream for TunerTsStreamer.
+ *
+ * @return {@link TunerHal}
+ */
+ public TunerHal getTunerHal() {
+ return mTunerHal;
+ }
+
+ /**
+ * Returns the current tuned channel for TunerTsStreamer.
+ *
+ * @return {@link TunerChannel}
+ */
+ public TunerChannel getChannel() {
+ return mChannel;
+ }
+
+ /**
+ * Returns the current buffered position from tuner.
+ *
+ * @return the current buffered position
+ */
+ public long getBufferedPosition() {
+ synchronized (mCircularBufferMonitor) {
+ return mBytesFetched;
+ }
+ }
+
+ 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() {
+ // Buffers for streaming data from the tuner and the internal buffer.
+ byte[] dataBuffer = new byte[READ_BUFFER_SIZE];
+
+ while (true) {
+ synchronized (mCircularBufferMonitor) {
+ if (!mStreaming) {
+ break;
+ }
+ }
+
+ 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 {
+ // When buffer is underrun, we sleep for short time to prevent
+ // unnecessary CPU draining.
+ sleep(BUFFER_UNDERRUN_SLEEP_MS);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ continue;
+ }
+
+ if (mTsStreamWriter != null) {
+ mTsStreamWriter.writeToFile(dataBuffer, bytesWritten);
+ }
+
+ if (mEventDetector != null) {
+ mEventDetector.feedTSStream(dataBuffer, 0, bytesWritten);
+ }
+ synchronized (mCircularBufferMonitor) {
+ int posInBuffer = (int) (mBytesFetched % CIRCULAR_BUFFER_SIZE);
+ int bytesToCopyInFirstPass = bytesWritten;
+ if (posInBuffer + bytesToCopyInFirstPass > mCircularBuffer.length) {
+ bytesToCopyInFirstPass = mCircularBuffer.length - posInBuffer;
+ }
+ System.arraycopy(
+ dataBuffer, 0, mCircularBuffer, posInBuffer, bytesToCopyInFirstPass);
+ if (bytesToCopyInFirstPass < bytesWritten) {
+ System.arraycopy(
+ dataBuffer,
+ bytesToCopyInFirstPass,
+ mCircularBuffer,
+ 0,
+ bytesWritten - bytesToCopyInFirstPass);
+ }
+ mBytesFetched += bytesWritten;
+ mCircularBufferMonitor.notifyAll();
+ }
+ }
+
+ Log.i(TAG, "Streaming stopped");
+ }
+ }
+
+ /**
+ * Reads data from internal buffer.
+ *
+ * @param pos the position to read from
+ * @param buffer to read
+ * @param offset start position of the read buffer
+ * @param amount number of bytes to read
+ * @return number of read bytes when successful, {@code -1} otherwise
+ * @throws IOException
+ */
+ public int readAt(long pos, byte[] buffer, int offset, int amount) throws IOException {
+ while (true) {
+ synchronized (mCircularBufferMonitor) {
+ if (!mStreaming) {
+ return READ_ERROR_STREAMING_ENDED;
+ }
+ if (mBytesFetched - CIRCULAR_BUFFER_SIZE > pos) {
+ Log.w(TAG, "Demux is requesting the data which is already overwritten.");
+ return READ_ERROR_BUFFER_OVERWRITTEN;
+ }
+ if (mBytesFetched < pos + amount) {
+ try {
+ mCircularBufferMonitor.wait(READ_TIMEOUT_MS);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ // Try again to prevent starvation.
+ // Give chances to read from other threads.
+ continue;
+ }
+ int startPos = (int) (pos % CIRCULAR_BUFFER_SIZE);
+ int endPos = (int) ((pos + amount) % CIRCULAR_BUFFER_SIZE);
+ int firstLength = (startPos > endPos ? CIRCULAR_BUFFER_SIZE : endPos) - startPos;
+ System.arraycopy(mCircularBuffer, startPos, buffer, offset, firstLength);
+ if (firstLength < amount) {
+ System.arraycopy(
+ mCircularBuffer, 0, buffer, offset + firstLength, amount - firstLength);
+ }
+ mCircularBufferMonitor.notifyAll();
+ return amount;
+ }
+ }
+ }
+}
diff --git a/src/com/android/tv/tuner/source/TunerTsStreamerManager.java b/src/com/android/tv/tuner/source/TunerTsStreamerManager.java
new file mode 100644
index 00000000..e94bd56c
--- /dev/null
+++ b/src/com/android/tv/tuner/source/TunerTsStreamerManager.java
@@ -0,0 +1,303 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.source;
+
+import android.content.Context;
+import com.android.tv.common.AutoCloseableUtils;
+import com.android.tv.common.SoftPreconditions;
+import com.android.tv.tuner.TunerHal;
+import com.android.tv.tuner.data.TunerChannel;
+import com.android.tv.tuner.tvinput.EventDetector;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Manages {@link TunerTsStreamer} for playback and recording. The class hides handling of {@link
+ * TunerHal} from other classes. This class is used by {@link TsDataSourceManager}. Don't use this
+ * class directly.
+ */
+class TunerTsStreamerManager {
+ // The lock will protect mStreamerFinder, mSourceToStreamerMap and some part of TsStreamCreator
+ // to support timely {@link TunerTsStreamer} cancellation due to a new tune request from
+ // the same session.
+ 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;
+
+ /**
+ * Returns the singleton instance for the class
+ *
+ * @return TunerTsStreamerManager
+ */
+ static synchronized TunerTsStreamerManager getInstance() {
+ if (sInstance == null) {
+ sInstance = new TunerTsStreamerManager();
+ }
+ return sInstance;
+ }
+
+ private TunerTsStreamerManager() {}
+
+ synchronized TsDataSource createDataSource(
+ Context context,
+ TunerChannel channel,
+ EventDetector.EventListener listener,
+ int sessionId,
+ boolean reuse) {
+ TsStreamerCreator creator;
+ synchronized (mCancelLock) {
+ if (mStreamerFinder.containsLocked(channel)) {
+ 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;
+ }
+ creator = new TsStreamerCreator(context, channel, listener);
+ mCreators.put(sessionId, creator);
+ }
+ TunerTsStreamer streamer = creator.create(sessionId, reuse);
+ synchronized (mCancelLock) {
+ mCreators.remove(sessionId);
+ if (streamer == null) {
+ return null;
+ }
+ if (!creator.isCancelledLocked()) {
+ mStreamerFinder.putLocked(channel, sessionId, streamer);
+ TsDataSource source = streamer.createDataSource();
+ mListeners.put(sessionId, listener);
+ mSourceToStreamerMap.put(source, streamer);
+ return source;
+ }
+ }
+ // Created streamer was cancelled by a new tune request.
+ streamer.stopStream();
+ TunerHal hal = streamer.getTunerHal();
+ hal.setHasPendingTune(false);
+ mTunerHalManager.releaseTunerHal(hal, sessionId, reuse);
+ return null;
+ }
+
+ synchronized void releaseDataSource(TsDataSource source, int sessionId, boolean reuse) {
+ TunerTsStreamer streamer;
+ synchronized (mCancelLock) {
+ streamer = mSourceToStreamerMap.get(source);
+ mSourceToStreamerMap.remove(source);
+ 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);
+ if (mStreamerFinder.containsLocked(channel)) {
+ return;
+ }
+ }
+ streamer.stopStream();
+ TunerHal hal = streamer.getTunerHal();
+ hal.setHasPendingTune(false);
+ mTunerHalManager.releaseTunerHal(hal, sessionId, reuse);
+ }
+
+ void setHasPendingTune(int sessionId) {
+ synchronized (mCancelLock) {
+ if (mCreators.containsKey(sessionId)) {
+ mCreators.get(sessionId).cancelLocked();
+ }
+ }
+ }
+
+ /** 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);
+ }
+
+ private class StreamerFinder {
+ private final Map<TunerChannel, Set<Integer>> mSessions = new HashMap<>();
+ private final Map<TunerChannel, TunerTsStreamer> mStreamers = new HashMap<>();
+
+ // @GuardedBy("mCancelLock")
+ private void putLocked(TunerChannel channel, int sessionId, TunerTsStreamer streamer) {
+ Set<Integer> sessions = new HashSet<>();
+ sessions.add(sessionId);
+ mSessions.put(channel, sessions);
+ mStreamers.put(channel, streamer);
+ }
+
+ // @GuardedBy("mCancelLock")
+ private void appendSessionLocked(TunerChannel channel, int sessionId) {
+ if (mSessions.containsKey(channel)) {
+ mSessions.get(channel).add(sessionId);
+ }
+ }
+
+ // @GuardedBy("mCancelLock")
+ private void removeSessionLocked(TunerChannel channel, int sessionId) {
+ Set<Integer> sessions = mSessions.get(channel);
+ sessions.remove(sessionId);
+ if (sessions.size() == 0) {
+ mSessions.remove(channel);
+ mStreamers.remove(channel);
+ }
+ }
+
+ // @GuardedBy("mCancelLock")
+ private boolean containsLocked(TunerChannel channel) {
+ return mSessions.containsKey(channel);
+ }
+
+ // @GuardedBy("mCancelLock")
+ private TunerTsStreamer getStreamerLocked(TunerChannel channel) {
+ return mStreamers.containsKey(channel) ? mStreamers.get(channel) : null;
+ }
+ }
+
+ /**
+ * {@link TunerTsStreamer} creation can be cancelled by a new tune request for the same session.
+ * The class supports the cancellation in creating new {@link TunerTsStreamer}.
+ */
+ private class TsStreamerCreator {
+ private final Context mContext;
+ private final TunerChannel mChannel;
+ private final EventDetector.EventListener mEventListener;
+ // mCancelled will be {@code true} if a new tune request for the same session
+ // cancels create().
+ private boolean mCancelled;
+ private TunerHal mTunerHal;
+
+ private TsStreamerCreator(
+ Context context, TunerChannel channel, EventDetector.EventListener listener) {
+ mContext = context;
+ mChannel = channel;
+ mEventListener = listener;
+ }
+
+ private TunerTsStreamer create(int sessionId, boolean reuse) {
+ TunerHal hal = mTunerHalManager.getOrCreateTunerHal(mContext, sessionId);
+ if (hal == null) {
+ return null;
+ }
+ boolean canceled = false;
+ synchronized (mCancelLock) {
+ if (!mCancelled) {
+ mTunerHal = hal;
+ } else {
+ canceled = true;
+ }
+ }
+ if (!canceled) {
+ TunerTsStreamer tsStreamer = new TunerTsStreamer(hal, mEventListener, mContext);
+ if (tsStreamer.startStream(mChannel)) {
+ return tsStreamer;
+ }
+ synchronized (mCancelLock) {
+ mTunerHal = null;
+ }
+ }
+ hal.setHasPendingTune(false);
+ // Since TunerTsStreamer is not properly created, closes TunerHal.
+ // And do not re-use TunerHal when it is not cancelled.
+ mTunerHalManager.releaseTunerHal(hal, sessionId, mCancelled && reuse);
+ return null;
+ }
+
+ // @GuardedBy("mCancelLock")
+ private void cancelLocked() {
+ if (mCancelled) {
+ return;
+ }
+ mCancelled = true;
+ if (mTunerHal != null) {
+ mTunerHal.setHasPendingTune(true);
+ }
+ }
+
+ // @GuardedBy("mCancelLock")
+ private boolean isCancelledLocked() {
+ return mCancelled;
+ }
+ }
+
+ /**
+ * Supports sharing {@link TunerHal} among multiple sessions. The class also supports session
+ * affinity for {@link TunerHal} allocation.
+ */
+ private class TunerHalManager {
+ private final Map<Integer, TunerHal> mTunerHals = new HashMap<>();
+
+ private TunerHal getOrCreateTunerHal(Context context, int sessionId) {
+ // Handles session affinity.
+ TunerHal hal = mTunerHals.get(sessionId);
+ if (hal != null) {
+ mTunerHals.remove(sessionId);
+ return hal;
+ }
+ // Finds a TunerHal which is cached for other sessions.
+ Iterator it = mTunerHals.keySet().iterator();
+ if (it.hasNext()) {
+ Integer key = (Integer) it.next();
+ hal = mTunerHals.get(key);
+ mTunerHals.remove(key);
+ return hal;
+ }
+ return TunerHal.createInstance(context);
+ }
+
+ private void releaseTunerHal(TunerHal hal, int sessionId, boolean reuse) {
+ if (!reuse || !hal.isReusable()) {
+ AutoCloseableUtils.closeQuietly(hal);
+ return;
+ }
+ TunerHal cachedHal = mTunerHals.get(sessionId);
+ if (cachedHal != hal) {
+ mTunerHals.put(sessionId, hal);
+ if (cachedHal != null) {
+ AutoCloseableUtils.closeQuietly(cachedHal);
+ }
+ }
+ }
+
+ private void releaseCachedHal(int sessionId) {
+ TunerHal hal = mTunerHals.get(sessionId);
+ if (hal != null) {
+ mTunerHals.remove(sessionId);
+ }
+ if (hal != null) {
+ AutoCloseableUtils.closeQuietly(hal);
+ }
+ }
+
+ private void addTunerHal(TunerHal tunerHal, int sessionId) {
+ mTunerHals.put(sessionId, tunerHal);
+ }
+ }
+}
diff --git a/src/com/android/tv/tuner/ts/SectionParser.java b/src/com/android/tv/tuner/ts/SectionParser.java
new file mode 100644
index 00000000..6d0eb90f
--- /dev/null
+++ b/src/com/android/tv/tuner/ts/SectionParser.java
@@ -0,0 +1,2083 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.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.PsiData.PatItem;
+import com.android.tv.tuner.data.PsiData.PmtItem;
+import com.android.tv.tuner.data.PsipData.Ac3AudioDescriptor;
+import com.android.tv.tuner.data.PsipData.CaptionServiceDescriptor;
+import com.android.tv.tuner.data.PsipData.ContentAdvisoryDescriptor;
+import com.android.tv.tuner.data.PsipData.EitItem;
+import com.android.tv.tuner.data.PsipData.EttItem;
+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.ArrayList;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/** Parses ATSC PSIP sections. */
+public class SectionParser {
+ private static final String TAG = "SectionParser";
+ private static final boolean DEBUG = false;
+
+ private static final byte TABLE_ID_PAT = (byte) 0x00;
+ private static final byte TABLE_ID_PMT = (byte) 0x02;
+ private static final byte TABLE_ID_MGT = (byte) 0xc7;
+ private static final byte TABLE_ID_TVCT = (byte) 0xc8;
+ private static final byte TABLE_ID_CVCT = (byte) 0xc9;
+ 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;
+ public static final int DESCRIPTOR_TAG_CONTENT_ADVISORY = 0x87;
+ public static final int DESCRIPTOR_TAG_AC3_AUDIO_STREAM = 0x81;
+ 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;
+ private static final byte MODE_SCSU = (byte) 0x3e;
+ private static final int MAX_SHORT_NAME_BYTES = 14;
+
+ // See ANSI/CEA-766-C.
+ private static final int RATING_REGION_US_TV = 1;
+ private static final int RATING_REGION_KR_TV = 4;
+
+ // 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
+ * To see the details of pycrc, visit http://www.tty1.net/pycrc/index_en.html
+ */
+ public static final int[] CRC_TABLE = {
+ 0x00000000, 0x04c11db7, 0x09823b6e, 0x0d4326d9,
+ 0x130476dc, 0x17c56b6b, 0x1a864db2, 0x1e475005,
+ 0x2608edb8, 0x22c9f00f, 0x2f8ad6d6, 0x2b4bcb61,
+ 0x350c9b64, 0x31cd86d3, 0x3c8ea00a, 0x384fbdbd,
+ 0x4c11db70, 0x48d0c6c7, 0x4593e01e, 0x4152fda9,
+ 0x5f15adac, 0x5bd4b01b, 0x569796c2, 0x52568b75,
+ 0x6a1936c8, 0x6ed82b7f, 0x639b0da6, 0x675a1011,
+ 0x791d4014, 0x7ddc5da3, 0x709f7b7a, 0x745e66cd,
+ 0x9823b6e0, 0x9ce2ab57, 0x91a18d8e, 0x95609039,
+ 0x8b27c03c, 0x8fe6dd8b, 0x82a5fb52, 0x8664e6e5,
+ 0xbe2b5b58, 0xbaea46ef, 0xb7a96036, 0xb3687d81,
+ 0xad2f2d84, 0xa9ee3033, 0xa4ad16ea, 0xa06c0b5d,
+ 0xd4326d90, 0xd0f37027, 0xddb056fe, 0xd9714b49,
+ 0xc7361b4c, 0xc3f706fb, 0xceb42022, 0xca753d95,
+ 0xf23a8028, 0xf6fb9d9f, 0xfbb8bb46, 0xff79a6f1,
+ 0xe13ef6f4, 0xe5ffeb43, 0xe8bccd9a, 0xec7dd02d,
+ 0x34867077, 0x30476dc0, 0x3d044b19, 0x39c556ae,
+ 0x278206ab, 0x23431b1c, 0x2e003dc5, 0x2ac12072,
+ 0x128e9dcf, 0x164f8078, 0x1b0ca6a1, 0x1fcdbb16,
+ 0x018aeb13, 0x054bf6a4, 0x0808d07d, 0x0cc9cdca,
+ 0x7897ab07, 0x7c56b6b0, 0x71159069, 0x75d48dde,
+ 0x6b93dddb, 0x6f52c06c, 0x6211e6b5, 0x66d0fb02,
+ 0x5e9f46bf, 0x5a5e5b08, 0x571d7dd1, 0x53dc6066,
+ 0x4d9b3063, 0x495a2dd4, 0x44190b0d, 0x40d816ba,
+ 0xaca5c697, 0xa864db20, 0xa527fdf9, 0xa1e6e04e,
+ 0xbfa1b04b, 0xbb60adfc, 0xb6238b25, 0xb2e29692,
+ 0x8aad2b2f, 0x8e6c3698, 0x832f1041, 0x87ee0df6,
+ 0x99a95df3, 0x9d684044, 0x902b669d, 0x94ea7b2a,
+ 0xe0b41de7, 0xe4750050, 0xe9362689, 0xedf73b3e,
+ 0xf3b06b3b, 0xf771768c, 0xfa325055, 0xfef34de2,
+ 0xc6bcf05f, 0xc27dede8, 0xcf3ecb31, 0xcbffd686,
+ 0xd5b88683, 0xd1799b34, 0xdc3abded, 0xd8fba05a,
+ 0x690ce0ee, 0x6dcdfd59, 0x608edb80, 0x644fc637,
+ 0x7a089632, 0x7ec98b85, 0x738aad5c, 0x774bb0eb,
+ 0x4f040d56, 0x4bc510e1, 0x46863638, 0x42472b8f,
+ 0x5c007b8a, 0x58c1663d, 0x558240e4, 0x51435d53,
+ 0x251d3b9e, 0x21dc2629, 0x2c9f00f0, 0x285e1d47,
+ 0x36194d42, 0x32d850f5, 0x3f9b762c, 0x3b5a6b9b,
+ 0x0315d626, 0x07d4cb91, 0x0a97ed48, 0x0e56f0ff,
+ 0x1011a0fa, 0x14d0bd4d, 0x19939b94, 0x1d528623,
+ 0xf12f560e, 0xf5ee4bb9, 0xf8ad6d60, 0xfc6c70d7,
+ 0xe22b20d2, 0xe6ea3d65, 0xeba91bbc, 0xef68060b,
+ 0xd727bbb6, 0xd3e6a601, 0xdea580d8, 0xda649d6f,
+ 0xc423cd6a, 0xc0e2d0dd, 0xcda1f604, 0xc960ebb3,
+ 0xbd3e8d7e, 0xb9ff90c9, 0xb4bcb610, 0xb07daba7,
+ 0xae3afba2, 0xaafbe615, 0xa7b8c0cc, 0xa379dd7b,
+ 0x9b3660c6, 0x9ff77d71, 0x92b45ba8, 0x9675461f,
+ 0x8832161a, 0x8cf30bad, 0x81b02d74, 0x857130c3,
+ 0x5d8a9099, 0x594b8d2e, 0x5408abf7, 0x50c9b640,
+ 0x4e8ee645, 0x4a4ffbf2, 0x470cdd2b, 0x43cdc09c,
+ 0x7b827d21, 0x7f436096, 0x7200464f, 0x76c15bf8,
+ 0x68860bfd, 0x6c47164a, 0x61043093, 0x65c52d24,
+ 0x119b4be9, 0x155a565e, 0x18197087, 0x1cd86d30,
+ 0x029f3d35, 0x065e2082, 0x0b1d065b, 0x0fdc1bec,
+ 0x3793a651, 0x3352bbe6, 0x3e119d3f, 0x3ad08088,
+ 0x2497d08d, 0x2056cd3a, 0x2d15ebe3, 0x29d4f654,
+ 0xc5a92679, 0xc1683bce, 0xcc2b1d17, 0xc8ea00a0,
+ 0xd6ad50a5, 0xd26c4d12, 0xdf2f6bcb, 0xdbee767c,
+ 0xe3a1cbc1, 0xe760d676, 0xea23f0af, 0xeee2ed18,
+ 0xf0a5bd1d, 0xf464a0aa, 0xf9278673, 0xfde69bc4,
+ 0x89b8fd09, 0x8d79e0be, 0x803ac667, 0x84fbdbd0,
+ 0x9abc8bd5, 0x9e7d9662, 0x933eb0bb, 0x97ffad0c,
+ 0xafb010b1, 0xab710d06, 0xa6322bdf, 0xa2f33668,
+ 0xbcb4666d, 0xb8757bda, 0xb5365d03, 0xb1f740b4
+ };
+
+ // A table which maps ATSC genres to TIF genres.
+ // See ATSC/65 Table 6.20.
+ private static final String[] CANONICAL_GENRES_TABLE = {
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ Genres.EDUCATION,
+ Genres.ENTERTAINMENT,
+ Genres.MOVIES,
+ Genres.NEWS,
+ Genres.LIFE_STYLE,
+ Genres.SPORTS,
+ null,
+ Genres.MOVIES,
+ null,
+ Genres.FAMILY_KIDS,
+ Genres.DRAMA,
+ null,
+ Genres.ENTERTAINMENT,
+ Genres.SPORTS,
+ Genres.SPORTS,
+ null,
+ null,
+ Genres.MUSIC,
+ Genres.EDUCATION,
+ null,
+ Genres.COMEDY,
+ null,
+ Genres.MUSIC,
+ null,
+ null,
+ Genres.MOVIES,
+ Genres.ENTERTAINMENT,
+ Genres.NEWS,
+ Genres.DRAMA,
+ Genres.EDUCATION,
+ Genres.MOVIES,
+ Genres.SPORTS,
+ Genres.MOVIES,
+ null,
+ Genres.LIFE_STYLE,
+ Genres.ARTS,
+ Genres.LIFE_STYLE,
+ Genres.SPORTS,
+ null,
+ null,
+ Genres.GAMING,
+ Genres.LIFE_STYLE,
+ Genres.SPORTS,
+ null,
+ Genres.LIFE_STYLE,
+ Genres.EDUCATION,
+ Genres.EDUCATION,
+ Genres.LIFE_STYLE,
+ Genres.SPORTS,
+ Genres.LIFE_STYLE,
+ Genres.MOVIES,
+ Genres.NEWS,
+ null,
+ null,
+ null,
+ Genres.EDUCATION,
+ null,
+ null,
+ null,
+ Genres.EDUCATION,
+ null,
+ null,
+ null,
+ Genres.DRAMA,
+ Genres.MUSIC,
+ Genres.MOVIES,
+ null,
+ Genres.ANIMAL_WILDLIFE,
+ null,
+ null,
+ Genres.PREMIER,
+ null,
+ null,
+ null,
+ null,
+ Genres.SPORTS,
+ Genres.ARTS,
+ null,
+ null,
+ null,
+ Genres.MOVIES,
+ Genres.TECH_SCIENCE,
+ Genres.DRAMA,
+ null,
+ Genres.SHOPPING,
+ Genres.DRAMA,
+ null,
+ Genres.MOVIES,
+ Genres.ENTERTAINMENT,
+ Genres.TECH_SCIENCE,
+ Genres.SPORTS,
+ Genres.TRAVEL,
+ Genres.ENTERTAINMENT,
+ Genres.ARTS,
+ Genres.NEWS,
+ null,
+ Genres.ARTS,
+ Genres.SPORTS,
+ Genres.SPORTS,
+ Genres.NEWS,
+ Genres.SPORTS,
+ Genres.SPORTS,
+ Genres.SPORTS,
+ Genres.FAMILY_KIDS,
+ Genres.FAMILY_KIDS,
+ Genres.MOVIES,
+ null,
+ Genres.TECH_SCIENCE,
+ Genres.MUSIC,
+ null,
+ Genres.SPORTS,
+ Genres.FAMILY_KIDS,
+ Genres.NEWS,
+ Genres.SPORTS,
+ Genres.NEWS,
+ Genres.SPORTS,
+ Genres.ANIMAL_WILDLIFE,
+ null,
+ Genres.MUSIC,
+ Genres.NEWS,
+ Genres.SPORTS,
+ null,
+ Genres.NEWS,
+ Genres.NEWS,
+ Genres.NEWS,
+ Genres.NEWS,
+ Genres.SPORTS,
+ Genres.MOVIES,
+ Genres.ARTS,
+ Genres.ANIMAL_WILDLIFE,
+ Genres.MUSIC,
+ Genres.MUSIC,
+ Genres.MOVIES,
+ Genres.EDUCATION,
+ Genres.DRAMA,
+ Genres.SPORTS,
+ Genres.SPORTS,
+ Genres.SPORTS,
+ Genres.SPORTS,
+ null,
+ Genres.SPORTS,
+ Genres.SPORTS,
+ };
+
+ // A table which contains ATSC categorical genre code assignments.
+ // See ATSC/65 Table 6.20.
+ private static final String[] BROADCAST_GENRES_TABLE =
+ new String[] {
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ "Education",
+ "Entertainment",
+ "Movie",
+ "News",
+ "Religious",
+ "Sports",
+ "Other",
+ "Action",
+ "Advertisement",
+ "Animated",
+ "Anthology",
+ "Automobile",
+ "Awards",
+ "Baseball",
+ "Basketball",
+ "Bulletin",
+ "Business",
+ "Classical",
+ "College",
+ "Combat",
+ "Comedy",
+ "Commentary",
+ "Concert",
+ "Consumer",
+ "Contemporary",
+ "Crime",
+ "Dance",
+ "Documentary",
+ "Drama",
+ "Elementary",
+ "Erotica",
+ "Exercise",
+ "Fantasy",
+ "Farm",
+ "Fashion",
+ "Fiction",
+ "Food",
+ "Football",
+ "Foreign",
+ "Fund Raiser",
+ "Game/Quiz",
+ "Garden",
+ "Golf",
+ "Government",
+ "Health",
+ "High School",
+ "History",
+ "Hobby",
+ "Hockey",
+ "Home",
+ "Horror",
+ "Information",
+ "Instruction",
+ "International",
+ "Interview",
+ "Language",
+ "Legal",
+ "Live",
+ "Local",
+ "Math",
+ "Medical",
+ "Meeting",
+ "Military",
+ "Miniseries",
+ "Music",
+ "Mystery",
+ "National",
+ "Nature",
+ "Police",
+ "Politics",
+ "Premier",
+ "Prerecorded",
+ "Product",
+ "Professional",
+ "Public",
+ "Racing",
+ "Reading",
+ "Repair",
+ "Repeat",
+ "Review",
+ "Romance",
+ "Science",
+ "Series",
+ "Service",
+ "Shopping",
+ "Soap Opera",
+ "Special",
+ "Suspense",
+ "Talk",
+ "Technical",
+ "Tennis",
+ "Travel",
+ "Variety",
+ "Video",
+ "Weather",
+ "Western",
+ "Art",
+ "Auto Racing",
+ "Aviation",
+ "Biography",
+ "Boating",
+ "Bowling",
+ "Boxing",
+ "Cartoon",
+ "Children",
+ "Classic Film",
+ "Community",
+ "Computers",
+ "Country Music",
+ "Court",
+ "Extreme Sports",
+ "Family",
+ "Financial",
+ "Gymnastics",
+ "Headlines",
+ "Horse Racing",
+ "Hunting/Fishing/Outdoors",
+ "Independent",
+ "Jazz",
+ "Magazine",
+ "Motorcycle Racing",
+ "Music/Film/Books",
+ "News-International",
+ "News-Local",
+ "News-National",
+ "News-Regional",
+ "Olympics",
+ "Original",
+ "Performing Arts",
+ "Pets/Animals",
+ "Pop",
+ "Rock & Roll",
+ "Sci-Fi",
+ "Self Improvement",
+ "Sitcom",
+ "Skating",
+ "Skiing",
+ "Soccer",
+ "Track/Field",
+ "True",
+ "Volleyball",
+ "Wrestling",
+ };
+
+ // Audio language code map from ISO 639-2/B to 639-2/T, in order to show correct audio language.
+ private static final HashMap<String, String> ISO_LANGUAGE_CODE_MAP;
+
+ static {
+ ISO_LANGUAGE_CODE_MAP = new HashMap<>();
+ ISO_LANGUAGE_CODE_MAP.put("alb", "sqi");
+ ISO_LANGUAGE_CODE_MAP.put("arm", "hye");
+ ISO_LANGUAGE_CODE_MAP.put("baq", "eus");
+ ISO_LANGUAGE_CODE_MAP.put("bur", "mya");
+ ISO_LANGUAGE_CODE_MAP.put("chi", "zho");
+ ISO_LANGUAGE_CODE_MAP.put("cze", "ces");
+ ISO_LANGUAGE_CODE_MAP.put("dut", "nld");
+ ISO_LANGUAGE_CODE_MAP.put("fre", "fra");
+ ISO_LANGUAGE_CODE_MAP.put("geo", "kat");
+ ISO_LANGUAGE_CODE_MAP.put("ger", "deu");
+ ISO_LANGUAGE_CODE_MAP.put("gre", "ell");
+ ISO_LANGUAGE_CODE_MAP.put("ice", "isl");
+ ISO_LANGUAGE_CODE_MAP.put("mac", "mkd");
+ ISO_LANGUAGE_CODE_MAP.put("mao", "mri");
+ ISO_LANGUAGE_CODE_MAP.put("may", "msa");
+ ISO_LANGUAGE_CODE_MAP.put("per", "fas");
+ ISO_LANGUAGE_CODE_MAP.put("rum", "ron");
+ ISO_LANGUAGE_CODE_MAP.put("slo", "slk");
+ ISO_LANGUAGE_CODE_MAP.put("tib", "bod");
+ ISO_LANGUAGE_CODE_MAP.put("wel", "cym");
+ ISO_LANGUAGE_CODE_MAP.put("esl", "spa"); // Special entry for channel 9-1 KQED in bay area.
+ }
+
+ // Containers to store the last version numbers of the PSIP sections.
+ private final HashMap<PsipSection, Integer> mSectionVersionMap = new HashMap<>();
+ private final SparseArray<List<EttItem>> mParsedEttItems = new SparseArray<>();
+
+ public interface OutputListener {
+ void onPatParsed(List<PatItem> items);
+
+ void onPmtParsed(int programNumber, List<PmtItem> items);
+
+ void onMgtParsed(List<MgtItem> items);
+
+ 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;
+
+ public SectionParser(OutputListener listener) {
+ mListener = listener;
+ }
+
+ public void parseSections(ByteArrayBuffer data) {
+ int pos = 0;
+ while (pos + 3 <= data.length()) {
+ if ((data.byteAt(pos) & 0xff) == 0xff) {
+ // Clear stuffing bytes according to H222.0 section 2.4.4.
+ data.setLength(0);
+ break;
+ }
+ int sectionLength =
+ (((data.byteAt(pos + 1) & 0x0f) << 8) | (data.byteAt(pos + 2) & 0xff)) + 3;
+ if (pos + sectionLength > data.length()) {
+ break;
+ }
+ if (DEBUG) {
+ Log.d(TAG, "parseSections 0x" + Integer.toHexString(data.byteAt(pos) & 0xff));
+ }
+ parseSection(Arrays.copyOfRange(data.buffer(), pos, pos + sectionLength));
+ pos += sectionLength;
+ }
+ if (mListener != null) {
+ for (int i = 0; i < mParsedEttItems.size(); ++i) {
+ int sourceId = mParsedEttItems.keyAt(i);
+ List<EttItem> descriptions = mParsedEttItems.valueAt(i);
+ mListener.onEttParsed(sourceId, descriptions);
+ }
+ }
+ mParsedEttItems.clear();
+ }
+
+ public void resetVersionNumbers() {
+ mSectionVersionMap.clear();
+ }
+
+ private void parseSection(byte[] data) {
+ if (!checkSanity(data)) {
+ Log.d(TAG, "Bad CRC!");
+ return;
+ }
+ PsipSection section = PsipSection.create(data);
+ if (section == null) {
+ return;
+ }
+
+ // The currentNextIndicator indicates that the section sent is currently applicable.
+ if (!section.getCurrentNextIndicator()) {
+ return;
+ }
+ int versionNumber = (data[5] & 0x3e) >> 1;
+ Integer oldVersionNumber = mSectionVersionMap.get(section);
+
+ // The versionNumber shall be incremented when a change in the information carried within
+ // the section occurs.
+ if (oldVersionNumber != null && versionNumber == oldVersionNumber) {
+ return;
+ }
+ boolean result = false;
+ switch (data[0]) {
+ case TABLE_ID_PAT:
+ result = parsePAT(data);
+ break;
+ case TABLE_ID_PMT:
+ result = parsePMT(data);
+ break;
+ case TABLE_ID_MGT:
+ result = parseMGT(data);
+ break;
+ case TABLE_ID_TVCT:
+ case TABLE_ID_CVCT:
+ result = parseVCT(data);
+ break;
+ case TABLE_ID_EIT:
+ result = parseEIT(data);
+ break;
+ 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;
+ }
+ if (result) {
+ mSectionVersionMap.put(section, versionNumber);
+ }
+ }
+
+ private boolean parsePAT(byte[] data) {
+ if (DEBUG) {
+ Log.d(TAG, "PAT is discovered.");
+ }
+ int pos = 8;
+
+ List<PatItem> results = new ArrayList<>();
+ for (; pos < data.length - 4; pos = pos + 4) {
+ if (pos > data.length - 4 - 4) {
+ Log.e(TAG, "Broken PAT.");
+ return false;
+ }
+ int programNo = ((data[pos] & 0xff) << 8) | (data[pos + 1] & 0xff);
+ int pmtPid = ((data[pos + 2] & 0x1f) << 8) | (data[pos + 3] & 0xff);
+ results.add(new PatItem(programNo, pmtPid));
+ }
+ if (mListener != null) {
+ mListener.onPatParsed(results);
+ }
+ return true;
+ }
+
+ private boolean parsePMT(byte[] data) {
+ int table_id_ext = ((data[3] & 0xff) << 8) | (data[4] & 0xff);
+ if (DEBUG) {
+ Log.d(TAG, "PMT is discovered. programNo = " + table_id_ext);
+ }
+ if (data.length <= 11) {
+ Log.e(TAG, "Broken PMT.");
+ return false;
+ }
+ int pcrPid = (data[8] & 0x1f) << 8 | data[9];
+ int programInfoLen = (data[10] & 0x0f) << 8 | data[11];
+ int pos = 12;
+ List<TsDescriptor> descriptors = parseDescriptors(data, pos, pos + programInfoLen);
+ pos += programInfoLen;
+ if (DEBUG) {
+ Log.d(TAG, "PMT descriptors size: " + descriptors.size());
+ }
+ List<PmtItem> results = new ArrayList<>();
+ for (; pos < data.length - 4; ) {
+ if (pos < 0) {
+ Log.e(TAG, "Broken PMT.");
+ return false;
+ }
+ int streamType = data[pos] & 0xff;
+ int esPid = (data[pos + 1] & 0x1f) << 8 | (data[pos + 2] & 0xff);
+ int esInfoLen = (data[pos + 3] & 0xf) << 8 | (data[pos + 4] & 0xff);
+ if (data.length < pos + esInfoLen + 5) {
+ Log.e(TAG, "Broken PMT.");
+ return false;
+ }
+ descriptors = parseDescriptors(data, pos + 5, pos + 5 + esInfoLen);
+ List<AtscAudioTrack> audioTracks = generateAudioTracks(descriptors);
+ List<AtscCaptionTrack> captionTracks = generateCaptionTracks(descriptors);
+ PmtItem pmtItem = new PmtItem(streamType, esPid, audioTracks, captionTracks);
+ if (DEBUG) {
+ Log.d(TAG, "PMT " + pmtItem + " descriptors size: " + descriptors.size());
+ }
+ results.add(pmtItem);
+ pos = pos + esInfoLen + 5;
+ }
+ results.add(new PmtItem(PmtItem.ES_PID_PCR, pcrPid, null, null));
+ if (mListener != null) {
+ mListener.onPmtParsed(table_id_ext, results);
+ }
+ return true;
+ }
+
+ private boolean parseMGT(byte[] data) {
+ // For details of the structure for MGT, see ATSC A/65 Table 6.2.
+ if (DEBUG) {
+ Log.d(TAG, "MGT is discovered.");
+ }
+ if (data.length <= 10) {
+ Log.e(TAG, "Broken MGT.");
+ return false;
+ }
+ int tablesDefined = ((data[9] & 0xff) << 8) | (data[10] & 0xff);
+ int pos = 11;
+ List<MgtItem> results = new ArrayList<>();
+ for (int i = 0; i < tablesDefined; ++i) {
+ if (data.length <= pos + 10) {
+ Log.e(TAG, "Broken MGT.");
+ return false;
+ }
+ int tableType = ((data[pos] & 0xff) << 8) | (data[pos + 1] & 0xff);
+ int tableTypePid = ((data[pos + 2] & 0x1f) << 8) | (data[pos + 3] & 0xff);
+ int descriptorsLength = ((data[pos + 9] & 0x0f) << 8) | (data[pos + 10] & 0xff);
+ pos += 11 + descriptorsLength;
+ results.add(new MgtItem(tableType, tableTypePid));
+ }
+ // Skip the remaining descriptor part which we don't use.
+
+ if (mListener != null) {
+ mListener.onMgtParsed(results);
+ }
+ return true;
+ }
+
+ private boolean parseVCT(byte[] data) {
+ // For details of the structure for VCT, see ATSC A/65 Table 6.4 and 6.8.
+ if (DEBUG) {
+ Log.d(TAG, "VCT is discovered.");
+ }
+ if (data.length <= 9) {
+ Log.e(TAG, "Broken VCT.");
+ return false;
+ }
+ int numChannelsInSection = (data[9] & 0xff);
+ int sectionNumber = (data[6] & 0xff);
+ int lastSectionNumber = (data[7] & 0xff);
+ if (sectionNumber > lastSectionNumber) {
+ // According to section 6.3.1 of the spec ATSC A/65,
+ // last section number is the largest section number.
+ Log.w(
+ TAG,
+ "Invalid VCT. Section Number "
+ + sectionNumber
+ + " > Last Section Number "
+ + lastSectionNumber);
+ return false;
+ }
+ int pos = 10;
+ List<VctItem> results = new ArrayList<>();
+ for (int i = 0; i < numChannelsInSection; ++i) {
+ if (data.length <= pos + 31) {
+ Log.e(TAG, "Broken VCT.");
+ return false;
+ }
+ String shortName = "";
+ int shortNameSize = getShortNameSize(data, pos);
+ try {
+ shortName =
+ new String(Arrays.copyOfRange(data, pos, pos + shortNameSize), "UTF-16");
+ } catch (UnsupportedEncodingException e) {
+ Log.e(TAG, "Broken VCT.", e);
+ return false;
+ }
+ if ((data[pos + 14] & 0xf0) != 0xf0) {
+ Log.e(TAG, "Broken VCT.");
+ return false;
+ }
+ int majorNumber = ((data[pos + 14] & 0x0f) << 6) | ((data[pos + 15] & 0xff) >> 2);
+ int minorNumber = ((data[pos + 15] & 0x03) << 8) | (data[pos + 16] & 0xff);
+ if ((majorNumber & 0x3f0) == 0x3f0) {
+ // If the six MSBs are 111111, these indicate that there is only one-part channel
+ // number. To see details, refer A/65 Section 6.3.2.
+ majorNumber = ((majorNumber & 0xf) << 10) + minorNumber;
+ minorNumber = 0;
+ }
+ int channelTsid = ((data[pos + 22] & 0xff) << 8) | (data[pos + 23] & 0xff);
+ int programNumber = ((data[pos + 24] & 0xff) << 8) | (data[pos + 25] & 0xff);
+ boolean accessControlled = (data[pos + 26] & 0x20) != 0;
+ boolean hidden = (data[pos + 26] & 0x10) != 0;
+ int serviceType = (data[pos + 27] & 0x3f);
+ int sourceId = ((data[pos + 28] & 0xff) << 8) | (data[pos + 29] & 0xff);
+ int descriptorsPos = pos + 32;
+ int descriptorsLength = ((data[pos + 30] & 0x03) << 8) | (data[pos + 31] & 0xff);
+ pos += 32 + descriptorsLength;
+ if (data.length < pos) {
+ Log.e(TAG, "Broken VCT.");
+ return false;
+ }
+ List<TsDescriptor> descriptors =
+ parseDescriptors(data, descriptorsPos, descriptorsPos + descriptorsLength);
+ String longName = null;
+ for (TsDescriptor descriptor : descriptors) {
+ if (descriptor instanceof ExtendedChannelNameDescriptor) {
+ ExtendedChannelNameDescriptor extendedChannelNameDescriptor =
+ (ExtendedChannelNameDescriptor) descriptor;
+ longName = extendedChannelNameDescriptor.getLongChannelName();
+ break;
+ }
+ }
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ String.format(
+ "Found channel [%s] %s - serviceType: %d tsid: 0x%x program: %d "
+ + "channel: %d-%d encrypted: %b hidden: %b, descriptors: %d",
+ shortName,
+ longName,
+ serviceType,
+ channelTsid,
+ programNumber,
+ majorNumber,
+ minorNumber,
+ accessControlled,
+ hidden,
+ descriptors.size()));
+ }
+ if (!accessControlled
+ && !hidden
+ && (serviceType == Channel.SERVICE_TYPE_ATSC_AUDIO
+ || serviceType == Channel.SERVICE_TYPE_ATSC_DIGITAL_TELEVISION
+ || serviceType
+ == Channel.SERVICE_TYPE_UNASSOCIATED_SMALL_SCREEN_SERVICE)) {
+ // Hide hidden, encrypted, or unsupported ATSC service type channels
+ results.add(
+ new VctItem(
+ shortName,
+ longName,
+ serviceType,
+ channelTsid,
+ programNumber,
+ majorNumber,
+ minorNumber,
+ sourceId));
+ }
+ }
+ // Skip the remaining descriptor part which we don't use.
+
+ if (mListener != null) {
+ mListener.onVctParsed(results, sectionNumber, lastSectionNumber);
+ }
+ return true;
+ }
+
+ private boolean parseEIT(byte[] data) {
+ // For details of the structure for EIT, see ATSC A/65 Table 6.11.
+ if (DEBUG) {
+ Log.d(TAG, "EIT is discovered.");
+ }
+ if (data.length <= 9) {
+ Log.e(TAG, "Broken EIT.");
+ return false;
+ }
+ int sourceId = ((data[3] & 0xff) << 8) | (data[4] & 0xff);
+ int numEventsInSection = (data[9] & 0xff);
+
+ int pos = 10;
+ List<EitItem> results = new ArrayList<>();
+ for (int i = 0; i < numEventsInSection; ++i) {
+ if (data.length <= pos + 9) {
+ Log.e(TAG, "Broken EIT.");
+ return false;
+ }
+ if ((data[pos] & 0xc0) != 0xc0) {
+ Log.e(TAG, "Broken EIT.");
+ return false;
+ }
+ int eventId = ((data[pos] & 0x3f) << 8) + (data[pos + 1] & 0xff);
+ long startTime =
+ ((data[pos + 2] & (long) 0xff) << 24)
+ | ((data[pos + 3] & 0xff) << 16)
+ | ((data[pos + 4] & 0xff) << 8)
+ | (data[pos + 5] & 0xff);
+ int lengthInSecond =
+ ((data[pos + 6] & 0x0f) << 16)
+ | ((data[pos + 7] & 0xff) << 8)
+ | (data[pos + 8] & 0xff);
+ int titleLength = (data[pos + 9] & 0xff);
+ if (data.length <= pos + 10 + titleLength + 1) {
+ Log.e(TAG, "Broken EIT.");
+ return false;
+ }
+ String titleText = "";
+ if (titleLength > 0) {
+ titleText = extractText(data, pos + 10);
+ }
+ if ((data[pos + 10 + titleLength] & 0xf0) != 0xf0) {
+ Log.e(TAG, "Broken EIT.");
+ return false;
+ }
+ int descriptorsLength =
+ ((data[pos + 10 + titleLength] & 0x0f) << 8)
+ | (data[pos + 10 + titleLength + 1] & 0xff);
+ int descriptorsPos = pos + 10 + titleLength + 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("EIT descriptors size: %d", descriptors.size()));
+ }
+ String contentRating = generateContentRating(descriptors);
+ String broadcastGenre = generateBroadcastGenre(descriptors);
+ String canonicalGenre = generateCanonicalGenre(descriptors);
+ List<AtscAudioTrack> audioTracks = generateAudioTracks(descriptors);
+ List<AtscCaptionTrack> captionTracks = generateCaptionTracks(descriptors);
+ pos += 10 + titleLength + 2 + descriptorsLength;
+ results.add(
+ new EitItem(
+ EitItem.INVALID_PROGRAM_ID,
+ eventId,
+ titleText,
+ startTime,
+ lengthInSecond,
+ contentRating,
+ audioTracks,
+ captionTracks,
+ broadcastGenre,
+ canonicalGenre,
+ null));
+ }
+ if (mListener != null) {
+ mListener.onEitParsed(sourceId, results);
+ }
+ return true;
+ }
+
+ private boolean parseETT(byte[] data) {
+ // For details of the structure for ETT, see ATSC A/65 Table 6.13.
+ if (DEBUG) {
+ Log.d(TAG, "ETT is discovered.");
+ }
+ if (data.length <= 12) {
+ Log.e(TAG, "Broken ETT.");
+ return false;
+ }
+ int sourceId = ((data[9] & 0xff) << 8) | (data[10] & 0xff);
+ int eventId = (((data[11] & 0xff) << 8) | (data[12] & 0xff)) >> 2;
+ String text = extractText(data, 13);
+ List<EttItem> ettItems = mParsedEttItems.get(sourceId);
+ if (ettItems == null) {
+ ettItems = new ArrayList<>();
+ mParsedEttItems.put(sourceId, ettItems);
+ }
+ ettItems.add(new EttItem(eventId, text));
+ 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.
+ List<AtscAudioTrack> ac3Tracks = new ArrayList<>();
+ List<AtscAudioTrack> iso639LanguageTracks = new ArrayList<>();
+ for (TsDescriptor descriptor : descriptors) {
+ if (descriptor instanceof Ac3AudioDescriptor) {
+ Ac3AudioDescriptor audioDescriptor = (Ac3AudioDescriptor) descriptor;
+ AtscAudioTrack audioTrack = new AtscAudioTrack();
+ 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();
+ ac3Tracks.add(audioTrack);
+ }
+ }
+ for (TsDescriptor descriptor : descriptors) {
+ if (descriptor instanceof Iso639LanguageDescriptor) {
+ Iso639LanguageDescriptor iso639LanguageDescriptor =
+ (Iso639LanguageDescriptor) descriptor;
+ iso639LanguageTracks.addAll(iso639LanguageDescriptor.getAudioTracks());
+ }
+ }
+
+ // An AC3 audio stream descriptor only has a audio channel count and a audio sample rate
+ // while a ISO 639 Language descriptor only has a audio type, which describes a main use
+ // case of its audio track.
+ // Some channels contain only AC3 audio stream descriptors with valid language values.
+ // Other channels contain both an AC3 audio stream descriptor and a ISO 639 Language
+ // descriptor per audio track, and those AC3 audio stream descriptors often have a null
+ // value of language field.
+ // Combines two descriptors into one in order to gather more audio track specific
+ // information as much as possible.
+ List<AtscAudioTrack> tracks = new ArrayList<>();
+ if (!ac3Tracks.isEmpty()
+ && !iso639LanguageTracks.isEmpty()
+ && ac3Tracks.size() != iso639LanguageTracks.size()) {
+ // This shouldn't be happen. In here, it handles two cases. The first case is that the
+ // only one type of descriptors arrives. The second case is that the two types of
+ // descriptors have the same number of tracks.
+ Log.e(TAG, "AC3 audio stream descriptors size != ISO 639 Language descriptors size");
+ return tracks;
+ }
+ int size = Math.max(ac3Tracks.size(), iso639LanguageTracks.size());
+ for (int i = 0; i < size; ++i) {
+ AtscAudioTrack audioTrack = null;
+ if (i < ac3Tracks.size()) {
+ audioTrack = ac3Tracks.get(i);
+ }
+ if (i < iso639LanguageTracks.size()) {
+ if (audioTrack == null) {
+ audioTrack = iso639LanguageTracks.get(i);
+ } else {
+ AtscAudioTrack iso639LanguageTrack = iso639LanguageTracks.get(i);
+ if (audioTrack.language == null || TextUtils.equals(audioTrack.language, "")) {
+ audioTrack.language = iso639LanguageTrack.language;
+ }
+ audioTrack.audioType = iso639LanguageTrack.audioType;
+ }
+ }
+ String language = ISO_LANGUAGE_CODE_MAP.get(audioTrack.language);
+ if (language != null) {
+ audioTrack.language = language;
+ }
+ tracks.add(audioTrack);
+ }
+ return tracks;
+ }
+
+ private static List<AtscCaptionTrack> generateCaptionTracks(List<TsDescriptor> descriptors) {
+ List<AtscCaptionTrack> services = new ArrayList<>();
+ for (TsDescriptor descriptor : descriptors) {
+ if (descriptor instanceof CaptionServiceDescriptor) {
+ CaptionServiceDescriptor captionServiceDescriptor =
+ (CaptionServiceDescriptor) descriptor;
+ services.addAll(captionServiceDescriptor.getCaptionTracks());
+ }
+ }
+ return services;
+ }
+
+ @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)) {
+ 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;
+ }
+ }
+ 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;
+ }
+ }
+ 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) {
+ for (TsDescriptor descriptor : descriptors) {
+ if (descriptor instanceof GenreDescriptor) {
+ GenreDescriptor genreDescriptor = (GenreDescriptor) descriptor;
+ return TextUtils.join(",", genreDescriptor.getBroadcastGenres());
+ }
+ }
+ return null;
+ }
+
+ private static String generateCanonicalGenre(List<TsDescriptor> descriptors) {
+ for (TsDescriptor descriptor : descriptors) {
+ if (descriptor instanceof GenreDescriptor) {
+ GenreDescriptor genreDescriptor = (GenreDescriptor) descriptor;
+ return Genres.encode(genreDescriptor.getCanonicalGenres());
+ }
+ }
+ 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<>();
+ if (data.length < limit) {
+ return descriptors;
+ }
+ int pos = offset;
+ while (pos + 1 < limit) {
+ int tag = data[pos] & 0xff;
+ int length = data[pos + 1] & 0xff;
+ if (length <= 0) {
+ break;
+ }
+ if (limit < pos + length + 2) {
+ break;
+ }
+ if (DEBUG) {
+ Log.d(TAG, String.format("Descriptor tag: %02x", tag));
+ }
+ TsDescriptor descriptor = null;
+ switch (tag) {
+ case DESCRIPTOR_TAG_CONTENT_ADVISORY:
+ descriptor = parseContentAdvisory(data, pos, pos + length + 2);
+ break;
+
+ case DESCRIPTOR_TAG_CAPTION_SERVICE:
+ descriptor = parseCaptionService(data, pos, pos + length + 2);
+ break;
+
+ case DESCRIPTOR_TAG_EXTENDED_CHANNEL_NAME:
+ descriptor = parseLongChannelName(data, pos, pos + length + 2);
+ break;
+
+ case DESCRIPTOR_TAG_GENRE:
+ descriptor = parseGenre(data, pos, pos + length + 2);
+ break;
+
+ case DESCRIPTOR_TAG_AC3_AUDIO_STREAM:
+ descriptor = parseAc3AudioStream(data, pos, pos + length + 2);
+ break;
+
+ case DESCRIPTOR_TAG_ISO639LANGUAGE:
+ 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) {
+ if (DEBUG) {
+ Log.d(TAG, "Descriptor parsed: " + descriptor);
+ }
+ descriptors.add(descriptor);
+ }
+ pos += length + 2;
+ }
+ return descriptors;
+ }
+
+ private static Iso639LanguageDescriptor parseIso639Language(byte[] data, int pos, int limit) {
+ // For the details of the structure of ISO 639 language descriptor,
+ // see ISO13818-1 second edition Section 2.6.18.
+ pos += 2;
+ List<AtscAudioTrack> audioTracks = new ArrayList<>();
+ while (pos + 4 <= limit) {
+ if (limit <= pos + 3) {
+ Log.e(TAG, "Broken Iso639Language.");
+ return null;
+ }
+ String language = new String(data, pos, 3);
+ int audioType = data[pos + 3] & 0xff;
+ AtscAudioTrack audioTrack = new AtscAudioTrack();
+ audioTrack.language = language;
+ audioTrack.audioType = audioType;
+ audioTracks.add(audioTrack);
+ pos += 4;
+ }
+ return new Iso639LanguageDescriptor(audioTracks);
+ }
+
+ private static CaptionServiceDescriptor parseCaptionService(byte[] data, int pos, int limit) {
+ // For the details of the structure of caption service descriptor,
+ // see ATSC A/65 Section 6.9.2.
+ if (limit <= pos + 2) {
+ Log.e(TAG, "Broken CaptionServiceDescriptor.");
+ return null;
+ }
+ List<AtscCaptionTrack> services = new ArrayList<>();
+ pos += 2;
+ int numberServices = data[pos] & 0x1f;
+ ++pos;
+ if (limit < pos + numberServices * 6) {
+ Log.e(TAG, "Broken CaptionServiceDescriptor.");
+ return null;
+ }
+ for (int i = 0; i < numberServices; ++i) {
+ String language = new String(Arrays.copyOfRange(data, pos, pos + 3));
+ pos += 3;
+ boolean ccType = (data[pos] & 0x80) != 0;
+ if (!ccType) {
+ pos += 3;
+ continue;
+ }
+ int captionServiceNumber = data[pos] & 0x3f;
+ ++pos;
+ boolean easyReader = (data[pos] & 0x80) != 0;
+ boolean wideAspectRatio = (data[pos] & 0x40) != 0;
+ byte[] reserved = new byte[2];
+ reserved[0] = (byte) (data[pos] << 2);
+ reserved[0] |= (byte) ((data[pos + 1] & 0xc0) >>> 6);
+ reserved[1] = (byte) ((data[pos + 1] & 0x3f) << 2);
+ pos += 2;
+ AtscCaptionTrack captionTrack = new AtscCaptionTrack();
+ captionTrack.language = language;
+ captionTrack.serviceNumber = captionServiceNumber;
+ captionTrack.easyReader = easyReader;
+ captionTrack.wideAspectRatio = wideAspectRatio;
+ services.add(captionTrack);
+ }
+ return new CaptionServiceDescriptor(services);
+ }
+
+ private static ContentAdvisoryDescriptor parseContentAdvisory(byte[] data, int pos, int limit) {
+ // For details of the structure for content advisory descriptor, see A/65 Table 6.27.
+ if (limit <= pos + 2) {
+ Log.e(TAG, "Broken ContentAdvisory");
+ return null;
+ }
+ int count = data[pos + 2] & 0x3f;
+ pos += 3;
+ List<RatingRegion> ratingRegions = new ArrayList<>();
+ for (int i = 0; i < count; ++i) {
+ if (limit <= pos + 1) {
+ Log.e(TAG, "Broken ContentAdvisory");
+ return null;
+ }
+ List<RegionalRating> indices = new ArrayList<>();
+ 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");
+ return null;
+ }
+ 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));
+ }
+ if (limit <= pos) {
+ Log.e(TAG, "Broken ContentAdvisory");
+ return null;
+ }
+ int ratingDescriptionLength = data[pos] & 0xff;
+ ++pos;
+ if (limit < pos + ratingDescriptionLength) {
+ Log.e(TAG, "Broken ContentAdvisory");
+ return null;
+ }
+ String ratingDescription = extractText(data, pos);
+ pos += ratingDescriptionLength;
+ ratingRegions.add(new RatingRegion(ratingRegion, ratingDescription, indices));
+ }
+ return new ContentAdvisoryDescriptor(ratingRegions);
+ }
+
+ private static ExtendedChannelNameDescriptor parseLongChannelName(
+ byte[] data, int pos, int limit) {
+ if (limit <= pos + 2) {
+ Log.e(TAG, "Broken ExtendedChannelName.");
+ return null;
+ }
+ pos += 2;
+ String text = extractText(data, pos);
+ if (text == null) {
+ Log.e(TAG, "Broken ExtendedChannelName.");
+ return null;
+ }
+ return new ExtendedChannelNameDescriptor(text);
+ }
+
+ private static GenreDescriptor parseGenre(byte[] data, int pos, int limit) {
+ pos += 2;
+ int attributeCount = data[pos] & 0x1f;
+ if (limit <= pos + attributeCount) {
+ Log.e(TAG, "Broken Genre.");
+ return null;
+ }
+ HashSet<String> broadcastGenreSet = new HashSet<>();
+ HashSet<String> canonicalGenreSet = new HashSet<>();
+ for (int i = 0; i < attributeCount; ++i) {
+ ++pos;
+ int genreCode = data[pos] & 0xff;
+ if (genreCode < BROADCAST_GENRES_TABLE.length) {
+ String broadcastGenre = BROADCAST_GENRES_TABLE[genreCode];
+ if (broadcastGenre != null && !broadcastGenreSet.contains(broadcastGenre)) {
+ broadcastGenreSet.add(broadcastGenre);
+ }
+ }
+ if (genreCode < CANONICAL_GENRES_TABLE.length) {
+ String canonicalGenre = CANONICAL_GENRES_TABLE[genreCode];
+ if (canonicalGenre != null && !canonicalGenreSet.contains(canonicalGenre)) {
+ canonicalGenreSet.add(canonicalGenre);
+ }
+ }
+ }
+ return new GenreDescriptor(
+ broadcastGenreSet.toArray(new String[broadcastGenreSet.size()]),
+ canonicalGenreSet.toArray(new String[canonicalGenreSet.size()]));
+ }
+
+ private static TsDescriptor parseAc3AudioStream(byte[] data, int pos, int limit) {
+ // For details of the AC3 audio stream descriptor, see A/52 Table A4.1.
+ if (limit <= pos + 5) {
+ Log.e(TAG, "Broken AC3 audio stream descriptor.");
+ return null;
+ }
+ pos += 2;
+ byte sampleRateCode = (byte) ((data[pos] & 0xe0) >> 5);
+ byte bsid = (byte) (data[pos] & 0x1f);
+ ++pos;
+ byte bitRateCode = (byte) ((data[pos] & 0xfc) >> 2);
+ byte surroundMode = (byte) (data[pos] & 0x03);
+ ++pos;
+ byte bsmod = (byte) ((data[pos] & 0xe0) >> 5);
+ int numChannels = (data[pos] & 0x1e) >> 1;
+ boolean fullSvc = (data[pos] & 0x01) != 0;
+ ++pos;
+ byte langCod = data[pos];
+ byte langCod2 = 0;
+ if (numChannels == 0) {
+ if (limit <= pos) {
+ Log.e(TAG, "Broken AC3 audio stream descriptor.");
+ return null;
+ }
+ ++pos;
+ langCod2 = data[pos];
+ }
+ if (limit <= pos + 1) {
+ Log.e(TAG, "Broken AC3 audio stream descriptor.");
+ return null;
+ }
+ byte mainId = 0;
+ byte priority = 0;
+ byte asvcflags = 0;
+ ++pos;
+ if (bsmod < 2) {
+ mainId = (byte) ((data[pos] & 0xe0) >> 5);
+ priority = (byte) ((data[pos] & 0x18) >> 3);
+ if ((data[pos] & 0x07) != 0x07) {
+ Log.e(TAG, "Broken AC3 audio stream descriptor reserved failed");
+ return null;
+ }
+ } else {
+ asvcflags = data[pos];
+ }
+
+ // See A/52B Table A3.6 num_channels.
+ int numEncodedChannels;
+ switch (numChannels) {
+ case 1:
+ case 8:
+ numEncodedChannels = 1;
+ break;
+ case 2:
+ case 9:
+ numEncodedChannels = 2;
+ break;
+ case 3:
+ case 4:
+ case 10:
+ numEncodedChannels = 3;
+ break;
+ case 5:
+ case 6:
+ case 11:
+ numEncodedChannels = 4;
+ break;
+ case 7:
+ case 12:
+ numEncodedChannels = 5;
+ break;
+ case 13:
+ numEncodedChannels = 6;
+ break;
+ default:
+ numEncodedChannels = 0;
+ break;
+ }
+
+ if (limit <= pos + 1) {
+ Log.w(TAG, "Missing text and language fields on AC3 audio stream descriptor.");
+ return new Ac3AudioDescriptor(
+ sampleRateCode,
+ bsid,
+ bitRateCode,
+ surroundMode,
+ bsmod,
+ numEncodedChannels,
+ fullSvc,
+ langCod,
+ langCod2,
+ mainId,
+ priority,
+ asvcflags,
+ null,
+ null,
+ null);
+ }
+ ++pos;
+ int textLen = (data[pos] & 0xfe) >> 1;
+ boolean textCode = (data[pos] & 0x01) != 0;
+ ++pos;
+ String text = "";
+ if (textLen > 0) {
+ if (limit < pos + textLen) {
+ Log.e(TAG, "Broken AC3 audio stream descriptor");
+ return null;
+ }
+ if (textCode) {
+ text = new String(data, pos, textLen);
+ } else {
+ text = new String(data, pos, textLen, Charset.forName("UTF-16"));
+ }
+ pos += textLen;
+ }
+ String language = null;
+ String language2 = null;
+ if (pos < limit) {
+ // Many AC3 audio stream descriptors skip the language fields.
+ boolean languageFlag1 = (data[pos] & 0x80) != 0;
+ boolean languageFlag2 = (data[pos] & 0x40) != 0;
+ if ((data[pos] & 0x3f) != 0x3f) {
+ Log.e(TAG, "Broken AC3 audio stream descriptor");
+ return null;
+ }
+ if (pos + (languageFlag1 ? 3 : 0) + (languageFlag2 ? 3 : 0) > limit) {
+ Log.e(TAG, "Broken AC3 audio stream descriptor");
+ return null;
+ }
+ ++pos;
+ if (languageFlag1) {
+ language = new String(data, pos, 3);
+ pos += 3;
+ }
+ if (languageFlag2) {
+ language2 = new String(data, pos, 3);
+ }
+ }
+
+ return new Ac3AudioDescriptor(
+ sampleRateCode,
+ bsid,
+ bitRateCode,
+ surroundMode,
+ bsmod,
+ numEncodedChannels,
+ fullSvc,
+ langCod,
+ langCod2,
+ mainId,
+ priority,
+ asvcflags,
+ text,
+ 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) {
+ return i;
+ }
+ }
+ return MAX_SHORT_NAME_BYTES;
+ }
+
+ private static String extractText(byte[] data, int pos) {
+ if (data.length < pos) {
+ return null;
+ }
+ int numStrings = data[pos] & 0xff;
+ pos++;
+ for (int i = 0; i < numStrings; ++i) {
+ if (data.length <= pos + 3) {
+ Log.e(TAG, "Broken text.");
+ return null;
+ }
+ int numSegments = data[pos + 3] & 0xff;
+ pos += 4;
+ for (int j = 0; j < numSegments; ++j) {
+ if (data.length <= pos + 2) {
+ Log.e(TAG, "Broken text.");
+ return null;
+ }
+ int compressionType = data[pos] & 0xff;
+ int mode = data[pos + 1] & 0xff;
+ int numBytes = data[pos + 2] & 0xff;
+ if (data.length < pos + 3 + numBytes) {
+ Log.e(TAG, "Broken text.");
+ return null;
+ }
+ byte[] bytes = Arrays.copyOfRange(data, pos + 3, pos + 3 + numBytes);
+ if (compressionType == COMPRESSION_TYPE_NO_COMPRESSION) {
+ try {
+ switch (mode) {
+ case MODE_SELECTED_UNICODE_RANGE_1:
+ return new String(bytes, "ISO-8859-1");
+ case MODE_SCSU:
+ return UnicodeDecompressor.decompress(bytes);
+ case MODE_UTF16:
+ return new String(bytes, "UTF-16");
+ }
+ } catch (UnsupportedEncodingException e) {
+ Log.e(TAG, "Unsupported text format.", e);
+ }
+ }
+ pos += 3 + numBytes;
+ }
+ }
+ 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;
+ }
+ boolean hasCRC = (data[1] & 0x80) != 0; // section_syntax_indicator
+ if (hasCRC) {
+ int crc = 0xffffffff;
+ for (byte b : data) {
+ int index = ((crc >> 24) ^ (b & 0xff)) & 0xff;
+ crc = CRC_TABLE[index] ^ (crc << 8);
+ }
+ if (crc != 0) {
+ return false;
+ }
+ }
+ return true;
+ }
+}
diff --git a/src/com/android/tv/tuner/ts/TsParser.java b/src/com/android/tv/tuner/ts/TsParser.java
new file mode 100644
index 00000000..fbedc2c3
--- /dev/null
+++ b/src/com/android/tv/tuner/ts/TsParser.java
@@ -0,0 +1,543 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.ts;
+
+import android.util.Log;
+import android.util.SparseArray;
+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.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;
+import com.android.tv.tuner.util.ByteArrayBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeSet;
+
+/** Parses MPEG-2 TS packets. */
+public class TsParser {
+ private static final String TAG = "TsParser";
+ private static final boolean DEBUG = false;
+
+ 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;
+
+ /*
+ * Using a SparseArray removes the need to auto box the int key for mStreamMap
+ * in feedTdPacket which is called 100 times a second. This greatly reduces the
+ * number of objects created and the frequency of garbage collection.
+ * Other maps might be suitable for a SparseArray, but the performance
+ * trade offs must be considered carefully.
+ * mStreamMap is the only one called at such a high rate.
+ */
+ private final SparseArray<Stream> mStreamMap = new SparseArray<>();
+ private final Map<Integer, VctItem> mSourceIdToVctItemMap = new HashMap<>();
+ private final Map<Integer, String> mSourceIdToVctItemDescriptionMap = new HashMap<>();
+ 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<>();
+ private final TreeSet<Integer> mETTPids = new TreeSet<>();
+ 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;
+ private int mVctSectionParsedCount;
+ private boolean[] mVctSectionParsed;
+
+ public interface TsOutputListener {
+ void onPatDetected(List<PatItem> items);
+
+ void onEitPidDetected(int pid);
+
+ void onVctItemParsed(VctItem channel, List<PmtItem> pmtItems);
+
+ void onEitItemParsed(VctItem channel, List<EitItem> items);
+
+ void onEttPidDetected(int pid);
+
+ void onAllVctItemsParsed();
+
+ void onSdtItemParsed(SdtItem channel, List<PmtItem> pmtItems);
+ }
+
+ private abstract class Stream {
+ private static final int INVALID_CONTINUITY_COUNTER = -1;
+ private static final int NUM_CONTINUITY_COUNTER = 16;
+
+ protected int mContinuityCounter = INVALID_CONTINUITY_COUNTER;
+ protected final ByteArrayBuffer mPacket = new ByteArrayBuffer(TS_PACKET_SIZE);
+
+ public void feedData(byte[] data, int continuityCounter, boolean startIndicator) {
+ if ((mContinuityCounter + 1) % NUM_CONTINUITY_COUNTER != continuityCounter) {
+ mPacket.setLength(0);
+ }
+ mContinuityCounter = continuityCounter;
+ handleData(data, startIndicator);
+ }
+
+ protected abstract void handleData(byte[] data, boolean startIndicator);
+
+ protected abstract void resetDataVersions();
+ }
+
+ private class SectionStream extends Stream {
+ private final SectionParser mSectionParser;
+ private final int mPid;
+
+ public SectionStream(int pid) {
+ mPid = pid;
+ mSectionParser = new SectionParser(mSectionListener);
+ }
+
+ @Override
+ protected void handleData(byte[] data, boolean startIndicator) {
+ int startPos = 0;
+ if (mPacket.length() == 0) {
+ if (startIndicator) {
+ startPos = (data[0] & 0xff) + 1;
+ } else {
+ // Don't know where the section starts yet. Wait until start indicator is on.
+ return;
+ }
+ } else {
+ if (startIndicator) {
+ startPos = 1;
+ }
+ }
+
+ // When a broken packet is encountered, parsing will stop and return right away.
+ if (startPos >= data.length) {
+ mPacket.setLength(0);
+ return;
+ }
+ mPacket.append(data, startPos, data.length - startPos);
+ mSectionParser.parseSections(mPacket);
+ }
+
+ @Override
+ protected void resetDataVersions() {
+ mSectionParser.resetVersionNumbers();
+ }
+
+ private final OutputListener mSectionListener =
+ new OutputListener() {
+ @Override
+ public void onPatParsed(List<PatItem> items) {
+ for (PatItem i : items) {
+ startListening(i.getPmtPid());
+ }
+ if (mListener != null) {
+ mListener.onPatDetected(items);
+ }
+ }
+
+ @Override
+ public void onPmtParsed(int programNumber, List<PmtItem> items) {
+ mProgramNumberToPMTMap.put(programNumber, items);
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "onPMTParsed, programNo "
+ + programNumber
+ + " handledStatus is "
+ + mProgramNumberHandledStatus.get(
+ programNumber, false));
+ }
+ int statusIndex = mProgramNumberHandledStatus.indexOfKey(programNumber);
+ if (statusIndex < 0) {
+ mProgramNumberHandledStatus.put(programNumber, false);
+ }
+ if (!mProgramNumberHandledStatus.get(programNumber)) {
+ VctItem vctItem = mProgramNumberToVctItemMap.get(programNumber);
+ if (vctItem != null) {
+ // When PMT is parsed later than VCT.
+ mProgramNumberHandledStatus.put(programNumber, true);
+ handleVctItem(vctItem, items);
+ mHandledVctItemCount++;
+ if (mHandledVctItemCount >= mVctItemCount
+ && mVctSectionParsedCount >= mVctSectionParsed.length
+ && mListener != null) {
+ mListener.onAllVctItemsParsed();
+ }
+ }
+ SdtItem sdtItem = mProgramNumberToSdtItemMap.get(programNumber);
+ if (sdtItem != null) {
+ // When PMT is parsed later than SDT.
+ mProgramNumberHandledStatus.put(programNumber, true);
+ handleSdtItem(sdtItem, items);
+ }
+ }
+ }
+
+ @Override
+ public void onMgtParsed(List<MgtItem> items) {
+ for (MgtItem i : items) {
+ if (mStreamMap.get(i.getTableTypePid()) != null) {
+ continue;
+ }
+ if (i.getTableType() >= MgtItem.TABLE_TYPE_EIT_RANGE_START
+ && i.getTableType() <= MgtItem.TABLE_TYPE_EIT_RANGE_END) {
+ startListening(i.getTableTypePid());
+ mEITPids.add(i.getTableTypePid());
+ if (mListener != null) {
+ mListener.onEitPidDetected(i.getTableTypePid());
+ }
+ } else if (i.getTableType() == MgtItem.TABLE_TYPE_CHANNEL_ETT
+ || (i.getTableType() >= MgtItem.TABLE_TYPE_ETT_RANGE_START
+ && i.getTableType()
+ <= MgtItem.TABLE_TYPE_ETT_RANGE_END)) {
+ startListening(i.getTableTypePid());
+ mETTPids.add(i.getTableTypePid());
+ if (mListener != null) {
+ mListener.onEttPidDetected(i.getTableTypePid());
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onVctParsed(
+ List<VctItem> items, int sectionNumber, int lastSectionNumber) {
+ if (mVctSectionParsed == null) {
+ mVctSectionParsed = new boolean[lastSectionNumber + 1];
+ } else if (mVctSectionParsed[sectionNumber]) {
+ // The current section was handled before.
+ if (DEBUG) {
+ Log.d(TAG, "Duplicate VCT section found.");
+ }
+ return;
+ }
+ mVctSectionParsed[sectionNumber] = true;
+ mVctSectionParsedCount++;
+ mVctItemCount += items.size();
+ for (VctItem i : items) {
+ if (DEBUG) Log.d(TAG, "onVCTParsed " + i);
+ if (i.getSourceId() != 0) {
+ mSourceIdToVctItemMap.put(i.getSourceId(), i);
+ i.setDescription(
+ mSourceIdToVctItemDescriptionMap.get(i.getSourceId()));
+ }
+ int programNumber = i.getProgramNumber();
+ mProgramNumberToVctItemMap.put(programNumber, i);
+ List<PmtItem> pmtList = mProgramNumberToPMTMap.get(programNumber);
+ if (pmtList != null) {
+ mProgramNumberHandledStatus.put(programNumber, true);
+ handleVctItem(i, pmtList);
+ mHandledVctItemCount++;
+ if (mHandledVctItemCount >= mVctItemCount
+ && mVctSectionParsedCount >= mVctSectionParsed.length
+ && mListener != null) {
+ mListener.onAllVctItemsParsed();
+ }
+ } else {
+ mProgramNumberHandledStatus.put(programNumber, false);
+ Log.i(
+ TAG,
+ "onVCTParsed, but PMT for programNo "
+ + programNumber
+ + " is not found yet.");
+ }
+ }
+ }
+
+ @Override
+ public void onEitParsed(int sourceId, List<EitItem> items) {
+ if (DEBUG) Log.d(TAG, "onEITParsed " + sourceId);
+ EventSourceEntry entry = new EventSourceEntry(mPid, sourceId);
+ mEitMap.put(entry, items);
+ handleEvents(sourceId);
+ }
+
+ @Override
+ public void onEttParsed(int sourceId, List<EttItem> descriptions) {
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ String.format(
+ "onETTParsed sourceId: %d, descriptions.size(): %d",
+ sourceId, descriptions.size()));
+ }
+ for (EttItem item : descriptions) {
+ if (item.eventId == 0) {
+ // Channel description
+ mSourceIdToVctItemDescriptionMap.put(sourceId, item.text);
+ VctItem vctItem = mSourceIdToVctItemMap.get(sourceId);
+ if (vctItem != null) {
+ vctItem.setDescription(item.text);
+ List<PmtItem> pmtItems =
+ mProgramNumberToPMTMap.get(vctItem.getProgramNumber());
+ if (pmtItems != null) {
+ handleVctItem(vctItem, pmtItems);
+ }
+ }
+ }
+ }
+
+ // Event Information description
+ EventSourceEntry entry = new EventSourceEntry(mPid, sourceId);
+ 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.");
+ }
+ }
+ }
+ };
+ }
+
+ private static class EventSourceEntry {
+ public final int pid;
+ public final int sourceId;
+
+ public EventSourceEntry(int pid, int sourceId) {
+ this.pid = pid;
+ this.sourceId = sourceId;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 17;
+ result = 31 * result + pid;
+ result = 31 * result + sourceId;
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof EventSourceEntry) {
+ EventSourceEntry another = (EventSourceEntry) obj;
+ return pid == another.pid && sourceId == another.sourceId;
+ }
+ return false;
+ }
+ }
+
+ private void handleVctItem(VctItem channel, List<PmtItem> pmtItems) {
+ if (DEBUG) {
+ Log.d(TAG, "handleVctItem " + channel);
+ }
+ if (mListener != null) {
+ mListener.onVctItemParsed(channel, pmtItems);
+ }
+ int sourceId = channel.getSourceId();
+ int statusIndex = mVctItemHandledStatus.indexOfKey(sourceId);
+ if (statusIndex < 0) {
+ mVctItemHandledStatus.put(sourceId, false);
+ return;
+ }
+ if (!mVctItemHandledStatus.valueAt(statusIndex)) {
+ List<EitItem> eitItems = mSourceIdToEitMap.get(sourceId);
+ if (eitItems != null) {
+ // When VCT is parsed later than EIT.
+ mVctItemHandledStatus.put(sourceId, true);
+ handleEitItems(channel, eitItems);
+ }
+ }
+ }
+
+ private void handleEitItems(VctItem channel, List<EitItem> items) {
+ if (mListener != null) {
+ mListener.onEitItemParsed(channel, items);
+ }
+ }
+
+ 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) {
+ List<EitItem> eitItems = mEitMap.get(new EventSourceEntry(pid, sourceId));
+ if (eitItems != null) {
+ for (EitItem item : eitItems) {
+ item.setDescription(null);
+ itemSet.put(item.getEventId(), item);
+ }
+ }
+ }
+ for (int pid : mETTPids) {
+ List<EttItem> ettItems = mETTMap.get(new EventSourceEntry(pid, sourceId));
+ if (ettItems != null) {
+ for (EttItem ettItem : ettItems) {
+ if (ettItem.eventId != 0) {
+ EitItem item = itemSet.get(ettItem.eventId);
+ if (item != null) {
+ item.setDescription(ettItem.text);
+ }
+ }
+ }
+ }
+ }
+ List<EitItem> items = new ArrayList<>(itemSet.values());
+ mSourceIdToEitMap.put(sourceId, items);
+ VctItem channel = mSourceIdToVctItemMap.get(sourceId);
+ if (channel != null && mProgramNumberHandledStatus.get(channel.getProgramNumber())) {
+ mVctItemHandledStatus.put(sourceId, true);
+ handleEitItems(channel, items);
+ } else {
+ mVctItemHandledStatus.put(sourceId, false);
+ 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, boolean isDvbSignal) {
+ startListening(PAT_PID);
+ startListening(ATSC_SI_BASE_PID);
+ mIsDvbSignal = isDvbSignal;
+ if (isDvbSignal) {
+ startListening(DVB_EIT_PID);
+ startListening(DVB_SDT_PID);
+ }
+ mListener = listener;
+ }
+
+ private void startListening(int pid) {
+ mStreamMap.put(pid, new SectionStream(pid));
+ }
+
+ private boolean feedTSPacket(byte[] tsData, int pos) {
+ if (tsData.length < pos + TS_PACKET_SIZE) {
+ if (DEBUG) Log.d(TAG, "Data should include a single TS packet.");
+ return false;
+ }
+ if (tsData[pos] != TS_PACKET_START_CODE) {
+ if (DEBUG) Log.d(TAG, "Invalid ts packet.");
+ return false;
+ }
+ if ((tsData[pos + 1] & TS_PACKET_TEI_MASK) != 0) {
+ if (DEBUG) Log.d(TAG, "Erroneous ts packet.");
+ return false;
+ }
+
+ // For details for the structure of TS packet, see H.222.0 Table 2-2.
+ int pid = ((tsData[pos + 1] & 0x1f) << 8) | (tsData[pos + 2] & 0xff);
+ boolean hasAdaptation = (tsData[pos + 3] & 0x20) != 0;
+ boolean hasPayload = (tsData[pos + 3] & 0x10) != 0;
+ boolean payloadStartIndicator = (tsData[pos + 1] & 0x40) != 0;
+ int continuityCounter = tsData[pos + 3] & 0x0f;
+ Stream stream = mStreamMap.get(pid);
+ int payloadPos = pos;
+ payloadPos += hasAdaptation ? 5 + (tsData[pos + 4] & 0xff) : 4;
+ if (!hasPayload || stream == null) {
+ // We are not interested in this packet.
+ return false;
+ }
+ if (payloadPos >= pos + TS_PACKET_SIZE) {
+ if (DEBUG) Log.d(TAG, "Payload should be included in a single TS packet.");
+ return false;
+ }
+ stream.feedData(
+ Arrays.copyOfRange(tsData, payloadPos, pos + TS_PACKET_SIZE),
+ continuityCounter,
+ payloadStartIndicator);
+ return true;
+ }
+
+ /**
+ * Feeds MPEG-2 TS data to parse.
+ *
+ * @param tsData buffer for ATSC TS stream
+ * @param pos the offset where buffer starts
+ * @param length The length of available data
+ */
+ public void feedTSData(byte[] tsData, int pos, int length) {
+ for (; pos <= length - TS_PACKET_SIZE; pos += TS_PACKET_SIZE) {
+ feedTSPacket(tsData, pos);
+ }
+ }
+
+ /**
+ * Retrieves the channel information regardless of being well-formed.
+ *
+ * @return {@link List} of {@link TunerChannel}
+ */
+ public List<TunerChannel> getMalFormedChannels() {
+ List<TunerChannel> incompleteChannels = new ArrayList<>();
+ for (int i = 0; i < mProgramNumberHandledStatus.size(); i++) {
+ if (!mProgramNumberHandledStatus.valueAt(i)) {
+ int programNumber = mProgramNumberHandledStatus.keyAt(i);
+ List<PmtItem> pmtList = mProgramNumberToPMTMap.get(programNumber);
+ if (pmtList != null) {
+ TunerChannel tunerChannel = new TunerChannel(programNumber, pmtList);
+ incompleteChannels.add(tunerChannel);
+ }
+ }
+ }
+ 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
new file mode 100644
index 00000000..49fc0ca1
--- /dev/null
+++ b/src/com/android/tv/tuner/tvinput/ChannelDataManager.java
@@ -0,0 +1,801 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.tvinput;
+
+import android.content.ComponentName;
+import android.content.ContentProviderOperation;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+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;
+import android.os.RemoteException;
+import android.support.annotation.Nullable;
+import android.text.format.DateUtils;
+import android.util.Log;
+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;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentSkipListMap;
+import java.util.concurrent.ConcurrentSkipListSet;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/** Manages the channel info and EPG data through {@link TvInputManager}. */
+public class ChannelDataManager implements Handler.Callback {
+ private static final String TAG = "ChannelDataManager";
+
+ private static final String[] ALL_PROGRAMS_SELECTION_ARGS =
+ new String[] {
+ TvContract.Programs._ID,
+ TvContract.Programs.COLUMN_TITLE,
+ TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS,
+ TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS,
+ TvContract.Programs.COLUMN_CONTENT_RATING,
+ TvContract.Programs.COLUMN_BROADCAST_GENRE,
+ TvContract.Programs.COLUMN_CANONICAL_GENRE,
+ TvContract.Programs.COLUMN_SHORT_DESCRIPTION,
+ TvContract.Programs.COLUMN_VERSION_NUMBER
+ };
+ private static final String[] CHANNEL_DATA_SELECTION_ARGS =
+ new String[] {
+ TvContract.Channels._ID,
+ TvContract.Channels.COLUMN_INTERNAL_PROVIDER_DATA,
+ TvContract.Channels.COLUMN_INTERNAL_PROVIDER_FLAG1
+ };
+
+ private static final int MSG_HANDLE_EVENTS = 1;
+ private static final int MSG_HANDLE_CHANNEL = 2;
+ private static final int MSG_BUILD_CHANNEL_MAP = 3;
+ private static final int MSG_REQUEST_PROGRAMS = 4;
+ private static final int MSG_CLEAR_CHANNELS = 6;
+ private static final int MSG_CHECK_VERSION = 7;
+
+ // Throttle the batch operations to avoid TransactionTooLargeException.
+ private static final int BATCH_OPERATION_COUNT = 100;
+ // At most 16 days of program information is delivered through an EIT,
+ // according to the Chapter 6.4 of ATSC Recommended Practice A/69.
+ private static final long PROGRAM_QUERY_DURATION = TimeUnit.DAYS.toMillis(16);
+
+ /**
+ * A version number to enforce consistency of the channel data.
+ *
+ * <p>WARNING: If a change in the database serialization lead to breaking the backward
+ * compatibility, you must increment this value so that the old data are purged, and the user is
+ * requested to perform the auto-scan again to generate the new data set.
+ */
+ private static final int VERSION = 6;
+
+ private final Context mContext;
+ private final String mInputId;
+ private ProgramInfoListener mListener;
+ private ChannelScanListener mChannelScanListener;
+ private Handler mChannelScanHandler;
+ private final HandlerThread mHandlerThread;
+ private final Handler mHandler;
+ private final ConcurrentHashMap<Long, TunerChannel> mTunerChannelMap;
+ private final ConcurrentSkipListMap<TunerChannel, Long> mTunerChannelIdMap;
+ private final Uri mChannelsUri;
+
+ // Used for scanning
+ private final ConcurrentSkipListSet<TunerChannel> mScannedChannels;
+ private final ConcurrentSkipListSet<TunerChannel> mPreviousScannedChannels;
+ private final AtomicBoolean mIsScanning;
+ private final AtomicBoolean scanCompleted = new AtomicBoolean();
+
+ public interface ProgramInfoListener {
+
+ /**
+ * Invoked when a request for getting programs of a channel has been processed and passes
+ * the requested channel and the programs retrieved from database to the listener.
+ */
+ void onRequestProgramsResponse(TunerChannel channel, List<EitItem> programs);
+
+ /**
+ * Invoked when programs of a channel have been arrived and passes the arrived channel and
+ * programs to the listener.
+ */
+ void onProgramsArrived(TunerChannel channel, List<EitItem> programs);
+
+ /**
+ * Invoked when a channel has been arrived and passes the arrived channel to the listener.
+ */
+ void onChannelArrived(TunerChannel channel);
+
+ /**
+ * Invoked when the database schema has been changed and the old-format channels have been
+ * deleted. A receiver should notify to a user that re-scanning channels is necessary.
+ */
+ void onRescanNeeded();
+ }
+
+ public interface ChannelScanListener {
+ /** Invoked when all pending channels have been handled. */
+ void onChannelHandlingDone();
+ }
+
+ public ChannelDataManager(Context context) {
+ mContext = context;
+ mInputId =
+ TvContract.buildInputId(
+ new ComponentName(
+ mContext.getPackageName(), TunerTvInputService.class.getName()));
+ mChannelsUri = TvContract.buildChannelsUriForInput(mInputId);
+ mTunerChannelMap = new ConcurrentHashMap<>();
+ mTunerChannelIdMap = new ConcurrentSkipListMap<>();
+ mHandlerThread = new HandlerThread("TvInputServiceBackgroundThread");
+ mHandlerThread.start();
+ mHandler = new Handler(mHandlerThread.getLooper(), this);
+ mIsScanning = new AtomicBoolean();
+ mScannedChannels = new ConcurrentSkipListSet<>();
+ mPreviousScannedChannels = new ConcurrentSkipListSet<>();
+ }
+
+ // Public methods
+ public void checkDataVersion(Context context) {
+ int version = TunerPreferences.getChannelDataVersion(context);
+ Log.d(TAG, "ChannelDataManager.VERSION=" + VERSION + " (current=" + version + ")");
+ if (version == VERSION) {
+ // Everything is awesome. Return and continue.
+ return;
+ }
+ setCurrentVersion(context);
+
+ if (version == TunerPreferences.CHANNEL_DATA_VERSION_NOT_SET) {
+ mHandler.sendEmptyMessage(MSG_CHECK_VERSION);
+ } else {
+ // The stored channel data seem outdated. Delete them all.
+ mHandler.sendEmptyMessage(MSG_CLEAR_CHANNELS);
+ }
+ }
+
+ public void setCurrentVersion(Context context) {
+ TunerPreferences.setChannelDataVersion(context, VERSION);
+ }
+
+ public void setListener(ProgramInfoListener listener) {
+ mListener = listener;
+ }
+
+ public void setChannelScanListener(ChannelScanListener listener, Handler handler) {
+ mChannelScanListener = listener;
+ mChannelScanHandler = handler;
+ }
+
+ public void release() {
+ mHandler.removeCallbacksAndMessages(null);
+ releaseSafely();
+ }
+
+ public void releaseSafely() {
+ mHandlerThread.quitSafely();
+ mListener = null;
+ mChannelScanListener = null;
+ mChannelScanHandler = null;
+ }
+
+ public TunerChannel getChannel(long channelId) {
+ TunerChannel channel = mTunerChannelMap.get(channelId);
+ if (channel != null) {
+ return channel;
+ }
+ mHandler.sendEmptyMessage(MSG_BUILD_CHANNEL_MAP);
+ byte[] data = null;
+ try (Cursor cursor =
+ mContext.getContentResolver()
+ .query(
+ TvContract.buildChannelUri(channelId),
+ CHANNEL_DATA_SELECTION_ARGS,
+ null,
+ null,
+ null)) {
+ if (cursor != null && cursor.moveToFirst()) {
+ data = cursor.getBlob(1);
+ }
+ }
+ if (data == null) {
+ return null;
+ }
+ channel = TunerChannel.parseFrom(data);
+ if (channel == null) {
+ return null;
+ }
+ channel.setChannelId(channelId);
+ return channel;
+ }
+
+ public void requestProgramsData(TunerChannel channel) {
+ mHandler.removeMessages(MSG_REQUEST_PROGRAMS);
+ mHandler.obtainMessage(MSG_REQUEST_PROGRAMS, channel).sendToTarget();
+ }
+
+ public void notifyEventDetected(TunerChannel channel, List<EitItem> items) {
+ mHandler.obtainMessage(MSG_HANDLE_EVENTS, new ChannelEvent(channel, items)).sendToTarget();
+ }
+
+ public void notifyChannelDetected(TunerChannel channel, boolean channelArrivedAtFirstTime) {
+ if (mIsScanning.get()) {
+ // During scanning, channels should be handle first to improve scan time.
+ // EIT items can be handled in background after channel scan.
+ mHandler.sendMessageAtFrontOfQueue(mHandler.obtainMessage(MSG_HANDLE_CHANNEL, channel));
+ } else {
+ mHandler.obtainMessage(MSG_HANDLE_CHANNEL, channel).sendToTarget();
+ }
+ }
+
+ // For scanning process
+ /**
+ * Invoked when starting a scanning mode. This method gets the previous channels to detect the
+ * obsolete channels after scanning and initializes the variables used for scanning.
+ */
+ public void notifyScanStarted() {
+ mScannedChannels.clear();
+ mPreviousScannedChannels.clear();
+ try (Cursor cursor =
+ mContext.getContentResolver()
+ .query(mChannelsUri, CHANNEL_DATA_SELECTION_ARGS, null, null, null)) {
+ if (cursor != null && cursor.moveToFirst()) {
+ do {
+ long channelId = cursor.getLong(0);
+ byte[] data = cursor.getBlob(1);
+ TunerChannel channel = TunerChannel.parseFrom(data);
+ if (channel != null) {
+ channel.setChannelId(channelId);
+ mPreviousScannedChannels.add(channel);
+ }
+ } while (cursor.moveToNext());
+ }
+ }
+ mIsScanning.set(true);
+ }
+
+ /**
+ * Invoked when completing the scanning mode. Passes {@code MSG_SCAN_COMPLETED} to the handler
+ * in order to wait for finishing the remaining messages in the handler queue. Then removes the
+ * obsolete channels, which are previously scanned but are not in the current scanned result.
+ */
+ public void notifyScanCompleted() {
+ // Send a dummy message to check whether there is any MSG_HANDLE_CHANNEL in queue
+ // and avoid race conditions.
+ scanCompleted.set(true);
+ mHandler.sendMessageAtFrontOfQueue(mHandler.obtainMessage(MSG_HANDLE_CHANNEL, null));
+ }
+
+ public void scannedChannelHandlingCompleted() {
+ mIsScanning.set(false);
+ if (!mPreviousScannedChannels.isEmpty()) {
+ ArrayList<ContentProviderOperation> ops = new ArrayList<>();
+ for (TunerChannel channel : mPreviousScannedChannels) {
+ ops.add(
+ ContentProviderOperation.newDelete(
+ TvContract.buildChannelUri(channel.getChannelId()))
+ .build());
+ }
+ try {
+ mContext.getContentResolver().applyBatch(TvContract.AUTHORITY, ops);
+ } catch (RemoteException | OperationApplicationException e) {
+ Log.e(TAG, "Error deleting obsolete channels", e);
+ }
+ }
+ if (mChannelScanListener != null && mChannelScanHandler != null) {
+ mChannelScanHandler.post(
+ new Runnable() {
+ @Override
+ public void run() {
+ mChannelScanListener.onChannelHandlingDone();
+ }
+ });
+ } else {
+ Log.e(TAG, "Error. mChannelScanListener is null.");
+ }
+ }
+
+ /** Returns the number of scanned channels in the scanning mode. */
+ public int getScannedChannelCount() {
+ return mScannedChannels.size();
+ }
+
+ /**
+ * Removes all callbacks and messages in handler to avoid previous messages from last channel.
+ */
+ public void removeAllCallbacksAndMessages() {
+ mHandler.removeCallbacksAndMessages(null);
+ }
+
+ @Override
+ public boolean handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_HANDLE_EVENTS:
+ {
+ ChannelEvent event = (ChannelEvent) msg.obj;
+ handleEvents(event.channel, event.eitItems);
+ return true;
+ }
+ case MSG_HANDLE_CHANNEL:
+ {
+ TunerChannel channel = (TunerChannel) msg.obj;
+ if (channel != null) {
+ handleChannel(channel);
+ }
+ if (scanCompleted.get()
+ && mIsScanning.get()
+ && !mHandler.hasMessages(MSG_HANDLE_CHANNEL)) {
+ // Complete the scan when all found channels have already been handled.
+ scannedChannelHandlingCompleted();
+ }
+ return true;
+ }
+ case MSG_BUILD_CHANNEL_MAP:
+ {
+ mHandler.removeMessages(MSG_BUILD_CHANNEL_MAP);
+ buildChannelMap();
+ return true;
+ }
+ case MSG_REQUEST_PROGRAMS:
+ {
+ if (mHandler.hasMessages(MSG_REQUEST_PROGRAMS)) {
+ return true;
+ }
+ TunerChannel channel = (TunerChannel) msg.obj;
+ if (mListener != null) {
+ mListener.onRequestProgramsResponse(
+ channel, getAllProgramsForChannel(channel));
+ }
+ return true;
+ }
+ case MSG_CLEAR_CHANNELS:
+ {
+ clearChannels();
+ return true;
+ }
+ case MSG_CHECK_VERSION:
+ {
+ checkVersion();
+ return true;
+ }
+ }
+ return false;
+ }
+
+ // Private methods
+ private void handleEvents(TunerChannel channel, List<EitItem> items) {
+ long channelId = getChannelId(channel);
+ if (channelId <= 0) {
+ return;
+ }
+ channel.setChannelId(channelId);
+
+ // Schedule the audio and caption tracks of the current program and the programs being
+ // listed after the current one into TIS.
+ if (mListener != null) {
+ mListener.onProgramsArrived(channel, items);
+ }
+
+ long currentTime = System.currentTimeMillis();
+ List<EitItem> oldItems =
+ getAllProgramsForChannel(
+ channel, currentTime, currentTime + PROGRAM_QUERY_DURATION);
+ ArrayList<ContentProviderOperation> ops = new ArrayList<>();
+ // TODO: Find a right way to check if the programs are added outside.
+ boolean addedOutside = false;
+ for (EitItem item : oldItems) {
+ if (item.getEventId() == 0) {
+ // The event has been added outside TV tuner.
+ addedOutside = true;
+ break;
+ }
+ }
+
+ // Inserting programs only when there is no overlapping with existing data assuming that:
+ // 1. external EPG is more accurate and rich and
+ // 2. the data we add here will be updated when we apply external EPG.
+ if (addedOutside) {
+ // oldItemCount cannot be 0 if addedOutside is true.
+ int oldItemCount = oldItems.size();
+ for (EitItem newItem : items) {
+ if (newItem.getEndTimeUtcMillis() < currentTime) {
+ continue;
+ }
+ long newItemStartTime = newItem.getStartTimeUtcMillis();
+ long newItemEndTime = newItem.getEndTimeUtcMillis();
+ if (newItemStartTime < oldItems.get(0).getStartTimeUtcMillis()) {
+ // Start time smaller than that of any old items. Insert if no overlap.
+ if (newItemEndTime > oldItems.get(0).getStartTimeUtcMillis()) continue;
+ } else if (newItemStartTime
+ > oldItems.get(oldItemCount - 1).getStartTimeUtcMillis()) {
+ // Start time larger than that of any old item. Insert if no overlap.
+ if (newItemStartTime < oldItems.get(oldItemCount - 1).getEndTimeUtcMillis())
+ continue;
+ } else {
+ int pos =
+ Collections.binarySearch(
+ oldItems,
+ newItem,
+ new Comparator<EitItem>() {
+ @Override
+ public int compare(EitItem lhs, EitItem rhs) {
+ return Long.compare(
+ lhs.getStartTimeUtcMillis(),
+ rhs.getStartTimeUtcMillis());
+ }
+ });
+ if (pos >= 0) {
+ // Same start Time found. Overlapped.
+ continue;
+ }
+ int insertPoint = -1 - pos;
+ // Check the two adjacent items.
+ if (newItemStartTime < oldItems.get(insertPoint - 1).getEndTimeUtcMillis()
+ || newItemEndTime > oldItems.get(insertPoint).getStartTimeUtcMillis()) {
+ continue;
+ }
+ }
+ ops.add(
+ buildContentProviderOperation(
+ ContentProviderOperation.newInsert(TvContract.Programs.CONTENT_URI),
+ newItem,
+ channel));
+ if (ops.size() >= BATCH_OPERATION_COUNT) {
+ applyBatch(channel.getName(), ops);
+ ops.clear();
+ }
+ }
+ applyBatch(channel.getName(), ops);
+ return;
+ }
+
+ List<EitItem> outdatedOldItems = new ArrayList<>();
+ Map<Integer, EitItem> newEitItemMap = new HashMap<>();
+ for (EitItem item : items) {
+ newEitItemMap.put(item.getEventId(), item);
+ }
+ for (EitItem oldItem : oldItems) {
+ EitItem item = newEitItemMap.get(oldItem.getEventId());
+ if (item == null) {
+ outdatedOldItems.add(oldItem);
+ continue;
+ }
+
+ // Since program descriptions arrive at different time, the older one may have the
+ // correct program description while the newer one has no clue what value is.
+ if (oldItem.getDescription() != null
+ && item.getDescription() == null
+ && oldItem.getEventId() == item.getEventId()
+ && oldItem.getStartTime() == item.getStartTime()
+ && oldItem.getLengthInSecond() == item.getLengthInSecond()
+ && Objects.equals(oldItem.getContentRating(), item.getContentRating())
+ && Objects.equals(oldItem.getBroadcastGenre(), item.getBroadcastGenre())
+ && Objects.equals(oldItem.getCanonicalGenre(), item.getCanonicalGenre())) {
+ item.setDescription(oldItem.getDescription());
+ }
+ if (item.compareTo(oldItem) != 0) {
+ ops.add(
+ buildContentProviderOperation(
+ ContentProviderOperation.newUpdate(
+ TvContract.buildProgramUri(oldItem.getProgramId())),
+ item,
+ null));
+ if (ops.size() >= BATCH_OPERATION_COUNT) {
+ applyBatch(channel.getName(), ops);
+ ops.clear();
+ }
+ }
+ newEitItemMap.remove(item.getEventId());
+ }
+ for (EitItem unverifiedOldItems : outdatedOldItems) {
+ if (unverifiedOldItems.getStartTimeUtcMillis() > currentTime) {
+ // The given new EIT item list covers partial time span of EPG. Here, we delete old
+ // item only when it has an overlapping with the new EIT item list.
+ long startTime = unverifiedOldItems.getStartTimeUtcMillis();
+ long endTime = unverifiedOldItems.getEndTimeUtcMillis();
+ for (EitItem item : newEitItemMap.values()) {
+ long newItemStartTime = item.getStartTimeUtcMillis();
+ long newItemEndTime = item.getEndTimeUtcMillis();
+ if ((startTime >= newItemStartTime && startTime < newItemEndTime)
+ || (endTime > newItemStartTime && endTime <= newItemEndTime)) {
+ ops.add(
+ ContentProviderOperation.newDelete(
+ TvContract.buildProgramUri(
+ unverifiedOldItems.getProgramId()))
+ .build());
+ if (ops.size() >= BATCH_OPERATION_COUNT) {
+ applyBatch(channel.getName(), ops);
+ ops.clear();
+ }
+ break;
+ }
+ }
+ }
+ }
+ for (EitItem item : newEitItemMap.values()) {
+ if (item.getEndTimeUtcMillis() < currentTime) {
+ continue;
+ }
+ ops.add(
+ buildContentProviderOperation(
+ ContentProviderOperation.newInsert(TvContract.Programs.CONTENT_URI),
+ item,
+ channel));
+ if (ops.size() >= BATCH_OPERATION_COUNT) {
+ applyBatch(channel.getName(), ops);
+ ops.clear();
+ }
+ }
+
+ applyBatch(channel.getName(), ops);
+ }
+
+ private ContentProviderOperation buildContentProviderOperation(
+ 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())
+ .withValue(
+ TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS,
+ item.getStartTimeUtcMillis())
+ .withValue(
+ TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS,
+ item.getEndTimeUtcMillis())
+ .withValue(TvContract.Programs.COLUMN_CONTENT_RATING, item.getContentRating())
+ .withValue(TvContract.Programs.COLUMN_AUDIO_LANGUAGE, item.getAudioLanguage())
+ .withValue(TvContract.Programs.COLUMN_SHORT_DESCRIPTION, item.getDescription())
+ .withValue(TvContract.Programs.COLUMN_VERSION_NUMBER, item.getEventId());
+ }
+ return builder.build();
+ }
+
+ private void applyBatch(String channelName, ArrayList<ContentProviderOperation> operations) {
+ try {
+ mContext.getContentResolver().applyBatch(TvContract.AUTHORITY, operations);
+ } catch (RemoteException | OperationApplicationException e) {
+ Log.e(TAG, "Error updating EPG " + channelName, e);
+ }
+ }
+
+ private void handleChannel(TunerChannel channel) {
+ long channelId = getChannelId(channel);
+ ContentValues values = new ContentValues();
+ values.put(TvContract.Channels.COLUMN_NETWORK_AFFILIATION, channel.getShortName());
+ values.put(TvContract.Channels.COLUMN_SERVICE_TYPE, channel.getServiceTypeName());
+ values.put(TvContract.Channels.COLUMN_TRANSPORT_STREAM_ID, channel.getTsid());
+ values.put(TvContract.Channels.COLUMN_DISPLAY_NUMBER, channel.getDisplayNumber());
+ 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);
+ values.put(
+ TvContract.Channels.COLUMN_TYPE,
+ "QAM256".equals(channel.getModulation())
+ ? TvContract.Channels.TYPE_ATSC_C
+ : TvContract.Channels.TYPE_ATSC_T);
+ values.put(TvContract.Channels.COLUMN_SERVICE_ID, channel.getProgramNumber());
+
+ // ATSC doesn't have original_network_id
+ values.put(TvContract.Channels.COLUMN_ORIGINAL_NETWORK_ID, channel.getFrequency());
+
+ Uri channelUri =
+ mContext.getContentResolver().insert(TvContract.Channels.CONTENT_URI, values);
+ channelId = ContentUris.parseId(channelUri);
+ } else {
+ mContext.getContentResolver()
+ .update(TvContract.buildChannelUri(channelId), values, null, null);
+ }
+ channel.setChannelId(channelId);
+ mTunerChannelMap.put(channelId, channel);
+ mTunerChannelIdMap.put(channel, channelId);
+ if (mIsScanning.get()) {
+ mScannedChannels.add(channel);
+ mPreviousScannedChannels.remove(channel);
+ }
+ if (mListener != null) {
+ mListener.onChannelArrived(channel);
+ }
+ }
+
+ private void clearChannels() {
+ int count = mContext.getContentResolver().delete(mChannelsUri, null, null);
+ if (count > 0) {
+ // We have just deleted obsolete data. Now tell the user that he or she needs
+ // to perform the auto-scan again.
+ if (mListener != null) {
+ mListener.onRescanNeeded();
+ }
+ }
+ }
+
+ private void checkVersion() {
+ 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;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private long getChannelId(TunerChannel channel) {
+ Long channelId = mTunerChannelIdMap.get(channel);
+ if (channelId != null) {
+ return channelId;
+ }
+ mHandler.sendEmptyMessage(MSG_BUILD_CHANNEL_MAP);
+ try (Cursor cursor =
+ mContext.getContentResolver()
+ .query(mChannelsUri, CHANNEL_DATA_SELECTION_ARGS, null, null, null)) {
+ if (cursor != null && cursor.moveToFirst()) {
+ do {
+ channelId = cursor.getLong(0);
+ byte[] providerData = cursor.getBlob(1);
+ TunerChannel tunerChannel = TunerChannel.parseFrom(providerData);
+ if (tunerChannel != null && tunerChannel.compareTo(channel) == 0) {
+ channel.setChannelId(channelId);
+ mTunerChannelIdMap.put(channel, channelId);
+ mTunerChannelMap.put(channelId, channel);
+ return channelId;
+ }
+ } while (cursor.moveToNext());
+ }
+ }
+ return -1;
+ }
+
+ private List<EitItem> getAllProgramsForChannel(TunerChannel channel) {
+ return getAllProgramsForChannel(channel, null, null);
+ }
+
+ private List<EitItem> getAllProgramsForChannel(
+ TunerChannel channel, @Nullable Long startTimeMs, @Nullable Long endTimeMs) {
+ List<EitItem> items = new ArrayList<>();
+ long channelId = channel.getChannelId();
+ Uri programsUri =
+ (startTimeMs == null || endTimeMs == null)
+ ? TvContract.buildProgramsUriForChannel(channelId)
+ : TvContract.buildProgramsUriForChannel(channelId, startTimeMs, endTimeMs);
+ try (Cursor cursor =
+ mContext.getContentResolver()
+ .query(programsUri, ALL_PROGRAMS_SELECTION_ARGS, null, null, null)) {
+ if (cursor != null && cursor.moveToFirst()) {
+ do {
+ long id = cursor.getLong(0);
+ String titleText = cursor.getString(1);
+ long startTime =
+ ConvertUtils.convertUnixEpochToGPSTime(
+ cursor.getLong(2) / DateUtils.SECOND_IN_MILLIS);
+ long endTime =
+ ConvertUtils.convertUnixEpochToGPSTime(
+ cursor.getLong(3) / DateUtils.SECOND_IN_MILLIS);
+ int lengthInSecond = (int) (endTime - startTime);
+ String contentRating = cursor.getString(4);
+ String broadcastGenre = cursor.getString(5);
+ String canonicalGenre = cursor.getString(6);
+ String description = cursor.getString(7);
+ int eventId = cursor.getInt(8);
+ EitItem eitItem =
+ new EitItem(
+ id,
+ eventId,
+ titleText,
+ startTime,
+ lengthInSecond,
+ contentRating,
+ null,
+ null,
+ broadcastGenre,
+ canonicalGenre,
+ description);
+ items.add(eitItem);
+ } while (cursor.moveToNext());
+ }
+ }
+ return items;
+ }
+
+ private void buildChannelMap() {
+ ArrayList<TunerChannel> channels = new ArrayList<>();
+ try (Cursor cursor =
+ mContext.getContentResolver()
+ .query(mChannelsUri, CHANNEL_DATA_SELECTION_ARGS, null, null, null)) {
+ if (cursor != null && cursor.moveToFirst()) {
+ do {
+ long channelId = cursor.getLong(0);
+ byte[] data = cursor.getBlob(1);
+ TunerChannel channel = TunerChannel.parseFrom(data);
+ if (channel != null) {
+ channel.setChannelId(channelId);
+ channels.add(channel);
+ }
+ } while (cursor.moveToNext());
+ }
+ }
+ mTunerChannelMap.clear();
+ mTunerChannelIdMap.clear();
+ for (TunerChannel channel : channels) {
+ mTunerChannelMap.put(channel.getChannelId(), channel);
+ mTunerChannelIdMap.put(channel, channel.getChannelId());
+ }
+ }
+
+ private static class ChannelEvent {
+ public final TunerChannel channel;
+ public final List<EitItem> eitItems;
+
+ public ChannelEvent(TunerChannel channel, List<EitItem> eitItems) {
+ this.channel = channel;
+ this.eitItems = eitItems;
+ }
+ }
+}
diff --git a/src/com/android/tv/tuner/tvinput/EventDetector.java b/src/com/android/tv/tuner/tvinput/EventDetector.java
new file mode 100644
index 00000000..c529c6db
--- /dev/null
+++ b/src/com/android/tv/tuner/tvinput/EventDetector.java
@@ -0,0 +1,349 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.tvinput;
+
+import android.util.Log;
+import android.util.SparseArray;
+import android.util.SparseBooleanArray;
+import com.android.tv.tuner.TunerHal;
+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 com.android.tv.tuner.ts.TsParser;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Detects channels and programs that are emerged or changed while parsing ATSC PSIP information.
+ */
+public class EventDetector {
+ private static final String TAG = "EventDetector";
+ private static final boolean DEBUG = false;
+ public static final int ALL_PROGRAM_NUMBERS = -1;
+
+ private final TunerHal mTunerHal;
+
+ private TsParser mTsParser;
+ private final Set<Integer> mPidSet = new HashSet<>();
+
+ // 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 List<EventListener> mEventListeners = new ArrayList<>();
+ private int mFrequency;
+ private String mModulation;
+ private int mProgramNumber = ALL_PROGRAM_NUMBERS;
+
+ private final TsParser.TsOutputListener mTsOutputListener =
+ new TsParser.TsOutputListener() {
+ @Override
+ public void onPatDetected(List<PsiData.PatItem> items) {
+ for (PsiData.PatItem i : items) {
+ if (mProgramNumber == ALL_PROGRAM_NUMBERS
+ || mProgramNumber == i.getProgramNo()) {
+ mTunerHal.addPidFilter(i.getPmtPid(), TunerHal.FILTER_TYPE_OTHER);
+ }
+ }
+ }
+
+ @Override
+ public void onEitPidDetected(int pid) {
+ startListening(pid);
+ }
+
+ @Override
+ public void onEitItemParsed(
+ PsipData.VctItem channel, List<PsipData.EitItem> items) {
+ TunerChannel tunerChannel = mChannelMap.get(channel.getProgramNumber());
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "onEitItemParsed tunerChannel:"
+ + tunerChannel
+ + " "
+ + channel.getProgramNumber());
+ }
+ int channelSourceId = channel.getSourceId();
+
+ // Source id 0 is useful for cases where a cable operator wishes to define a
+ // channel for
+ // which no EPG data is currently available.
+ // We don't handle such a case.
+ if (channelSourceId == 0) {
+ return;
+ }
+
+ // If at least a one caption track have been found in EIT items for the given
+ // channel,
+ // we starts to interpret the zero tracks as a clearance of the caption tracks.
+ boolean captionTracksFound = mEitCaptionTracksFound.get(channelSourceId);
+ for (PsipData.EitItem item : items) {
+ if (captionTracksFound) {
+ break;
+ }
+ List<AtscCaptionTrack> captionTracks = item.getCaptionTracks();
+ if (captionTracks != null && !captionTracks.isEmpty()) {
+ captionTracksFound = true;
+ }
+ }
+ mEitCaptionTracksFound.put(channelSourceId, captionTracksFound);
+ if (captionTracksFound) {
+ for (PsipData.EitItem item : items) {
+ item.setHasCaptionTrack();
+ }
+ }
+ if (tunerChannel != null && !mEventListeners.isEmpty()) {
+ for (EventListener eventListener : mEventListeners) {
+ eventListener.onEventDetected(tunerChannel, items);
+ }
+ }
+ }
+
+ @Override
+ public void onEttPidDetected(int pid) {
+ startListening(pid);
+ }
+
+ @Override
+ public void onAllVctItemsParsed() {
+ if (!mEventListeners.isEmpty()) {
+ for (EventListener eventListener : mEventListeners) {
+ eventListener.onChannelScanDone();
+ }
+ }
+ }
+
+ @Override
+ public void onVctItemParsed(
+ PsipData.VctItem channel, List<PsiData.PmtItem> pmtItems) {
+ if (DEBUG) {
+ Log.d(TAG, "onVctItemParsed VCT " + 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.getProgramNumber();
+
+ // If at least a one caption track have been found in VCT items for the given
+ // channel,
+ // we starts to interpret the zero tracks as a clearance of the caption tracks.
+ boolean captionTracksFound =
+ mVctCaptionTracksFound.get(channelProgramNumber)
+ || !captionTracks.isEmpty();
+ mVctCaptionTracksFound.put(channelProgramNumber, captionTracksFound);
+ if (captionTracksFound) {
+ tunerChannel.setHasCaptionTrack();
+ }
+ tunerChannel.setAudioTracks(audioTracks);
+ tunerChannel.setCaptionTracks(captionTracks);
+ tunerChannel.setFrequency(mFrequency);
+ tunerChannel.setModulation(mModulation);
+ mChannelMap.put(tunerChannel.getProgramNumber(), tunerChannel);
+ boolean found = mVctProgramNumberSet.contains(channelProgramNumber);
+ if (!found) {
+ mVctProgramNumberSet.add(channelProgramNumber);
+ }
+ 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);
+ }
+ }
+ }
+ };
+
+ /** Listener for detecting ATSC TV channels and receiving EPG data. */
+ public interface EventListener {
+
+ /**
+ * Fired when new information of an ATSC TV channel arrived.
+ *
+ * @param channel an ATSC TV channel
+ * @param channelArrivedAtFirstTime tells whether this channel arrived at first time
+ */
+ void onChannelDetected(TunerChannel channel, boolean channelArrivedAtFirstTime);
+
+ /**
+ * Fired when new program events of an ATSC TV channel arrived.
+ *
+ * @param channel an ATSC TV channel
+ * @param items a list of EIT items that were received
+ */
+ void onEventDetected(TunerChannel channel, List<PsipData.EitItem> items);
+
+ /**
+ * Fired when information of all detectable ATSC TV channels in current frequency arrived.
+ */
+ void onChannelScanDone();
+ }
+
+ /**
+ * Creates a detector for ATSC TV channles and program information.
+ *
+ * @param usbTunerInteface {@link TunerHal}
+ */
+ public EventDetector(TunerHal usbTunerInteface) {
+ mTunerHal = usbTunerInteface;
+ }
+
+ private void 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();
+ }
+
+ /**
+ * Starts detecting channel and program information.
+ *
+ * @param frequency The frequency to listen to.
+ * @param modulation The modulation type.
+ * @param programNumber The program number if this is for handling tune request. For scanning
+ * purpose, supply {@link #ALL_PROGRAM_NUMBERS}.
+ */
+ public void startDetecting(int frequency, String modulation, int programNumber) {
+ reset();
+ mFrequency = frequency;
+ mModulation = modulation;
+ mProgramNumber = programNumber;
+ }
+
+ private void startListening(int pid) {
+ if (mPidSet.contains(pid)) {
+ return;
+ }
+ mPidSet.add(pid);
+ mTunerHal.addPidFilter(pid, TunerHal.FILTER_TYPE_OTHER);
+ }
+
+ /**
+ * Feeds ATSC TS stream to detect channel and program information.
+ *
+ * @param data buffer for ATSC TS stream
+ * @param startOffset the offset where buffer starts
+ * @param length The length of available data
+ */
+ public void feedTSStream(byte[] data, int startOffset, int length) {
+ if (mPidSet.isEmpty()) {
+ startListening(TsParser.ATSC_SI_BASE_PID);
+ }
+ if (mTsParser != null) {
+ mTsParser.feedTSData(data, startOffset, length);
+ }
+ }
+
+ /**
+ * Retrieves the channel information regardless of being well-formed.
+ *
+ * @return {@link List} of {@link TunerChannel}
+ */
+ 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
new file mode 100644
index 00000000..f2ed72f1
--- /dev/null
+++ b/src/com/android/tv/tuner/tvinput/FileSourceEventDetector.java
@@ -0,0 +1,259 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.tvinput;
+
+import android.util.Log;
+import android.util.SparseArray;
+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.source.FileTsStreamer;
+import com.android.tv.tuner.ts.TsParser;
+import com.android.tv.tuner.tvinput.EventDetector.EventListener;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * PSIP event detector for a file source.
+ *
+ * <p>Uses {@link TsParser} to analyze input MPEG-2 transport stream, detects and reports various
+ * PSIP-related events via {@link TsParser.TsOutputListener}.
+ */
+public class FileSourceEventDetector {
+ private static final String TAG = "FileSourceEventDetector";
+ private static final boolean DEBUG = true;
+ public static final int ALL_PROGRAM_NUMBERS = 0;
+
+ 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, boolean enableDvbSignal) {
+ mEventListener = listener;
+ mEnableDvbSignal = enableDvbSignal;
+ }
+
+ /**
+ * Starts detecting channel and program information.
+ *
+ * @param provider MPEG-2 transport stream source.
+ * @param programNumber The program number if this is for handling tune request. For scanning
+ * purpose, supply {@link #ALL_PROGRAM_NUMBERS}.
+ */
+ public void start(FileTsStreamer.StreamProvider provider, int programNumber) {
+ mStreamProvider = provider;
+ mProgramNumber = programNumber;
+ reset();
+ }
+
+ private void reset() {
+ mTsParser = new TsParser(mTsOutputListener, mEnableDvbSignal); // TODO: Use TsParser.reset()
+ mStreamProvider.clearPidFilter();
+ mVctProgramNumberSet.clear();
+ mSdtProgramNumberSet.clear();
+ mVctCaptionTracksFound.clear();
+ mEitCaptionTracksFound.clear();
+ mChannelMap.clear();
+ }
+
+ public void feedTSStream(byte[] data, int startOffset, int length) {
+ if (mStreamProvider.isFilterEmpty()) {
+ startListening(TsParser.ATSC_SI_BASE_PID);
+ startListening(TsParser.PAT_PID);
+ }
+ if (mTsParser != null) {
+ mTsParser.feedTSData(data, startOffset, length);
+ }
+ }
+
+ private void startListening(int pid) {
+ if (mStreamProvider.isInFilter(pid)) {
+ return;
+ }
+ mStreamProvider.addPidFilter(pid);
+ }
+
+ private final TsParser.TsOutputListener mTsOutputListener =
+ new TsParser.TsOutputListener() {
+ @Override
+ public void onPatDetected(List<PatItem> items) {
+ for (PatItem i : items) {
+ if (mProgramNumber == ALL_PROGRAM_NUMBERS
+ || mProgramNumber == i.getProgramNo()) {
+ mStreamProvider.addPidFilter(i.getPmtPid());
+ }
+ }
+ }
+
+ @Override
+ public void onEitPidDetected(int pid) {
+ startListening(pid);
+ }
+
+ @Override
+ public void onEitItemParsed(VctItem channel, List<EitItem> items) {
+ TunerChannel tunerChannel = mChannelMap.get(channel.getProgramNumber());
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "onEitItemParsed tunerChannel:"
+ + tunerChannel
+ + " "
+ + channel.getProgramNumber());
+ }
+ int channelSourceId = channel.getSourceId();
+
+ // Source id 0 is useful for cases where a cable operator wishes to define a
+ // channel for
+ // which no EPG data is currently available.
+ // We don't handle such a case.
+ if (channelSourceId == 0) {
+ return;
+ }
+
+ // If at least a one caption track have been found in EIT items for the given
+ // channel,
+ // we starts to interpret the zero tracks as a clearance of the caption tracks.
+ boolean captionTracksFound = mEitCaptionTracksFound.get(channelSourceId);
+ for (EitItem item : items) {
+ if (captionTracksFound) {
+ break;
+ }
+ List<AtscCaptionTrack> captionTracks = item.getCaptionTracks();
+ if (captionTracks != null && !captionTracks.isEmpty()) {
+ captionTracksFound = true;
+ }
+ }
+ mEitCaptionTracksFound.put(channelSourceId, captionTracksFound);
+ if (captionTracksFound) {
+ for (EitItem item : items) {
+ item.setHasCaptionTrack();
+ }
+ }
+ if (tunerChannel != null && mEventListener != null) {
+ mEventListener.onEventDetected(tunerChannel, items);
+ }
+ }
+
+ @Override
+ public void onEttPidDetected(int pid) {
+ startListening(pid);
+ }
+
+ @Override
+ public void onAllVctItemsParsed() {
+ // do nothing.
+ }
+
+ @Override
+ public void onVctItemParsed(VctItem channel, List<PmtItem> pmtItems) {
+ if (DEBUG) {
+ Log.d(TAG, "onVctItemParsed VCT " + 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.forFile(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.getProgramNumber();
+
+ // If at least a one caption track have been found in VCT items for the given
+ // channel,
+ // we starts to interpret the zero tracks as a clearance of the caption tracks.
+ boolean captionTracksFound =
+ mVctCaptionTracksFound.get(channelProgramNumber)
+ || !captionTracks.isEmpty();
+ mVctCaptionTracksFound.put(channelProgramNumber, captionTracksFound);
+ if (captionTracksFound) {
+ tunerChannel.setHasCaptionTrack();
+ }
+ tunerChannel.setFilepath(mStreamProvider.getFilepath());
+ tunerChannel.setAudioTracks(audioTracks);
+ tunerChannel.setCaptionTracks(captionTracks);
+
+ mChannelMap.put(tunerChannel.getProgramNumber(), tunerChannel);
+ boolean found = mVctProgramNumberSet.contains(channelProgramNumber);
+ if (!found) {
+ mVctProgramNumberSet.add(channelProgramNumber);
+ }
+ if (mEventListener != null) {
+ 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/PlaybackBufferListener.java b/src/com/android/tv/tuner/tvinput/PlaybackBufferListener.java
new file mode 100644
index 00000000..1628bcfb
--- /dev/null
+++ b/src/com/android/tv/tuner/tvinput/PlaybackBufferListener.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.tvinput;
+
+/** The listener for buffer events occurred during playback. */
+public interface PlaybackBufferListener {
+
+ /**
+ * Invoked when the start position of the buffer has been changed.
+ *
+ * @param startTimeMs the new start time of the buffer in millisecond
+ */
+ void onBufferStartTimeChanged(long startTimeMs);
+
+ /**
+ * Invoked when the state of the buffer has been changed.
+ *
+ * @param available whether the buffer is available or not
+ */
+ void onBufferStateChanged(boolean available);
+
+ /** Invoked when the disk speed is too slow to write the buffers. */
+ void onDiskTooSlow();
+}
diff --git a/src/com/android/tv/tuner/tvinput/TunerDebug.java b/src/com/android/tv/tuner/tvinput/TunerDebug.java
new file mode 100644
index 00000000..1df0b5c3
--- /dev/null
+++ b/src/com/android/tv/tuner/tvinput/TunerDebug.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.tvinput;
+
+import android.os.SystemClock;
+import android.util.Log;
+
+/** A class to maintain various debugging information. */
+public class TunerDebug {
+ private static final String TAG = "TunerDebug";
+ public static final boolean ENABLED = false;
+
+ private int mVideoFrameDrop;
+ private int mBytesInQueue;
+
+ private long mAudioPositionUs;
+ private long mAudioPtsUs;
+ private long mVideoPtsUs;
+
+ private long mLastAudioPositionUs;
+ private long mLastAudioPtsUs;
+ private long mLastVideoPtsUs;
+ private long mLastCheckTimestampMs;
+
+ private long mAudioPositionUsRate;
+ private long mAudioPtsUsRate;
+ private long mVideoPtsUsRate;
+
+ private TunerDebug() {
+ mVideoFrameDrop = 0;
+ mLastCheckTimestampMs = SystemClock.elapsedRealtime();
+ }
+
+ private static class LazyHolder {
+ private static final TunerDebug INSTANCE = new TunerDebug();
+ }
+
+ public static TunerDebug getInstance() {
+ return LazyHolder.INSTANCE;
+ }
+
+ public static void notifyVideoFrameDrop(int count, long delta) {
+ // TODO: provide timestamp mismatch information using delta
+ TunerDebug sTunerDebug = getInstance();
+ sTunerDebug.mVideoFrameDrop += count;
+ }
+
+ public static int getVideoFrameDrop() {
+ TunerDebug sTunerDebug = getInstance();
+ int videoFrameDrop = sTunerDebug.mVideoFrameDrop;
+ if (videoFrameDrop > 0) {
+ Log.d(TAG, "Dropped video frame: " + videoFrameDrop);
+ }
+ sTunerDebug.mVideoFrameDrop = 0;
+ return videoFrameDrop;
+ }
+
+ public static void setBytesInQueue(int bytesInQueue) {
+ TunerDebug sTunerDebug = getInstance();
+ sTunerDebug.mBytesInQueue = bytesInQueue;
+ }
+
+ public static int getBytesInQueue() {
+ TunerDebug sTunerDebug = getInstance();
+ return sTunerDebug.mBytesInQueue;
+ }
+
+ public static void setAudioPositionUs(long audioPositionUs) {
+ TunerDebug sTunerDebug = getInstance();
+ sTunerDebug.mAudioPositionUs = audioPositionUs;
+ }
+
+ public static long getAudioPositionUs() {
+ TunerDebug sTunerDebug = getInstance();
+ return sTunerDebug.mAudioPositionUs;
+ }
+
+ public static void setAudioPtsUs(long audioPtsUs) {
+ TunerDebug sTunerDebug = getInstance();
+ sTunerDebug.mAudioPtsUs = audioPtsUs;
+ }
+
+ public static long getAudioPtsUs() {
+ TunerDebug sTunerDebug = getInstance();
+ return sTunerDebug.mAudioPtsUs;
+ }
+
+ public static void setVideoPtsUs(long videoPtsUs) {
+ TunerDebug sTunerDebug = getInstance();
+ sTunerDebug.mVideoPtsUs = videoPtsUs;
+ }
+
+ public static long getVideoPtsUs() {
+ TunerDebug sTunerDebug = getInstance();
+ return sTunerDebug.mVideoPtsUs;
+ }
+
+ public static void calculateDiff() {
+ TunerDebug sTunerDebug = getInstance();
+ long currentTime = SystemClock.elapsedRealtime();
+ long duration = currentTime - sTunerDebug.mLastCheckTimestampMs;
+ if (duration != 0) {
+ sTunerDebug.mAudioPositionUsRate =
+ (sTunerDebug.mAudioPositionUs - sTunerDebug.mLastAudioPositionUs)
+ * 1000
+ / duration;
+ sTunerDebug.mAudioPtsUsRate =
+ (sTunerDebug.mAudioPtsUs - sTunerDebug.mLastAudioPtsUs) * 1000 / duration;
+ sTunerDebug.mVideoPtsUsRate =
+ (sTunerDebug.mVideoPtsUs - sTunerDebug.mLastVideoPtsUs) * 1000 / duration;
+ }
+
+ sTunerDebug.mLastAudioPositionUs = sTunerDebug.mAudioPositionUs;
+ sTunerDebug.mLastAudioPtsUs = sTunerDebug.mAudioPtsUs;
+ sTunerDebug.mLastVideoPtsUs = sTunerDebug.mVideoPtsUs;
+ sTunerDebug.mLastCheckTimestampMs = currentTime;
+ }
+
+ public static long getAudioPositionUsRate() {
+ TunerDebug sTunerDebug = getInstance();
+ return sTunerDebug.mAudioPositionUsRate;
+ }
+
+ public static long getAudioPtsUsRate() {
+ TunerDebug sTunerDebug = getInstance();
+ return sTunerDebug.mAudioPtsUsRate;
+ }
+
+ public static long getVideoPtsUsRate() {
+ TunerDebug sTunerDebug = getInstance();
+ return sTunerDebug.mVideoPtsUsRate;
+ }
+}
diff --git a/src/com/android/tv/tuner/tvinput/TunerRecordingSession.java b/src/com/android/tv/tuner/tvinput/TunerRecordingSession.java
new file mode 100644
index 00000000..a1f0c773
--- /dev/null
+++ b/src/com/android/tv/tuner/tvinput/TunerRecordingSession.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.tvinput;
+
+import android.content.Context;
+import android.media.tv.TvInputService;
+import android.net.Uri;
+import android.support.annotation.MainThread;
+import android.support.annotation.Nullable;
+import android.support.annotation.WorkerThread;
+import android.util.Log;
+
+/** Processes DVR recordings, and deletes the previously recorded contents. */
+public class TunerRecordingSession extends TvInputService.RecordingSession {
+ private static final String TAG = "TunerRecordingSession";
+ private static final boolean DEBUG = false;
+
+ private final TunerRecordingSessionWorker mSessionWorker;
+
+ public TunerRecordingSession(
+ Context context, String inputId, ChannelDataManager channelDataManager) {
+ super(context);
+ mSessionWorker =
+ new TunerRecordingSessionWorker(context, inputId, channelDataManager, this);
+ }
+
+ // RecordingSession
+ @MainThread
+ @Override
+ public void onTune(Uri channelUri) {
+ // TODO(dvr): support calling more than once, http://b/27171225
+ if (DEBUG) {
+ Log.d(TAG, "Requesting recording session tune: " + channelUri);
+ }
+ mSessionWorker.tune(channelUri);
+ }
+
+ @MainThread
+ @Override
+ public void onRelease() {
+ if (DEBUG) {
+ Log.d(TAG, "Requesting recording session release.");
+ }
+ mSessionWorker.release();
+ }
+
+ @MainThread
+ @Override
+ public void onStartRecording(@Nullable Uri programUri) {
+ if (DEBUG) {
+ Log.d(TAG, "Requesting start recording.");
+ }
+ mSessionWorker.startRecording(programUri);
+ }
+
+ @MainThread
+ @Override
+ public void onStopRecording() {
+ if (DEBUG) {
+ Log.d(TAG, "Requesting stop recording.");
+ }
+ mSessionWorker.stopRecording();
+ }
+
+ // Called from TunerRecordingSessionImpl in a worker thread.
+ @WorkerThread
+ public void onTuned(Uri channelUri) {
+ if (DEBUG) {
+ Log.d(TAG, "Notifying recording session tuned.");
+ }
+ notifyTuned(channelUri);
+ }
+
+ @WorkerThread
+ public void onRecordFinished(final Uri recordedProgramUri) {
+ if (DEBUG) {
+ Log.d(TAG, "Notifying record successfully finished.");
+ }
+ notifyRecordingStopped(recordedProgramUri);
+ }
+
+ @WorkerThread
+ public void onError(int reason) {
+ Log.w(TAG, "Notifying recording error: " + reason);
+ notifyError(reason);
+ }
+}
diff --git a/src/com/android/tv/tuner/tvinput/TunerRecordingSessionWorker.java b/src/com/android/tv/tuner/tvinput/TunerRecordingSessionWorker.java
new file mode 100644
index 00000000..1bc4e295
--- /dev/null
+++ b/src/com/android/tv/tuner/tvinput/TunerRecordingSessionWorker.java
@@ -0,0 +1,683 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.tvinput;
+
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.media.tv.TvContract;
+import android.media.tv.TvInputManager;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Message;
+import android.support.annotation.IntDef;
+import android.support.annotation.MainThread;
+import android.support.annotation.Nullable;
+import android.util.Log;
+import android.util.Pair;
+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.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;
+import com.android.tv.tuner.exoplayer.buffer.DvrStorageManager;
+import com.android.tv.tuner.source.TsDataSource;
+import com.android.tv.tuner.source.TsDataSourceManager;
+import com.android.tv.util.Utils;
+import com.google.android.exoplayer.C;
+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.TimeUnit;
+
+/** Implements a DVR feature. */
+public class TunerRecordingSessionWorker
+ implements PlaybackBufferListener,
+ EventDetector.EventListener,
+ SampleExtractor.OnCompletionListener,
+ Handler.Callback {
+ private static final String TAG = "TunerRecordingSessionW";
+ private static final boolean DEBUG = false;
+
+ 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;
+ private static final int MSG_TUNE = 1;
+ private static final int MSG_START_RECORDING = 2;
+ private static final int MSG_PREPARE_RECODER = 3;
+ private static final int MSG_STOP_RECORDING = 4;
+ private static final int MSG_MONITOR_STORAGE_STATUS = 5;
+ private static final int MSG_RELEASE = 6;
+ private static final int MSG_UPDATE_CC_INFO = 7;
+ private final RecordingCapability mCapabilities;
+
+ public RecordingCapability getCapabilities() {
+ return mCapabilities;
+ }
+
+ @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_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;
+ private final DvrStorageStatusManager mDvrStorageStatusManager;
+ private final Handler mHandler;
+ private final TsDataSourceManager mSourceManager;
+ private final Random mRandom = new Random();
+
+ private TsDataSource mTunerSource;
+ private TunerChannel mChannel;
+ private File mStorageDir;
+ private long mRecordStartTime;
+ private long mRecordEndTime;
+ private boolean mRecorderRunning;
+ 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());
+ mContext = context;
+ HandlerThread handlerThread = new HandlerThread(TAG);
+ handlerThread.start();
+ mHandler = new Handler(handlerThread.getLooper(), this);
+ mDvrStorageStatusManager =
+ TvApplication.getSingletons(context).getDvrStorageStatusManager();
+ mChannelDataManager = dataManager;
+ mChannelDataManager.checkDataVersion(context);
+ mSourceManager = TsDataSourceManager.createSourceManager(true);
+ mCapabilities = new DvbDeviceAccessor(context).getRecordingCapability(inputId);
+ mInputId = inputId;
+ if (DEBUG) Log.d(TAG, mCapabilities.toString());
+ mSession = session;
+ }
+
+ // PlaybackBufferListener
+ @Override
+ public void onBufferStartTimeChanged(long startTimeMs) {}
+
+ @Override
+ public void onBufferStateChanged(boolean available) {}
+
+ @Override
+ public void onDiskTooSlow() {}
+
+ // EventDetector.EventListener
+ @Override
+ public void onChannelDetected(TunerChannel channel, boolean channelArrivedAtFirstTime) {
+ if (mChannel == null || mChannel.compareTo(channel) != 0) {
+ return;
+ }
+ mChannelDataManager.notifyChannelDetected(channel, channelArrivedAtFirstTime);
+ }
+
+ @Override
+ public void onEventDetected(TunerChannel channel, List<PsipData.EitItem> items) {
+ if (mChannel == null || mChannel.compareTo(channel) != 0) {
+ return;
+ }
+ mHandler.obtainMessage(MSG_UPDATE_CC_INFO, new Pair<>(channel, items)).sendToTarget();
+ mChannelDataManager.notifyEventDetected(channel, items);
+ }
+
+ @Override
+ public void onChannelScanDone() {
+ // do nothing.
+ }
+
+ // SampleExtractor.OnCompletionListener
+ @Override
+ public void onCompletion(boolean success, long lastExtractedPositionUs) {
+ onRecordingResult(success, lastExtractedPositionUs);
+ reset();
+ }
+
+ /** Tunes to {@code channelUri}. */
+ @MainThread
+ public void tune(Uri channelUri) {
+ mHandler.removeCallbacksAndMessages(null);
+ mHandler.obtainMessage(MSG_TUNE, 0, 0, channelUri).sendToTarget();
+ }
+
+ /** Starts recording. */
+ @MainThread
+ public void startRecording(@Nullable Uri programUri) {
+ mHandler.obtainMessage(MSG_START_RECORDING, programUri).sendToTarget();
+ }
+
+ /** Stops recording. */
+ @MainThread
+ public void stopRecording() {
+ mHandler.sendEmptyMessage(MSG_STOP_RECORDING);
+ }
+
+ /** Releases all resources. */
+ @MainThread
+ public void release() {
+ mHandler.removeCallbacksAndMessages(null);
+ mHandler.sendEmptyMessage(MSG_RELEASE);
+ }
+
+ @Override
+ public boolean handleMessage(Message msg) {
+ 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)) {
+ 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;
+ }
+ case MSG_START_RECORDING:
+ {
+ if (DEBUG) Log.d(TAG, "Start recording");
+ if (!doStartRecording((Uri) msg.obj)) {
+ reset();
+ }
+ return true;
+ }
+ case MSG_PREPARE_RECODER:
+ {
+ if (DEBUG) Log.d(TAG, "Preparing recorder");
+ if (!mRecorderRunning) {
+ return true;
+ }
+ try {
+ if (!mRecorder.prepare()) {
+ mHandler.sendEmptyMessageDelayed(
+ MSG_PREPARE_RECODER, PREPARE_RECORDER_POLL_MS);
+ }
+ } catch (IOException e) {
+ Log.w(TAG, "Failed to start recording. Couldn't prepare an extractor");
+ mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN);
+ reset();
+ }
+ return true;
+ }
+ case MSG_STOP_RECORDING:
+ {
+ if (DEBUG) Log.d(TAG, "Stop recording");
+ if (mSessionState != STATE_RECORDING) {
+ mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN);
+ reset();
+ return true;
+ }
+ if (mRecorderRunning) {
+ stopRecorder();
+ }
+ return true;
+ }
+ case MSG_MONITOR_STORAGE_STATUS:
+ {
+ if (mSessionState != STATE_RECORDING) {
+ return true;
+ }
+ if (!mDvrStorageStatusManager.isStorageSufficient()) {
+ if (mRecorderRunning) {
+ stopRecorder();
+ }
+ new DeleteRecordingTask().execute(mStorageDir);
+ mSession.onError(TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE);
+ reset();
+ } else {
+ mHandler.sendEmptyMessageDelayed(
+ MSG_MONITOR_STORAGE_STATUS, STORAGE_MONITOR_INTERVAL_MS);
+ }
+ return true;
+ }
+ case MSG_RELEASE:
+ {
+ // Since release was requested, current recording will be cancelled
+ // without notification.
+ reset();
+ mSourceManager.release();
+ mHandler.removeCallbacksAndMessages(null);
+ 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;
+ }
+
+ @Nullable
+ private TunerChannel getChannel(Uri channelUri) {
+ if (channelUri == null) {
+ return null;
+ }
+ long channelId;
+ try {
+ channelId = ContentUris.parseId(channelUri);
+ } catch (UnsupportedOperationException | NumberFormatException e) {
+ channelId = CHANNEL_ID_NONE;
+ }
+ return (channelId == CHANNEL_ID_NONE) ? null : mChannelDataManager.getChannel(channelId);
+ }
+
+ private String getStorageKey() {
+ long prefix = System.currentTimeMillis();
+ int suffix = mRandom.nextInt();
+ return String.format(Locale.ENGLISH, "%016x_%016x", prefix, suffix);
+ }
+
+ private void reset() {
+ if (mRecorder != null) {
+ mRecorder.release();
+ mRecorder = 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 && mSessionState != STATE_TUNING) {
+ mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN);
+ Log.e(TAG, "Tuning was requested from wrong status.");
+ return false;
+ }
+ mChannel = getChannel(channelUri);
+ if (mChannel == null) {
+ 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);
+ Log.w(TAG, "Tuning failed due to insufficient storage.");
+ return false;
+ }
+ mTunerSource = mSourceManager.createDataSource(mContext, mChannel, this);
+ if (mTunerSource == null) {
+ // Retry tuning in this case.
+ mSessionState = STATE_TUNING;
+ return true;
+ }
+ mSessionState = STATE_TUNED;
+ return true;
+ }
+
+ private boolean doStartRecording(@Nullable Uri programUri) {
+ if (mSessionState != STATE_TUNED) {
+ mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN);
+ Log.e(TAG, "Recording session status abnormal");
+ return false;
+ }
+ mStorageDir =
+ mDvrStorageStatusManager.isStorageSufficient()
+ ? new File(
+ mDvrStorageStatusManager.getRecordingRootDataDirectory(),
+ getStorageKey())
+ : null;
+ if (mStorageDir == null) {
+ mSession.onError(TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE);
+ Log.w(TAG, "Failed to start recording due to insufficient storage.");
+ return false;
+ }
+ // Since tuning might be happened a while ago, shifts the start position of tuned source.
+ mTunerSource.shiftStartPosition(mTunerSource.getBufferedPosition());
+ mRecordStartTime = System.currentTimeMillis();
+ 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;
+ mRecorderRunning = true;
+ mHandler.sendEmptyMessage(MSG_PREPARE_RECODER);
+ mHandler.removeMessages(MSG_MONITOR_STORAGE_STATUS);
+ mHandler.sendEmptyMessageDelayed(MSG_MONITOR_STORAGE_STATUS, STORAGE_MONITOR_INTERVAL_MS);
+ return true;
+ }
+
+ private void stopRecorder() {
+ // Do not change session status.
+ if (mRecorder != null) {
+ mRecorder.release();
+ mRecordEndTime = System.currentTimeMillis();
+ mRecorder = null;
+ }
+ mRecorderRunning = false;
+ mHandler.removeMessages(MSG_MONITOR_STORAGE_STATUS);
+ 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;
+ private String mSeriesId;
+ private final String mSeasonTitle;
+ private final String mEpisodeTitle;
+ private final String mSeasonNumber;
+ private final String mEpisodeNumber;
+ private final String mDescription;
+ private final String mPosterArtUri;
+ private final String mThumbnailUri;
+ private final String mCanonicalGenres;
+ private final String mContentRatings;
+ private final long mStartTimeUtcMillis;
+ private final long mEndTimeUtcMillis;
+ private final int mVideoWidth;
+ private final int mVideoHeight;
+ private final byte[] mInternalProviderData;
+
+ private static final String[] PROJECTION = {
+ TvContract.Programs.COLUMN_CHANNEL_ID,
+ TvContract.Programs.COLUMN_TITLE,
+ TvContract.Programs.COLUMN_SEASON_TITLE,
+ TvContract.Programs.COLUMN_EPISODE_TITLE,
+ TvContract.Programs.COLUMN_SEASON_DISPLAY_NUMBER,
+ TvContract.Programs.COLUMN_EPISODE_DISPLAY_NUMBER,
+ TvContract.Programs.COLUMN_SHORT_DESCRIPTION,
+ TvContract.Programs.COLUMN_POSTER_ART_URI,
+ TvContract.Programs.COLUMN_THUMBNAIL_URI,
+ TvContract.Programs.COLUMN_CANONICAL_GENRE,
+ TvContract.Programs.COLUMN_CONTENT_RATING,
+ TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS,
+ TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS,
+ TvContract.Programs.COLUMN_VIDEO_WIDTH,
+ TvContract.Programs.COLUMN_VIDEO_HEIGHT,
+ TvContract.Programs.COLUMN_INTERNAL_PROVIDER_DATA
+ };
+
+ public Program(Cursor cursor) {
+ int index = 0;
+ mChannelId = cursor.getLong(index++);
+ mTitle = cursor.getString(index++);
+ mSeasonTitle = cursor.getString(index++);
+ mEpisodeTitle = cursor.getString(index++);
+ mSeasonNumber = cursor.getString(index++);
+ mEpisodeNumber = cursor.getString(index++);
+ mDescription = cursor.getString(index++);
+ mPosterArtUri = cursor.getString(index++);
+ mThumbnailUri = cursor.getString(index++);
+ mCanonicalGenres = cursor.getString(index++);
+ mContentRatings = cursor.getString(index++);
+ mStartTimeUtcMillis = cursor.getLong(index++);
+ mEndTimeUtcMillis = cursor.getLong(index++);
+ mVideoWidth = cursor.getInt(index++);
+ mVideoHeight = cursor.getInt(index++);
+ mInternalProviderData = cursor.getBlob(index++);
+ SoftPreconditions.checkArgument(index == PROJECTION.length);
+ }
+
+ public Program(long channelId) {
+ mChannelId = channelId;
+ mTitle = "Unknown";
+ mSeasonTitle = "";
+ mEpisodeTitle = "";
+ mSeasonNumber = "";
+ mEpisodeNumber = "";
+ mDescription = "Unknown";
+ mPosterArtUri = null;
+ mThumbnailUri = null;
+ mCanonicalGenres = null;
+ mContentRatings = null;
+ mStartTimeUtcMillis = 0;
+ mEndTimeUtcMillis = 0;
+ mVideoWidth = 0;
+ mVideoHeight = 0;
+ mInternalProviderData = null;
+ }
+
+ public static Program onQuery(Cursor c) {
+ Program program = null;
+ if (c != null && c.moveToNext()) {
+ program = new Program(c);
+ }
+ return program;
+ }
+
+ public ContentValues buildValues() {
+ ContentValues values = new ContentValues();
+ int index = 0;
+ values.put(PROJECTION[index++], mChannelId);
+ values.put(PROJECTION[index++], mTitle);
+ values.put(PROJECTION[index++], mSeasonTitle);
+ values.put(PROJECTION[index++], mEpisodeTitle);
+ values.put(PROJECTION[index++], mSeasonNumber);
+ values.put(PROJECTION[index++], mEpisodeNumber);
+ values.put(PROJECTION[index++], mDescription);
+ values.put(PROJECTION[index++], mPosterArtUri);
+ values.put(PROJECTION[index++], mThumbnailUri);
+ values.put(PROJECTION[index++], mCanonicalGenres);
+ values.put(PROJECTION[index++], mContentRatings);
+ values.put(PROJECTION[index++], mStartTimeUtcMillis);
+ values.put(PROJECTION[index++], mEndTimeUtcMillis);
+ values.put(PROJECTION[index++], mVideoWidth);
+ values.put(PROJECTION[index++], mVideoHeight);
+ values.put(PROJECTION[index++], mInternalProviderData);
+ SoftPreconditions.checkArgument(index == PROJECTION.length);
+ return values;
+ }
+ }
+
+ private Program getRecordedProgram() {
+ ContentResolver resolver = mContext.getContentResolver();
+ Uri programUri = mProgramUri;
+ if (mProgramUri == null) {
+ long avg = mRecordStartTime / 2 + mRecordEndTime / 2;
+ programUri = TvContract.buildProgramsUriForChannel(mChannel.getChannelId(), avg, avg);
+ }
+ try (Cursor c = resolver.query(programUri, Program.PROJECTION, null, null, SORT_BY_TIME)) {
+ if (c != null) {
+ Program result = Program.onQuery(c);
+ if (DEBUG) {
+ Log.v(TAG, "Finished query for " + this);
+ }
+ return result;
+ } else {
+ if (c == null) {
+ Log.e(TAG, "Unknown query error for " + this);
+ } else {
+ if (DEBUG) Log.d(TAG, "Canceled query for " + this);
+ }
+ return null;
+ }
+ }
+ }
+
+ private Uri insertRecordedProgram(
+ Program program,
+ long channelId,
+ String storageUri,
+ long totalBytes,
+ long startTime,
+ long endTime) {
+ // TODO: Set title even though program is null.
+ RecordedProgram recordedProgram =
+ RecordedProgram.builder()
+ .setInputId(mInputId)
+ .setChannelId(channelId)
+ .setDataUri(storageUri)
+ .setDurationMillis(endTime - startTime)
+ .setDataBytes(totalBytes)
+ // startTime and endTime could be overridden by program's start and end
+ // value.
+ .setStartTimeUtcMillis(startTime)
+ .setEndTimeUtcMillis(endTime)
+ .build();
+ ContentValues values = RecordedProgram.toValues(recordedProgram);
+ if (program != null) {
+ values.putAll(program.buildValues());
+ }
+ return mContext.getContentResolver()
+ .insert(TvContract.RecordedPrograms.CONTENT_URI, values);
+ }
+
+ private void onRecordingResult(boolean success, long lastExtractedPositionUs) {
+ if (mSessionState != STATE_RECORDING) {
+ // Error notification is not needed.
+ Log.e(TAG, "Recording session status abnormal");
+ return;
+ }
+ if (mRecorderRunning) {
+ // In case of recorder not being stopped, because of premature termination of recording.
+ stopRecorder();
+ }
+ if (!success
+ && lastExtractedPositionUs
+ < TimeUnit.MILLISECONDS.toMicros(MIN_PARTIAL_RECORDING_DURATION_MS)) {
+ new DeleteRecordingTask().execute(mStorageDir);
+ mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN);
+ Log.w(TAG, "Recording failed during recording");
+ return;
+ }
+ Log.i(TAG, "recording finished " + (success ? "completely" : "partially"));
+ 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);
+ }
+
+ private static class DeleteRecordingTask extends AsyncTask<File, Void, Void> {
+
+ @Override
+ public Void doInBackground(File... files) {
+ if (files == null || files.length == 0) {
+ return null;
+ }
+ for (File file : files) {
+ Utils.deleteDirOrFile(file);
+ }
+ return null;
+ }
+ }
+}
diff --git a/src/com/android/tv/tuner/tvinput/TunerSession.java b/src/com/android/tv/tuner/tvinput/TunerSession.java
new file mode 100644
index 00000000..eec5da1f
--- /dev/null
+++ b/src/com/android/tv/tuner/tvinput/TunerSession.java
@@ -0,0 +1,341 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.tvinput;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.media.PlaybackParams;
+import android.media.tv.TvContentRating;
+import android.media.tv.TvInputManager;
+import android.media.tv.TvInputService;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Handler;
+import android.os.Message;
+import android.os.SystemClock;
+import android.text.Html;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.Surface;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+import android.widget.Toast;
+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.util.GlobalSettingsUtils;
+import com.android.tv.tuner.util.StatusTextUtils;
+import com.android.tv.tuner.util.SystemPropertiesProxy;
+import com.google.android.exoplayer.audio.AudioCapabilities;
+
+/**
+ * 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, 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";
+
+ public static final int MSG_UI_SHOW_MESSAGE = 1;
+ public static final int MSG_UI_HIDE_MESSAGE = 2;
+ public static final int MSG_UI_SHOW_AUDIO_UNPLAYABLE = 3;
+ public static final int MSG_UI_HIDE_AUDIO_UNPLAYABLE = 4;
+ public static final int MSG_UI_PROCESS_CAPTION_TRACK = 5;
+ 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_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;
+ private final View mOverlayView;
+ private final TextView mMessageView;
+ private final TextView mStatusView;
+ private final TextView mAudioStatusView;
+ private final ViewGroup mMessageLayout;
+ private final CaptionTrackRenderer mCaptionTrackRenderer;
+ private final TunerSessionWorker mSessionWorker;
+ private boolean mReleased = false;
+ private boolean mPlayPaused;
+ private long mTuneStartTimestamp;
+
+ public TunerSession(Context context, ChannelDataManager channelDataManager) {
+ super(context);
+ mContext = context;
+ mUiHandler = new Handler(this);
+ LayoutInflater inflater =
+ (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ mOverlayView = inflater.inflate(R.layout.ut_overlay_view, null);
+ mMessageLayout = (ViewGroup) mOverlayView.findViewById(R.id.message_layout);
+ mMessageLayout.setVisibility(View.INVISIBLE);
+ mMessageView = (TextView) mOverlayView.findViewById(R.id.message);
+ mStatusView = (TextView) mOverlayView.findViewById(R.id.tuner_status);
+ boolean showDebug = SystemPropertiesProxy.getBoolean(USBTUNER_SHOW_DEBUG, false);
+ mStatusView.setVisibility(showDebug ? View.VISIBLE : View.INVISIBLE);
+ mAudioStatusView = (TextView) mOverlayView.findViewById(R.id.audio_status);
+ mAudioStatusView.setVisibility(View.INVISIBLE);
+ CaptionLayout captionLayout = (CaptionLayout) mOverlayView.findViewById(R.id.caption);
+ mCaptionTrackRenderer = new CaptionTrackRenderer(captionLayout);
+ mSessionWorker = new TunerSessionWorker(context, channelDataManager, this);
+ TunerPreferences.setTunerPreferencesChangedListener(this);
+ }
+
+ public boolean isReleased() {
+ return mReleased;
+ }
+
+ @Override
+ public View onCreateOverlayView() {
+ return mOverlayView;
+ }
+
+ @Override
+ public boolean onSelectTrack(int type, String trackId) {
+ mSessionWorker.sendMessage(TunerSessionWorker.MSG_SELECT_TRACK, type, 0, trackId);
+ return false;
+ }
+
+ @Override
+ public void onSetCaptionEnabled(boolean enabled) {
+ mSessionWorker.setCaptionEnabled(enabled);
+ }
+
+ @Override
+ public void onSetStreamVolume(float volume) {
+ mSessionWorker.setStreamVolume(volume);
+ }
+
+ @Override
+ public boolean onSetSurface(Surface surface) {
+ mSessionWorker.setSurface(surface);
+ return true;
+ }
+
+ @Override
+ public void onTimeShiftPause() {
+ mSessionWorker.sendMessage(TunerSessionWorker.MSG_TIMESHIFT_PAUSE);
+ mPlayPaused = true;
+ }
+
+ @Override
+ public void onTimeShiftResume() {
+ mSessionWorker.sendMessage(TunerSessionWorker.MSG_TIMESHIFT_RESUME);
+ mPlayPaused = false;
+ }
+
+ @Override
+ public void onTimeShiftSeekTo(long timeMs) {
+ if (DEBUG) Log.d(TAG, "Timeshift seekTo requested position: " + timeMs / 1000);
+ mSessionWorker.sendMessage(
+ TunerSessionWorker.MSG_TIMESHIFT_SEEK_TO, mPlayPaused ? 1 : 0, 0, timeMs);
+ }
+
+ @Override
+ public void onTimeShiftSetPlaybackParams(PlaybackParams params) {
+ mSessionWorker.sendMessage(TunerSessionWorker.MSG_TIMESHIFT_SET_PLAYBACKPARAMS, params);
+ }
+
+ @Override
+ public long onTimeShiftGetStartPosition() {
+ return mSessionWorker.getStartPosition();
+ }
+
+ @Override
+ public long onTimeShiftGetCurrentPosition() {
+ return mSessionWorker.getCurrentPosition();
+ }
+
+ @Override
+ public boolean onTune(Uri channelUri) {
+ if (DEBUG) {
+ Log.d(TAG, "onTune to " + channelUri != null ? channelUri.toString() : "");
+ }
+ if (channelUri == null) {
+ Log.w(TAG, "onTune() is failed due to null channelUri.");
+ mSessionWorker.stopTune();
+ return false;
+ }
+ mTuneStartTimestamp = SystemClock.elapsedRealtime();
+ mSessionWorker.tune(channelUri);
+ mPlayPaused = false;
+ return true;
+ }
+
+ @TargetApi(Build.VERSION_CODES.N)
+ @Override
+ public void onTimeShiftPlay(Uri recordUri) {
+ if (recordUri == null) {
+ Log.w(TAG, "onTimeShiftPlay() is failed due to null channelUri.");
+ mSessionWorker.stopTune();
+ return;
+ }
+ mTuneStartTimestamp = SystemClock.elapsedRealtime();
+ mSessionWorker.tune(recordUri);
+ mPlayPaused = false;
+ }
+
+ @Override
+ public void onUnblockContent(TvContentRating unblockedRating) {
+ mSessionWorker.sendMessage(TunerSessionWorker.MSG_UNBLOCKED_RATING, unblockedRating);
+ }
+
+ @Override
+ public void onRelease() {
+ if (DEBUG) {
+ Log.d(TAG, "onRelease");
+ }
+ mReleased = true;
+ mSessionWorker.release();
+ mUiHandler.removeCallbacksAndMessages(null);
+ TunerPreferences.setTunerPreferencesChangedListener(null);
+ }
+
+ /** Sets {@link AudioCapabilities}. */
+ public void setAudioCapabilities(AudioCapabilities audioCapabilities) {
+ mSessionWorker.sendMessage(
+ TunerSessionWorker.MSG_AUDIO_CAPABILITIES_CHANGED, audioCapabilities);
+ }
+
+ @Override
+ public void notifyVideoAvailable() {
+ super.notifyVideoAvailable();
+ if (mTuneStartTimestamp != 0) {
+ Log.i(
+ TAG,
+ "[Profiler] Video available in "
+ + (SystemClock.elapsedRealtime() - mTuneStartTimestamp)
+ + " ms");
+ mTuneStartTimestamp = 0;
+ }
+ }
+
+ @Override
+ public void notifyVideoUnavailable(int reason) {
+ super.notifyVideoUnavailable(reason);
+ if (reason != TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING
+ && reason != TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL) {
+ notifyTimeShiftStatusChanged(TvInputManager.TIME_SHIFT_STATUS_UNAVAILABLE);
+ }
+ }
+
+ public void sendUiMessage(int message) {
+ mUiHandler.sendEmptyMessage(message);
+ }
+
+ public void sendUiMessage(int message, Object object) {
+ mUiHandler.obtainMessage(message, object).sendToTarget();
+ }
+
+ public void sendUiMessage(int message, int arg1, int arg2, Object object) {
+ mUiHandler.obtainMessage(message, arg1, arg2, object).sendToTarget();
+ }
+
+ @Override
+ public boolean handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_UI_SHOW_MESSAGE:
+ {
+ mMessageView.setText((String) msg.obj);
+ mMessageLayout.setVisibility(View.VISIBLE);
+ return true;
+ }
+ case MSG_UI_HIDE_MESSAGE:
+ {
+ mMessageLayout.setVisibility(View.INVISIBLE);
+ return true;
+ }
+ case MSG_UI_SHOW_AUDIO_UNPLAYABLE:
+ {
+ // Showing message of enabling surround sound only when global surround sound
+ // setting is "never".
+ final int value =
+ GlobalSettingsUtils.getEncodedSurroundOutputSettings(mContext);
+ if (value == GlobalSettingsUtils.ENCODED_SURROUND_OUTPUT_NEVER) {
+ mAudioStatusView.setText(
+ Html.fromHtml(
+ StatusTextUtils.getAudioWarningInHTML(
+ mContext.getString(
+ R.string.ut_surround_sound_disabled))));
+ } else {
+ 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:
+ {
+ mAudioStatusView.setVisibility(View.INVISIBLE);
+ return true;
+ }
+ case MSG_UI_PROCESS_CAPTION_TRACK:
+ {
+ mCaptionTrackRenderer.processCaptionEvent((CaptionEvent) msg.obj);
+ return true;
+ }
+ case MSG_UI_START_CAPTION_TRACK:
+ {
+ mCaptionTrackRenderer.start((AtscCaptionTrack) msg.obj);
+ return true;
+ }
+ case MSG_UI_STOP_CAPTION_TRACK:
+ {
+ mCaptionTrackRenderer.stop();
+ return true;
+ }
+ case MSG_UI_RESET_CAPTION_TRACK:
+ {
+ 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;
+ }
+ case MSG_UI_TOAST_RESCAN_NEEDED:
+ {
+ Toast.makeText(mContext, R.string.ut_rescan_needed, Toast.LENGTH_LONG).show();
+ return true;
+ }
+ }
+ 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
new file mode 100644
index 00000000..7a0897e2
--- /dev/null
+++ b/src/com/android/tv/tuner/tvinput/TunerSessionWorker.java
@@ -0,0 +1,1852 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.tvinput;
+
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.database.Cursor;
+import android.media.MediaFormat;
+import android.media.PlaybackParams;
+import android.media.tv.TvContentRating;
+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;
+import android.os.SystemClock;
+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;
+import android.view.Surface;
+import android.view.accessibility.CaptioningManager;
+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;
+import com.android.tv.tuner.data.TunerChannel;
+import com.android.tv.tuner.data.nano.Channel;
+import com.android.tv.tuner.data.nano.Track.AtscAudioTrack;
+import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack;
+import com.android.tv.tuner.exoplayer.MpegTsPlayer;
+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.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 com.google.android.exoplayer.ExoPlayer;
+import com.google.android.exoplayer.audio.AudioCapabilities;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * {@link TunerSessionWorker} implements a handler thread which processes TV input jobs such as
+ * handling {@link ExoPlayer}, managing a tuner device, trickplay, and so on.
+ */
+@WorkerThread
+public class TunerSessionWorker
+ implements PlaybackBufferListener,
+ MpegTsPlayer.VideoEventListener,
+ MpegTsPlayer.Listener,
+ EventDetector.EventListener,
+ ChannelDataManager.ProgramInfoListener,
+ Handler.Callback {
+ private static final String TAG = "TunerSessionWorker";
+ 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;
+ public static final int MSG_UPDATE_CAPTION_TRACK = 2;
+ public static final int MSG_SET_STREAM_VOLUME = 3;
+ public static final int MSG_TIMESHIFT_PAUSE = 4;
+ public static final int MSG_TIMESHIFT_RESUME = 5;
+ public static final int MSG_TIMESHIFT_SEEK_TO = 6;
+ 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;
+ private static final int MSG_RELEASE = 1001;
+ private static final int MSG_RETRY_PLAYBACK = 1002;
+ private static final int MSG_START_PLAYBACK = 1003;
+ private static final int MSG_UPDATE_PROGRAM = 1008;
+ private static final int MSG_SCHEDULE_OF_PROGRAMS = 1009;
+ private static final int MSG_UPDATE_CHANNEL_INFO = 1010;
+ private static final int MSG_TRICKPLAY_BY_SEEK = 1011;
+ private static final int MSG_SMOOTH_TRICKPLAY_MONITOR = 1012;
+ private static final int MSG_PARENTAL_CONTROLS = 1015;
+ private static final int MSG_RESCHEDULE_PROGRAMS = 1016;
+ private static final int MSG_BUFFER_START_TIME_CHANGED = 1017;
+ private static final int MSG_CHECK_SIGNAL = 1018;
+ private static final int MSG_DISCOVER_CAPTION_SERVICE_NUMBER = 1019;
+ private static final int MSG_RESET_PLAYBACK = 1020;
+ private static final int MSG_BUFFER_STATE_CHANGED = 1021;
+ private static final int MSG_PROGRAM_DATA_RESULT = 1022;
+ private static final int MSG_STOP_TUNE = 1023;
+ private static final int MSG_SET_SURFACE = 1024;
+ private static final int MSG_NOTIFY_AUDIO_TRACK_UPDATED = 1025;
+
+ private static final int TS_PACKET_SIZE = 188;
+ private static final int CHECK_NO_SIGNAL_INITIAL_DELAY_MS = 4000;
+ private static final int CHECK_NO_SIGNAL_PERIOD_MS = 500;
+ private static final int RECOVER_STOPPED_PLAYBACK_PERIOD_MS = 2500;
+ private static final int PARENTAL_CONTROLS_INTERVAL_MS = 5000;
+ private static final int RESCHEDULE_PROGRAMS_INITIAL_DELAY_MS = 4000;
+ private static final int RESCHEDULE_PROGRAMS_INTERVAL_MS = 10000;
+ private static final int RESCHEDULE_PROGRAMS_TOLERANCE_MS = 2000;
+ // The following 3s is defined empirically. This should be larger than 2s considering video
+ // key frame interval in the TS stream.
+ private static final int PLAYBACK_STATE_CHANGED_WAITING_THRESHOLD_MS = 3000;
+ private static final int PLAYBACK_RETRY_DELAY_MS = 5000;
+ private static final int MAX_IMMEDIATE_RETRY_COUNT = 5;
+ private static final long INVALID_TIME = -1;
+
+ // Some examples of the track ids of the audio tracks, "a0", "a1", "a2".
+ // The number after prefix is being used for indicating a index of the given audio track.
+ private static final String AUDIO_TRACK_PREFIX = "a";
+
+ // Some examples of the tracks id of the caption tracks, "s1", "s2", "s3".
+ // The number after prefix is being used for indicating a index of a caption service number
+ // of the given caption track.
+ private static final String SUBTITLE_TRACK_PREFIX = "s";
+ private static final int TRACK_PREFIX_SIZE = 1;
+ private static final String VIDEO_TRACK_ID = "v";
+ private static final long BUFFER_UNDERFLOW_BUFFER_MS = 5000;
+
+ // Actual interval would be divided by the speed.
+ 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;
+ private volatile MpegTsPlayer mPlayer;
+ private volatile TunerChannel mChannel;
+ 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;
+ private final ArrayList<TvTrackInfo> mTvTracks;
+ private final SparseArray<AtscAudioTrack> mAudioTrackMap;
+ private final SparseArray<AtscCaptionTrack> mCaptionTrackMap;
+ private AtscCaptionTrack mCaptionTrack;
+ private PlaybackParams mPlaybackParams = new PlaybackParams();
+ private boolean mPlayerStarted = false;
+ private boolean mReportedDrawnToSurface = false;
+ private boolean mReportedWeakSignal = false;
+ private EitItem mProgram;
+ private List<EitItem> mPrograms;
+ private final TvInputManager mTvInputManager;
+ private boolean mChannelBlocked;
+ private TvContentRating mUnblockedContentRating;
+ private long mLastPositionMs;
+ private AudioCapabilities mAudioCapabilities;
+ private long mLastLimitInBytes;
+ 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, TunerSession tunerSession) {
+ if (DEBUG) Log.d(TAG, "TunerSessionWorker created");
+ mContext = context;
+
+ // HandlerThread should be set up before it is registered as a listener in the all other
+ // components.
+ HandlerThread handlerThread = new HandlerThread(TAG);
+ handlerThread.start();
+ mHandler = new Handler(handlerThread.getLooper(), this);
+ mSession = tunerSession;
+ mChannelDataManager = channelDataManager;
+ mChannelDataManager.setListener(this);
+ mChannelDataManager.checkDataVersion(mContext);
+ mSourceManager = TsDataSourceManager.createSourceManager(false);
+ mTvInputManager = (TvInputManager) context.getSystemService(Context.TV_INPUT_SERVICE);
+ mTvTracks = new ArrayList<>();
+ mAudioTrackMap = new SparseArray<>();
+ mCaptionTrackMap = new SparseArray<>();
+ CaptioningManager captioningManager =
+ (CaptioningManager) context.getSystemService(Context.CAPTIONING_SERVICE);
+ mCaptionEnabled = captioningManager.isEnabled();
+ mPlaybackParams.setSpeed(1.0f);
+ 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
+ @MainThread
+ public void tune(Uri channelUri) {
+ mHandler.removeCallbacksAndMessages(null);
+ mSourceManager.setHasPendingTune();
+ sendMessage(MSG_TUNE, channelUri);
+ }
+
+ @MainThread
+ public void stopTune() {
+ mHandler.removeCallbacksAndMessages(null);
+ sendMessage(MSG_STOP_TUNE);
+ }
+
+ /** Sets {@link Surface}. */
+ @MainThread
+ public void setSurface(Surface surface) {
+ if (surface != null && !surface.isValid()) {
+ Log.w(TAG, "Ignoring invalid surface.");
+ return;
+ }
+ // mSurface is kept even when tune is called right after. But, messages can be deleted by
+ // tune or updateChannelBlockStatus. So mSurface should be stored here, not through message.
+ mSurface = surface;
+ mHandler.sendEmptyMessage(MSG_SET_SURFACE);
+ }
+
+ /** Sets volume. */
+ @MainThread
+ public void setStreamVolume(float volume) {
+ // mVolume is kept even when tune is called right after. But, messages can be deleted by
+ // tune or updateChannelBlockStatus. So mVolume is stored here and mPlayer.setVolume will be
+ // called in MSG_SET_STREAM_VOLUME.
+ mVolume = volume;
+ mHandler.sendEmptyMessage(MSG_SET_STREAM_VOLUME);
+ }
+
+ /** Sets if caption is enabled or disabled. */
+ @MainThread
+ public void setCaptionEnabled(boolean captionEnabled) {
+ // mCaptionEnabled is kept even when tune is called right after. But, messages can be
+ // deleted by tune or updateChannelBlockStatus. So mCaptionEnabled is stored here and
+ // start/stopCaptionTrack will be called in MSG_UPDATE_CAPTION_STATUS.
+ mCaptionEnabled = captionEnabled;
+ mHandler.sendEmptyMessage(MSG_UPDATE_CAPTION_TRACK);
+ }
+
+ public TunerChannel getCurrentChannel() {
+ return mChannel;
+ }
+
+ @MainThread
+ public long getStartPosition() {
+ return mBufferStartTimeMs;
+ }
+
+ private String getRecordingPath() {
+ return Uri.parse(mRecordingId).getPath();
+ }
+
+ private Long getDurationForRecording(String recordingId) {
+ DvrStorageManager storageManager =
+ new DvrStorageManager(new File(getRecordingPath()), false);
+ 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;
+ }
+ Log.e(TAG, "meta file for recording was not found: " + recordingId);
+ return null;
+ }
+
+ @MainThread
+ public long getCurrentPosition() {
+ // TODO: More precise time may be necessary.
+ MpegTsPlayer mpegTsPlayer = mPlayer;
+ long currentTime =
+ mpegTsPlayer != null
+ ? mRecordStartTimeMs + mpegTsPlayer.getCurrentPosition()
+ : mRecordStartTimeMs;
+ if (mChannel == null && mPlayerState == ExoPlayer.STATE_ENDED) {
+ currentTime = mRecordingDuration + mRecordStartTimeMs;
+ }
+ if (DEBUG) {
+ long systemCurrentTime = System.currentTimeMillis();
+ Log.d(
+ TAG,
+ "currentTime = "
+ + currentTime
+ + " ; System.currentTimeMillis() = "
+ + systemCurrentTime
+ + " ; diff = "
+ + (currentTime - systemCurrentTime));
+ }
+ return currentTime;
+ }
+
+ @AnyThread
+ public void sendMessage(int messageType) {
+ mHandler.sendEmptyMessage(messageType);
+ }
+
+ @AnyThread
+ public void sendMessage(int messageType, Object object) {
+ mHandler.obtainMessage(messageType, object).sendToTarget();
+ }
+
+ @AnyThread
+ public void sendMessage(int messageType, int arg1, int arg2, Object object) {
+ mHandler.obtainMessage(messageType, arg1, arg2, object).sendToTarget();
+ }
+
+ @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);
+ }
+
+ // MpegTsPlayer.Listener
+ // Called in the same thread as mHandler.
+ @Override
+ public void onStateChanged(boolean playWhenReady, int playbackState) {
+ if (DEBUG) Log.d(TAG, "ExoPlayer state change: " + playbackState + " " + playWhenReady);
+ if (playbackState == mPlayerState) {
+ return;
+ }
+ mReadyStartTimeMs = INVALID_TIME;
+ mPreparingStartTimeMs = INVALID_TIME;
+ mBufferingStartTimeMs = INVALID_TIME;
+ if (playbackState == ExoPlayer.STATE_READY) {
+ if (DEBUG) Log.d(TAG, "ExoPlayer ready");
+ if (!mPlayerStarted) {
+ sendMessage(MSG_START_PLAYBACK, System.identityHashCode(mPlayer));
+ }
+ mReadyStartTimeMs = SystemClock.elapsedRealtime();
+ } else if (playbackState == ExoPlayer.STATE_PREPARING) {
+ mPreparingStartTimeMs = SystemClock.elapsedRealtime();
+ } else if (playbackState == ExoPlayer.STATE_BUFFERING) {
+ mBufferingStartTimeMs = SystemClock.elapsedRealtime();
+ } else if (playbackState == ExoPlayer.STATE_ENDED) {
+ // Final status
+ // 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, System.identityHashCode(mPlayer));
+ }
+ }
+ mPlayerState = playbackState;
+ }
+
+ @Override
+ public void onError(Exception e) {
+ if (TunerPreferences.getStoreTsStream(mContext)) {
+ // Crash intentionally to capture the error causing TS file.
+ Log.e(
+ TAG,
+ "Crash intentionally to capture the error causing TS file. " + e.getMessage());
+ SoftPreconditions.checkState(false);
+ }
+ // There maybe some errors that finally raise ExoPlaybackException and will be handled here.
+ // If we are playing live stream, retrying playback maybe helpful. But for recorded stream,
+ // retrying playback is not helpful.
+ if (mChannel != null) {
+ mHandler.obtainMessage(MSG_RETRY_PLAYBACK, System.identityHashCode(mPlayer))
+ .sendToTarget();
+ }
+ }
+
+ @Override
+ public void onVideoSizeChanged(int width, int height, float pixelWidthHeight) {
+ if (mChannel != null && mChannel.hasVideo()) {
+ updateVideoTrack(width, height);
+ }
+ if (mRecordingId != null) {
+ updateVideoTrack(width, height);
+ }
+ }
+
+ @Override
+ public void onDrawnToSurface(MpegTsPlayer player, Surface surface) {
+ if (mSurface != null && mPlayerStarted) {
+ if (DEBUG) Log.d(TAG, "MSG_DRAWN_TO_SURFACE");
+ 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;
+
+ // If surface is drawn successfully, it means that the playback was brought back
+ // to normal and therefore, the playback recovery status will be reset through
+ // setting a zero value to the retry count.
+ // TODO: Consider audio only channels for detecting playback status changes to
+ // be normal.
+ mRetryCount = 0;
+ if (mCaptionEnabled && mCaptionTrack != null) {
+ startCaptionTrack();
+ } else {
+ stopCaptionTrack();
+ }
+ mHandler.sendEmptyMessage(MSG_NOTIFY_AUDIO_TRACK_UPDATED);
+ }
+ }
+
+ @Override
+ public void onSmoothTrickplayForceStopped() {
+ if (mPlayer == null || !mHandler.hasMessages(MSG_SMOOTH_TRICKPLAY_MONITOR)) {
+ return;
+ }
+ mHandler.removeMessages(MSG_SMOOTH_TRICKPLAY_MONITOR);
+ doTrickplayBySeek((int) mPlayer.getCurrentPosition());
+ }
+
+ @Override
+ public void onAudioUnplayable() {
+ if (mPlayer == null) {
+ return;
+ }
+ Log.i(TAG, "AC3 audio cannot be played due to device limitation");
+ mSession.sendUiMessage(TunerSession.MSG_UI_SHOW_AUDIO_UNPLAYABLE);
+ }
+
+ // MpegTsPlayer.VideoEventListener
+ @Override
+ public void onEmitCaptionEvent(Cea708Data.CaptionEvent event) {
+ mSession.sendUiMessage(TunerSession.MSG_UI_PROCESS_CAPTION_TRACK, event);
+ }
+
+ @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);
+ }
+
+ // ChannelDataManager.ProgramInfoListener
+ @Override
+ public void onProgramsArrived(TunerChannel channel, List<EitItem> programs) {
+ sendMessage(MSG_SCHEDULE_OF_PROGRAMS, new Pair<>(channel, programs));
+ }
+
+ @Override
+ public void onChannelArrived(TunerChannel channel) {
+ sendMessage(MSG_UPDATE_CHANNEL_INFO, channel);
+ }
+
+ @Override
+ public void onRescanNeeded() {
+ mSession.sendUiMessage(TunerSession.MSG_UI_TOAST_RESCAN_NEEDED);
+ }
+
+ @Override
+ public void onRequestProgramsResponse(TunerChannel channel, List<EitItem> programs) {
+ sendMessage(MSG_PROGRAM_DATA_RESULT, new Pair<>(channel, programs));
+ }
+
+ // PlaybackBufferListener
+ @Override
+ public void onBufferStartTimeChanged(long startTimeMs) {
+ sendMessage(MSG_BUFFER_START_TIME_CHANGED, startTimeMs);
+ }
+
+ @Override
+ public void onBufferStateChanged(boolean available) {
+ sendMessage(MSG_BUFFER_STATE_CHANGED, available);
+ }
+
+ @Override
+ public void onDiskTooSlow() {
+ mTrickplayDisabledByStorageIssue = true;
+ sendMessage(MSG_RETRY_PLAYBACK, System.identityHashCode(mPlayer));
+ }
+
+ // EventDetector.EventListener
+ @Override
+ public void onChannelDetected(TunerChannel channel, boolean channelArrivedAtFirstTime) {
+ mChannelDataManager.notifyChannelDetected(channel, channelArrivedAtFirstTime);
+ }
+
+ @Override
+ public void onEventDetected(TunerChannel channel, List<EitItem> items) {
+ mChannelDataManager.notifyEventDetected(channel, items);
+ }
+
+ @Override
+ public void onChannelScanDone() {
+ // do nothing.
+ }
+
+ private long parseChannel(Uri uri) {
+ try {
+ List<String> paths = uri.getPathSegments();
+ if (paths.size() > 1 && paths.get(0).equals(PLAY_FROM_CHANNEL)) {
+ return ContentUris.parseId(uri);
+ }
+ } catch (UnsupportedOperationException | NumberFormatException e) {
+ }
+ return -1;
+ }
+
+ private static class RecordedProgram {
+ private final long mChannelId;
+ private final String mDataUri;
+
+ private static final String[] PROJECTION = {
+ TvContract.Programs.COLUMN_CHANNEL_ID,
+ TvContract.RecordedPrograms.COLUMN_RECORDING_DATA_URI,
+ };
+
+ public RecordedProgram(Cursor cursor) {
+ int index = 0;
+ mChannelId = cursor.getLong(index++);
+ mDataUri = cursor.getString(index++);
+ }
+
+ public RecordedProgram(long channelId, String dataUri) {
+ mChannelId = channelId;
+ mDataUri = dataUri;
+ }
+
+ public static RecordedProgram onQuery(Cursor c) {
+ RecordedProgram recording = null;
+ if (c != null && c.moveToNext()) {
+ recording = new RecordedProgram(c);
+ }
+ return recording;
+ }
+
+ public String getDataUri() {
+ return mDataUri;
+ }
+ }
+
+ private RecordedProgram getRecordedProgram(Uri recordedUri) {
+ ContentResolver resolver = mContext.getContentResolver();
+ try (Cursor c = resolver.query(recordedUri, RecordedProgram.PROJECTION, null, null, null)) {
+ if (c != null) {
+ RecordedProgram result = RecordedProgram.onQuery(c);
+ if (DEBUG) {
+ Log.d(TAG, "Finished query for " + this);
+ }
+ return result;
+ } else {
+ if (c == null) {
+ Log.e(TAG, "Unknown query error for " + this);
+ } else {
+ if (DEBUG) Log.d(TAG, "Canceled query for " + this);
+ }
+ return null;
+ }
+ }
+ }
+
+ private String parseRecording(Uri uri) {
+ RecordedProgram recording = getRecordedProgram(uri);
+ if (recording != null) {
+ return recording.getDataUri();
+ }
+ return null;
+ }
+
+ @Override
+ public boolean handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_TUNE:
+ {
+ if (DEBUG) Log.d(TAG, "MSG_TUNE");
+
+ // When sequential tuning messages arrived, it skips middle tuning messages in
+ // order
+ // to change to the last requested channel quickly.
+ if (mHandler.hasMessages(MSG_TUNE)) {
+ 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);
+ TunerChannel channel =
+ (channelId == -1) ? null : mChannelDataManager.getChannel(channelId);
+ if (channelId == -1) {
+ recording = parseRecording(channelUri);
+ }
+ if (channel == null && recording == null) {
+ Log.w(TAG, "onTune() is failed. Can't find channel for " + channelUri);
+ stopTune();
+ notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN);
+ return true;
+ }
+ clearCallbacksAndMessagesSafely();
+ mChannelDataManager.removeAllCallbacksAndMessages();
+ if (channel != null) {
+ mChannelDataManager.requestProgramsData(channel);
+ }
+ prepareTune(channel, recording);
+ // TODO: Need to refactor. notifyContentAllowed() should not be called if
+ // parental
+ // control is turned on.
+ mSession.notifyContentAllowed();
+ resetTvTracks();
+ resetPlayback();
+ mHandler.sendEmptyMessageDelayed(
+ MSG_RESCHEDULE_PROGRAMS, RESCHEDULE_PROGRAMS_INITIAL_DELAY_MS);
+ return true;
+ }
+ case MSG_STOP_TUNE:
+ {
+ if (DEBUG) Log.d(TAG, "MSG_STOP_TUNE");
+ mChannel = null;
+ stopPlayback(true);
+ stopCaptionTrack();
+ resetTvTracks();
+ notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN);
+ return true;
+ }
+ case MSG_RELEASE:
+ {
+ if (DEBUG) Log.d(TAG, "MSG_RELEASE");
+ mHandler.removeCallbacksAndMessages(null);
+ stopPlayback(true);
+ stopCaptionTrack();
+ mSourceManager.release();
+ mHandler.getLooper().quitSafely();
+ if (mIsActiveSession) {
+ sActiveSessionSemaphore.release();
+ }
+ return true;
+ }
+ case MSG_RETRY_PLAYBACK:
+ {
+ 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.
+ mSourceManager.setKeepTuneStatus(false);
+ mRetryCount++;
+ 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(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.
+ mHandler.sendEmptyMessageDelayed(
+ MSG_RESET_PLAYBACK, RECOVER_STOPPED_PLAYBACK_PERIOD_MS);
+ }
+ }
+ return true;
+ }
+ 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((int) msg.obj);
+ }
+ return true;
+ }
+ case MSG_UPDATE_PROGRAM:
+ {
+ if (mChannel != null) {
+ EitItem program = (EitItem) msg.obj;
+ updateTvTracks(program, false);
+ mHandler.sendEmptyMessage(MSG_PARENTAL_CONTROLS);
+ }
+ return true;
+ }
+ case MSG_SCHEDULE_OF_PROGRAMS:
+ {
+ mHandler.removeMessages(MSG_UPDATE_PROGRAM);
+ Pair<TunerChannel, List<EitItem>> pair =
+ (Pair<TunerChannel, List<EitItem>>) msg.obj;
+ TunerChannel channel = pair.first;
+ if (mChannel == null) {
+ return true;
+ }
+ if (mChannel != null && mChannel.compareTo(channel) != 0) {
+ return true;
+ }
+ mPrograms = pair.second;
+ EitItem currentProgram = getCurrentProgram();
+ if (currentProgram == null) {
+ mProgram = null;
+ }
+ long currentTimeMs = getCurrentPosition();
+ if (mPrograms != null) {
+ for (EitItem item : mPrograms) {
+ if (currentProgram != null && currentProgram.compareTo(item) == 0) {
+ if (DEBUG) {
+ Log.d(TAG, "Update current TvTracks " + item);
+ }
+ if (mProgram != null && mProgram.compareTo(item) == 0) {
+ continue;
+ }
+ mProgram = item;
+ updateTvTracks(item, false);
+ } else if (item.getStartTimeUtcMillis() > currentTimeMs) {
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "Update next TvTracks "
+ + item
+ + " "
+ + (item.getStartTimeUtcMillis()
+ - currentTimeMs));
+ }
+ mHandler.sendMessageDelayed(
+ mHandler.obtainMessage(MSG_UPDATE_PROGRAM, item),
+ item.getStartTimeUtcMillis() - currentTimeMs);
+ }
+ }
+ }
+ mHandler.sendEmptyMessage(MSG_PARENTAL_CONTROLS);
+ return true;
+ }
+ case MSG_UPDATE_CHANNEL_INFO:
+ {
+ TunerChannel channel = (TunerChannel) msg.obj;
+ if (mChannel != null && mChannel.compareTo(channel) == 0) {
+ updateChannelInfo(channel);
+ }
+ return true;
+ }
+ case MSG_PROGRAM_DATA_RESULT:
+ {
+ TunerChannel channel = (TunerChannel) ((Pair) msg.obj).first;
+
+ // If there already exists, skip it since real-time data is a top priority,
+ if (mChannel != null
+ && mChannel.compareTo(channel) == 0
+ && mPrograms == null
+ && mProgram == null) {
+ sendMessage(MSG_SCHEDULE_OF_PROGRAMS, msg.obj);
+ }
+ return true;
+ }
+ case MSG_TRICKPLAY_BY_SEEK:
+ {
+ if (mPlayer == null) {
+ return true;
+ }
+ doTrickplayBySeek(msg.arg1);
+ return true;
+ }
+ case MSG_SMOOTH_TRICKPLAY_MONITOR:
+ {
+ if (mPlayer == null) {
+ return true;
+ }
+ long systemCurrentTime = System.currentTimeMillis();
+ long position = getCurrentPosition();
+ if (mRecordingId == null) {
+ // Checks if the position exceeds the upper bound when forwarding,
+ // or exceed the lower bound when rewinding.
+ // If the direction is not checked, there can be some issues.
+ // (See b/29939781 for more details.)
+ if ((position > systemCurrentTime && mPlaybackParams.getSpeed() > 0L)
+ || (position < mBufferStartTimeMs
+ && mPlaybackParams.getSpeed() < 0L)) {
+ doTimeShiftResume();
+ return true;
+ }
+ } else {
+ if (position > mRecordingDuration || position < 0) {
+ doTimeShiftPause();
+ return true;
+ }
+ }
+ mHandler.sendEmptyMessageDelayed(
+ MSG_SMOOTH_TRICKPLAY_MONITOR, TRICKPLAY_MONITOR_INTERVAL_MS);
+ return true;
+ }
+ case MSG_RESCHEDULE_PROGRAMS:
+ {
+ if (mHandler.hasMessages(MSG_SCHEDULE_OF_PROGRAMS)) {
+ mHandler.sendEmptyMessage(MSG_RESCHEDULE_PROGRAMS);
+ } else {
+ doReschedulePrograms();
+ }
+ return true;
+ }
+ case MSG_PARENTAL_CONTROLS:
+ {
+ doParentalControls();
+ mHandler.removeMessages(MSG_PARENTAL_CONTROLS);
+ mHandler.sendEmptyMessageDelayed(
+ MSG_PARENTAL_CONTROLS, PARENTAL_CONTROLS_INTERVAL_MS);
+ return true;
+ }
+ case MSG_UNBLOCKED_RATING:
+ {
+ mUnblockedContentRating = (TvContentRating) msg.obj;
+ doParentalControls();
+ mHandler.removeMessages(MSG_PARENTAL_CONTROLS);
+ mHandler.sendEmptyMessageDelayed(
+ MSG_PARENTAL_CONTROLS, PARENTAL_CONTROLS_INTERVAL_MS);
+ return true;
+ }
+ case MSG_DISCOVER_CAPTION_SERVICE_NUMBER:
+ {
+ int serviceNumber = (int) msg.obj;
+ doDiscoverCaptionServiceNumber(serviceNumber);
+ return true;
+ }
+ case MSG_SELECT_TRACK:
+ {
+ if (mChannel != null || mRecordingId != null) {
+ doSelectTrack(msg.arg1, (String) msg.obj);
+ }
+ return true;
+ }
+ case MSG_UPDATE_CAPTION_TRACK:
+ {
+ if (mCaptionEnabled) {
+ startCaptionTrack();
+ } else {
+ stopCaptionTrack();
+ }
+ return true;
+ }
+ case MSG_TIMESHIFT_PAUSE:
+ {
+ if (DEBUG) Log.d(TAG, "MSG_TIMESHIFT_PAUSE");
+ if (mPlayer == null) {
+ return true;
+ }
+ setTrickplayEnabledIfNeeded();
+ doTimeShiftPause();
+ return true;
+ }
+ case MSG_TIMESHIFT_RESUME:
+ {
+ if (DEBUG) Log.d(TAG, "MSG_TIMESHIFT_RESUME");
+ if (mPlayer == null) {
+ return true;
+ }
+ setTrickplayEnabledIfNeeded();
+ doTimeShiftResume();
+ return true;
+ }
+ case MSG_TIMESHIFT_SEEK_TO:
+ {
+ long position = (long) msg.obj;
+ if (DEBUG) Log.d(TAG, "MSG_TIMESHIFT_SEEK_TO (position=" + position + ")");
+ if (mPlayer == null) {
+ return true;
+ }
+ setTrickplayEnabledIfNeeded();
+ doTimeShiftSeekTo(position);
+ return true;
+ }
+ case MSG_TIMESHIFT_SET_PLAYBACKPARAMS:
+ {
+ if (mPlayer == null) {
+ return true;
+ }
+ setTrickplayEnabledIfNeeded();
+ doTimeShiftSetPlaybackParams((PlaybackParams) msg.obj);
+ return true;
+ }
+ case MSG_AUDIO_CAPABILITIES_CHANGED:
+ {
+ AudioCapabilities capabilities = (AudioCapabilities) msg.obj;
+ if (DEBUG) {
+ Log.d(TAG, "MSG_AUDIO_CAPABILITIES_CHANGED " + capabilities);
+ }
+ if (capabilities == null) {
+ return true;
+ }
+ if (!capabilities.equals(mAudioCapabilities)) {
+ // HDMI supported encodings are changed. restart player.
+ mAudioCapabilities = capabilities;
+ resetPlayback();
+ }
+ return true;
+ }
+ case MSG_SET_STREAM_VOLUME:
+ {
+ if (mPlayer != null && mPlayer.isPlaying()) {
+ mPlayer.setVolume(mVolume);
+ }
+ 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;
+ }
+ mBufferStartTimeMs = (long) msg.obj;
+ if (!hasEnoughBackwardBuffer()
+ && (!mPlayer.isPlaying() || mPlaybackParams.getSpeed() < 1.0f)) {
+ mPlayer.setPlayWhenReady(true);
+ mPlayer.setAudioTrackAndClosedCaption(true);
+ mPlaybackParams.setSpeed(1.0f);
+ }
+ return true;
+ }
+ case MSG_BUFFER_STATE_CHANGED:
+ {
+ boolean available = (boolean) msg.obj;
+ mSession.notifyTimeShiftStatusChanged(
+ available
+ ? TvInputManager.TIME_SHIFT_STATUS_AVAILABLE
+ : TvInputManager.TIME_SHIFT_STATUS_UNAVAILABLE);
+ return true;
+ }
+ case MSG_CHECK_SIGNAL:
+ {
+ if (mChannel == null || mPlayer == null) {
+ return true;
+ }
+ TsDataSource source = mPlayer.getDataSource();
+ long limitInBytes = source != null ? source.getBufferedPosition() : 0L;
+ if (TunerDebug.ENABLED) {
+ TunerDebug.calculateDiff();
+ mSession.sendUiMessage(
+ TunerSession.MSG_UI_SET_STATUS_TEXT,
+ Html.fromHtml(
+ StatusTextUtils.getStatusWarningInHTML(
+ (limitInBytes - mLastLimitInBytes) / TS_PACKET_SIZE,
+ TunerDebug.getVideoFrameDrop(),
+ TunerDebug.getBytesInQueue(),
+ TunerDebug.getAudioPositionUs(),
+ TunerDebug.getAudioPositionUsRate(),
+ TunerDebug.getAudioPtsUs(),
+ TunerDebug.getAudioPtsUsRate(),
+ TunerDebug.getVideoPtsUs(),
+ TunerDebug.getVideoPtsUsRate())));
+ }
+ mSession.sendUiMessage(TunerSession.MSG_UI_HIDE_MESSAGE);
+ long currentTime = SystemClock.elapsedRealtime();
+ 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_FILE
+ && (isBufferingTooLong || isPreparingTooLong);
+ if (isWeakSignal && !mReportedWeakSignal) {
+ if (!mHandler.hasMessages(MSG_RETRY_PLAYBACK)) {
+ mHandler.sendMessageDelayed(
+ mHandler.obtainMessage(
+ MSG_RETRY_PLAYBACK, System.identityHashCode(mPlayer)),
+ PLAYBACK_RETRY_DELAY_MS);
+ }
+ if (mPlayer != null) {
+ 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
+ > PLAYBACK_STATE_CHANGED_WAITING_THRESHOLD_MS;
+ if (!isPlaybackStable) {
+ // Wait until playback becomes stable.
+ } else if (mReportedDrawnToSurface) {
+ mHandler.removeMessages(MSG_RETRY_PLAYBACK);
+ notifyVideoAvailable();
+ mPlayer.setAudioTrackAndClosedCaption(true);
+ }
+ }
+ mLastLimitInBytes = limitInBytes;
+ mHandler.sendEmptyMessageDelayed(MSG_CHECK_SIGNAL, CHECK_NO_SIGNAL_PERIOD_MS);
+ return true;
+ }
+ case MSG_SET_SURFACE:
+ {
+ if (mPlayer != null) {
+ mPlayer.setSurface(mSurface);
+ } else {
+ // TODO: Since surface is dynamically set, we can remove the dependency of
+ // playback start on mSurface nullity.
+ resetPlayback();
+ }
+ return true;
+ }
+ case MSG_NOTIFY_AUDIO_TRACK_UPDATED:
+ {
+ notifyAudioTracksUpdated();
+ return true;
+ }
+ default:
+ {
+ Log.w(TAG, "Unhandled message code: " + msg.what);
+ return false;
+ }
+ }
+ }
+
+ // Private methods
+ private void doSelectTrack(int type, String trackId) {
+ int numTrackId =
+ trackId != null ? Integer.parseInt(trackId.substring(TRACK_PREFIX_SIZE)) : -1;
+ if (type == TvTrackInfo.TYPE_AUDIO) {
+ if (trackId == null) {
+ return;
+ }
+ 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) {
+ if (trackId == null) {
+ mSession.notifyTrackSelected(type, null);
+ mCaptionTrack = null;
+ stopCaptionTrack();
+ return;
+ }
+ for (TvTrackInfo track : mTvTracks) {
+ if (track.getId().equals(trackId)) {
+ // The service number of the caption service is used for track id of a
+ // subtitle track. Passes the following track id on to TsParser.
+ mSession.notifyTrackSelected(type, trackId);
+ mCaptionTrack = mCaptionTrackMap.get(numTrackId);
+ startCaptionTrack();
+ return;
+ }
+ }
+ }
+ }
+
+ 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);
+ Log.i(TAG, "Passthrough AC3 renderer");
+ if (DEBUG) Log.d(TAG, "ExoPlayer created");
+ return player;
+ }
+
+ private void startCaptionTrack() {
+ if (mCaptionEnabled && mCaptionTrack != null) {
+ mSession.sendUiMessage(TunerSession.MSG_UI_START_CAPTION_TRACK, mCaptionTrack);
+ if (mPlayer != null) {
+ mPlayer.setCaptionServiceNumber(mCaptionTrack.serviceNumber);
+ }
+ }
+ }
+
+ private void stopCaptionTrack() {
+ if (mPlayer != null) {
+ mPlayer.setCaptionServiceNumber(Cea708Data.EMPTY_SERVICE_NUMBER);
+ }
+ mSession.sendUiMessage(TunerSession.MSG_UI_STOP_CAPTION_TRACK);
+ }
+
+ private void resetTvTracks() {
+ mTvTracks.clear();
+ mAudioTrackMap.clear();
+ mCaptionTrackMap.clear();
+ mSession.sendUiMessage(TunerSession.MSG_UI_RESET_CAPTION_TRACK);
+ mSession.notifyTracksChanged(mTvTracks);
+ }
+
+ private void updateTvTracks(TvTracksInterface tvTracksInterface, boolean fromPmt) {
+ 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);
+ }
+ }
+ }
+
+ private void removeTvTracks(int trackType) {
+ Iterator<TvTrackInfo> iterator = mTvTracks.iterator();
+ while (iterator.hasNext()) {
+ TvTrackInfo tvTrackInfo = iterator.next();
+ if (tvTrackInfo.getType() == trackType) {
+ iterator.remove();
+ }
+ }
+ }
+
+ private void updateVideoTrack(int width, int height) {
+ removeTvTracks(TvTrackInfo.TYPE_VIDEO);
+ mTvTracks.add(
+ new TvTrackInfo.Builder(TvTrackInfo.TYPE_VIDEO, VIDEO_TRACK_ID)
+ .setVideoWidth(width)
+ .setVideoHeight(height)
+ .build());
+ mSession.notifyTracksChanged(mTvTracks);
+ mSession.notifyTrackSelected(TvTrackInfo.TYPE_VIDEO, VIDEO_TRACK_ID);
+ }
+
+ private void updateAudioTracks(List<AtscAudioTrack> audioTracks) {
+ if (DEBUG) {
+ Log.d(TAG, "Update AudioTracks " + audioTracks);
+ }
+ mAudioTrackMap.clear();
+ if (audioTracks != null) {
+ int index = 0;
+ for (AtscAudioTrack audioTrack : audioTracks) {
+ audioTrack.index = index;
+ mAudioTrackMap.put(index, audioTrack);
+ ++index;
+ }
+ }
+ mHandler.sendEmptyMessage(MSG_NOTIFY_AUDIO_TRACK_UPDATED);
+ }
+
+ private void notifyAudioTracksUpdated() {
+ if (mPlayer == null) {
+ // Audio tracks will be updated later once player initialization is done.
+ return;
+ }
+ int audioTrackCount = mPlayer.getTrackCount(MpegTsPlayer.TRACK_TYPE_AUDIO);
+ removeTvTracks(TvTrackInfo.TYPE_AUDIO);
+ for (int i = 0; i < audioTrackCount; i++) {
+ // 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(infoFromPlayer.channelCount);
+ builder.setAudioSampleRate(infoFromPlayer.sampleRate);
+ TvTrackInfo track = builder.build();
+ mTvTracks.add(track);
+ }
+ mSession.notifyTracksChanged(mTvTracks);
+ }
+
+ private void updateCaptionTracks(List<AtscCaptionTrack> captionTracks) {
+ if (DEBUG) {
+ Log.d(TAG, "Update CaptionTrack " + captionTracks);
+ }
+ removeTvTracks(TvTrackInfo.TYPE_SUBTITLE);
+ mCaptionTrackMap.clear();
+ if (captionTracks != null) {
+ for (AtscCaptionTrack captionTrack : captionTracks) {
+ if (mCaptionTrackMap.indexOfKey(captionTrack.serviceNumber) >= 0) {
+ continue;
+ }
+ String language = captionTrack.language;
+
+ // The service number of the caption service is used for track id of a subtitle.
+ // Later, when a subtitle is chosen, track id will be passed on to TsParser.
+ TvTrackInfo.Builder builder =
+ new TvTrackInfo.Builder(
+ TvTrackInfo.TYPE_SUBTITLE,
+ SUBTITLE_TRACK_PREFIX + captionTrack.serviceNumber);
+ builder.setLanguage(language);
+ mTvTracks.add(builder.build());
+ mCaptionTrackMap.put(captionTrack.serviceNumber, captionTrack);
+ }
+ }
+ mSession.notifyTracksChanged(mTvTracks);
+ }
+
+ private void updateChannelInfo(TunerChannel channel) {
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ String.format(
+ "Channel Info (old) videoPid: %d audioPid: %d " + "audioSize: %d",
+ mChannel.getVideoPid(),
+ mChannel.getAudioPid(),
+ mChannel.getAudioPids().size()));
+ }
+
+ // The list of the audio tracks resided in a channel is often changed depending on a
+ // program being on the air. So, we should update the streaming PIDs and types of the
+ // tuned channel according to the newly received channel data.
+ int oldVideoPid = mChannel.getVideoPid();
+ int oldAudioPid = mChannel.getAudioPid();
+ List<Integer> audioPids = channel.getAudioPids();
+ List<Integer> audioStreamTypes = channel.getAudioStreamTypes();
+ int size = audioPids.size();
+ mChannel.setVideoPid(channel.getVideoPid());
+ mChannel.setAudioPids(audioPids);
+ mChannel.setAudioStreamTypes(audioStreamTypes);
+ updateTvTracks(channel, true);
+ int index = audioPids.isEmpty() ? -1 : 0;
+ for (int i = 0; i < size; ++i) {
+ if (audioPids.get(i) == oldAudioPid) {
+ index = i;
+ break;
+ }
+ }
+ mChannel.selectAudioTrack(index);
+ mSession.notifyTrackSelected(
+ TvTrackInfo.TYPE_AUDIO, index == -1 ? null : AUDIO_TRACK_PREFIX + index);
+
+ // Reset playback if there is a change in the listening streaming PIDs.
+ if (oldVideoPid != mChannel.getVideoPid() || oldAudioPid != mChannel.getAudioPid()) {
+ // TODO: Implement a switching between tracks more smoothly.
+ resetPlayback();
+ }
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ String.format(
+ "Channel Info (new) videoPid: %d audioPid: %d " + " audioSize: %d",
+ mChannel.getVideoPid(),
+ mChannel.getAudioPid(),
+ mChannel.getAudioPids().size()));
+ }
+ }
+
+ private void stopPlayback(boolean removeChannelDataCallbacks) {
+ if (removeChannelDataCallbacks) {
+ mChannelDataManager.removeAllCallbacksAndMessages();
+ }
+ if (mPlayer != null) {
+ mPlayer.setPlayWhenReady(false);
+ mPlayer.release();
+ mPlayer = null;
+ mPlayerState = ExoPlayer.STATE_IDLE;
+ mPlaybackParams.setSpeed(1.0f);
+ mPlayerStarted = false;
+ mReportedDrawnToSurface = false;
+ 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(int playerHashCode) {
+ // TODO: provide hasAudio()/hasVideo() for play recordings.
+ if (mPlayer == null || System.identityHashCode(mPlayer) != playerHashCode) {
+ return;
+ }
+ if (mChannel != null && !mChannel.hasAudio()) {
+ if (DEBUG) Log.d(TAG, "Channel " + mChannel + " does not have audio.");
+ // Playbacks with video-only stream have not been tested yet.
+ // No video-only channel has been found.
+ notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN);
+ return;
+ }
+ if (mChannel != null
+ && ((mChannel.hasAudio() && !mPlayer.hasAudio())
+ || (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
+ // inside this method.
+ Surface surface = mSurface;
+ if (surface != null && !mPlayerStarted) {
+ mPlayer.setSurface(surface);
+ mPlayer.setPlayWhenReady(true);
+ mPlayer.setVolume(mVolume);
+ if (mChannel != null && mPlayer.hasAudio() && !mPlayer.hasVideo()) {
+ notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_AUDIO_ONLY);
+ } else if (!mReportedWeakSignal) {
+ // Doesn't show buffering during weak signal.
+ notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING);
+ }
+ mSession.sendUiMessage(TunerSession.MSG_UI_HIDE_MESSAGE);
+ mPlayerStarted = true;
+ }
+ }
+
+ private void preparePlayback() {
+ SoftPreconditions.checkState(mPlayer == null);
+ if (mChannel == null && mRecordingId == null) {
+ return;
+ }
+ mSourceManager.setKeepTuneStatus(true);
+ 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, 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);
+ 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;
+ mPlayerStarted = false;
+ mHandler.removeMessages(MSG_CHECK_SIGNAL);
+ mHandler.sendEmptyMessageDelayed(MSG_CHECK_SIGNAL, CHECK_NO_SIGNAL_INITIAL_DELAY_MS);
+ }
+ }
+
+ private void resetPlayback() {
+ long timestamp, oldTimestamp;
+ timestamp = SystemClock.elapsedRealtime();
+ stopPlayback(false);
+ stopCaptionTrack();
+ if (ENABLE_PROFILER) {
+ oldTimestamp = timestamp;
+ timestamp = SystemClock.elapsedRealtime();
+ Log.i(TAG, "[Profiler] stopPlayback() takes " + (timestamp - oldTimestamp) + " ms");
+ }
+ if (mChannelBlocked || mSurface == null) {
+ return;
+ }
+ preparePlayback();
+ }
+
+ private void prepareTune(TunerChannel channel, String recording) {
+ mChannelBlocked = false;
+ mUnblockedContentRating = null;
+ mRetryCount = 0;
+ mChannel = channel;
+ mRecordingId = recording;
+ mRecordingDuration = recording != null ? getDurationForRecording(recording) : null;
+ mProgram = null;
+ mPrograms = null;
+ 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);
+ }
+
+ private void doReschedulePrograms() {
+ long currentPositionMs = getCurrentPosition();
+ long forwardDifference =
+ Math.abs(currentPositionMs - mLastPositionMs - RESCHEDULE_PROGRAMS_INTERVAL_MS);
+ mLastPositionMs = currentPositionMs;
+
+ // A gap is measured as the time difference between previous and next current position
+ // periodically. If the gap has a significant difference with an interval of a period,
+ // this means that there is a change of playback status and the programs of the current
+ // channel should be rescheduled to new playback timeline.
+ if (forwardDifference > RESCHEDULE_PROGRAMS_TOLERANCE_MS) {
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "reschedule programs size:"
+ + (mPrograms != null ? mPrograms.size() : 0)
+ + " current program: "
+ + getCurrentProgram());
+ }
+ mHandler.obtainMessage(MSG_SCHEDULE_OF_PROGRAMS, new Pair<>(mChannel, mPrograms))
+ .sendToTarget();
+ }
+ mHandler.removeMessages(MSG_RESCHEDULE_PROGRAMS);
+ mHandler.sendEmptyMessageDelayed(MSG_RESCHEDULE_PROGRAMS, RESCHEDULE_PROGRAMS_INTERVAL_MS);
+ }
+
+ private int getTrickPlaySeekIntervalMs() {
+ return Math.max(
+ EXPECTED_KEY_FRAME_INTERVAL_MS / (int) Math.abs(mPlaybackParams.getSpeed()),
+ MIN_TRICKPLAY_SEEK_INTERVAL_MS);
+ }
+
+ private void doTrickplayBySeek(int seekPositionMs) {
+ mHandler.removeMessages(MSG_TRICKPLAY_BY_SEEK);
+ if (mPlaybackParams.getSpeed() == 1.0f || !mPlayer.isPrepared()) {
+ return;
+ }
+ if (seekPositionMs < mBufferStartTimeMs - mRecordStartTimeMs) {
+ if (mPlaybackParams.getSpeed() > 1.0f) {
+ // If fast forwarding, the seekPositionMs can be out of the buffered range
+ // because of chuck evictions.
+ seekPositionMs = (int) (mBufferStartTimeMs - mRecordStartTimeMs);
+ } else {
+ mPlayer.seekTo(mBufferStartTimeMs - mRecordStartTimeMs);
+ mPlaybackParams.setSpeed(1.0f);
+ mPlayer.setAudioTrackAndClosedCaption(true);
+ return;
+ }
+ } else if (seekPositionMs > System.currentTimeMillis() - mRecordStartTimeMs) {
+ // 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();
+ if (!mPlayer.isBuffering()) {
+ mPlayer.seekTo(seekPositionMs);
+ } else {
+ delayForNextSeek = MIN_TRICKPLAY_SEEK_INTERVAL_MS;
+ }
+ seekPositionMs += mPlaybackParams.getSpeed() * delayForNextSeek;
+ mHandler.sendMessageDelayed(
+ mHandler.obtainMessage(MSG_TRICKPLAY_BY_SEEK, seekPositionMs, 0), delayForNextSeek);
+ }
+
+ private void doTimeShiftPause() {
+ mHandler.removeMessages(MSG_SMOOTH_TRICKPLAY_MONITOR);
+ mHandler.removeMessages(MSG_TRICKPLAY_BY_SEEK);
+ if (!hasEnoughBackwardBuffer()) {
+ return;
+ }
+ mPlaybackParams.setSpeed(1.0f);
+ mPlayer.setPlayWhenReady(false);
+ mPlayer.setAudioTrackAndClosedCaption(true);
+ }
+
+ private void doTimeShiftResume() {
+ mHandler.removeMessages(MSG_SMOOTH_TRICKPLAY_MONITOR);
+ mHandler.removeMessages(MSG_TRICKPLAY_BY_SEEK);
+ mPlaybackParams.setSpeed(1.0f);
+ mPlayer.setPlayWhenReady(true);
+ mPlayer.setAudioTrackAndClosedCaption(true);
+ }
+
+ private void doTimeShiftSeekTo(long timeMs) {
+ mHandler.removeMessages(MSG_SMOOTH_TRICKPLAY_MONITOR);
+ mHandler.removeMessages(MSG_TRICKPLAY_BY_SEEK);
+ mPlayer.seekTo((int) (timeMs - mRecordStartTimeMs));
+ }
+
+ private void doTimeShiftSetPlaybackParams(PlaybackParams params) {
+ if (!hasEnoughBackwardBuffer() && params.getSpeed() < 1.0f) {
+ return;
+ }
+ mPlaybackParams = params;
+ float speed = mPlaybackParams.getSpeed();
+ if (speed == 1.0f) {
+ mHandler.removeMessages(MSG_SMOOTH_TRICKPLAY_MONITOR);
+ mHandler.removeMessages(MSG_TRICKPLAY_BY_SEEK);
+ doTimeShiftResume();
+ } else if (mPlayer.supportSmoothTrickPlay(speed)) {
+ mHandler.removeMessages(MSG_TRICKPLAY_BY_SEEK);
+ 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.setAudioTrackAndClosedCaption(false);
+ mPlayer.setPlayWhenReady(false);
+ // Initiate trickplay
+ mHandler.sendMessage(
+ mHandler.obtainMessage(
+ MSG_TRICKPLAY_BY_SEEK,
+ (int)
+ (mPlayer.getCurrentPosition()
+ + speed * getTrickPlaySeekIntervalMs()),
+ 0));
+ }
+ }
+ }
+
+ private EitItem getCurrentProgram() {
+ if (mPrograms == null || mPrograms.isEmpty()) {
+ return null;
+ }
+ if (mChannel.getType() == Channel.TYPE_FILE) {
+ // For the playback from the local file, we use the first one from the given program.
+ EitItem first = mPrograms.get(0);
+ if (first != null
+ && (mProgram == null
+ || first.getStartTimeUtcMillis() < mProgram.getStartTimeUtcMillis())) {
+ return first;
+ }
+ return null;
+ }
+ long currentTimeMs = getCurrentPosition();
+ for (EitItem item : mPrograms) {
+ if (item.getStartTimeUtcMillis() <= currentTimeMs
+ && item.getEndTimeUtcMillis() >= currentTimeMs) {
+ return item;
+ }
+ }
+ return null;
+ }
+
+ private void doParentalControls() {
+ boolean isParentalControlsEnabled = mTvInputManager.isParentalControlsEnabled();
+ if (isParentalControlsEnabled) {
+ TvContentRating blockContentRating = getContentRatingOfCurrentProgramBlocked();
+ if (DEBUG) {
+ if (blockContentRating != null) {
+ Log.d(
+ TAG,
+ "Check parental controls: blocked by content rating - "
+ + blockContentRating);
+ } else {
+ Log.d(TAG, "Check parental controls: available");
+ }
+ }
+ updateChannelBlockStatus(blockContentRating != null, blockContentRating);
+ } else {
+ if (DEBUG) {
+ Log.d(TAG, "Check parental controls: available");
+ }
+ updateChannelBlockStatus(false, null);
+ }
+ }
+
+ private void doDiscoverCaptionServiceNumber(int serviceNumber) {
+ int index = mCaptionTrackMap.indexOfKey(serviceNumber);
+ if (index < 0) {
+ AtscCaptionTrack captionTrack = new AtscCaptionTrack();
+ captionTrack.serviceNumber = serviceNumber;
+ captionTrack.wideAspectRatio = false;
+ captionTrack.easyReader = false;
+ mCaptionTrackMap.put(serviceNumber, captionTrack);
+ mTvTracks.add(
+ new TvTrackInfo.Builder(
+ TvTrackInfo.TYPE_SUBTITLE,
+ SUBTITLE_TRACK_PREFIX + serviceNumber)
+ .build());
+ mSession.notifyTracksChanged(mTvTracks);
+ }
+ }
+
+ private TvContentRating getContentRatingOfCurrentProgramBlocked() {
+ EitItem currentProgram = getCurrentProgram();
+ if (currentProgram == null) {
+ return null;
+ }
+ TvContentRating[] ratings =
+ mTvContentRatingCache.getRatings(currentProgram.getContentRating());
+ if (ratings == null || ratings.length == 0) {
+ ratings = new TvContentRating[] {TvContentRating.UNRATED};
+ }
+ for (TvContentRating rating : ratings) {
+ if (!Objects.equals(mUnblockedContentRating, rating)
+ && mTvInputManager.isRatingBlocked(rating)) {
+ return rating;
+ }
+ }
+ return null;
+ }
+
+ private void updateChannelBlockStatus(boolean channelBlocked, TvContentRating contentRating) {
+ if (mChannelBlocked == channelBlocked) {
+ return;
+ }
+ mChannelBlocked = channelBlocked;
+ if (mChannelBlocked) {
+ clearCallbacksAndMessagesSafely();
+ stopPlayback(true);
+ resetTvTracks();
+ if (contentRating != null) {
+ mSession.notifyContentBlocked(contentRating);
+ }
+ mHandler.sendEmptyMessageDelayed(MSG_PARENTAL_CONTROLS, PARENTAL_CONTROLS_INTERVAL_MS);
+ } else {
+ clearCallbacksAndMessagesSafely();
+ resetPlayback();
+ mSession.notifyContentAllowed();
+ mHandler.sendEmptyMessageDelayed(
+ MSG_RESCHEDULE_PROGRAMS, RESCHEDULE_PROGRAMS_INITIAL_DELAY_MS);
+ mHandler.removeMessages(MSG_CHECK_SIGNAL);
+ mHandler.sendEmptyMessageDelayed(MSG_CHECK_SIGNAL, CHECK_NO_SIGNAL_INITIAL_DELAY_MS);
+ }
+ }
+
+ @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;
+ }
+
+ private void notifyVideoUnavailable(final int reason) {
+ mReportedWeakSignal = (reason == TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL);
+ if (mSession != null) {
+ mSession.notifyVideoUnavailable(reason);
+ }
+ }
+
+ private void notifyVideoAvailable() {
+ mReportedWeakSignal = false;
+ if (mSession != null) {
+ mSession.notifyVideoAvailable();
+ }
+ }
+}
diff --git a/src/com/android/tv/tuner/tvinput/TunerStorageCleanUpService.java b/src/com/android/tv/tuner/tvinput/TunerStorageCleanUpService.java
new file mode 100644
index 00000000..f014d568
--- /dev/null
+++ b/src/com/android/tv/tuner/tvinput/TunerStorageCleanUpService.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.tv.tuner.tvinput;
+
+import android.app.job.JobParameters;
+import android.app.job.JobService;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.media.tv.TvContract;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.util.Log;
+import com.android.tv.TvApplication;
+import com.android.tv.dvr.DvrStorageStatusManager;
+import com.android.tv.util.Utils;
+import java.io.File;
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Creates {@link JobService} to clean up recorded program files which are not referenced from
+ * database.
+ */
+public class TunerStorageCleanUpService extends JobService {
+ private 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);
+ }
+
+ @Override
+ public boolean onStartJob(JobParameters params) {
+ mTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, params);
+ return true;
+ }
+
+ @Override
+ public boolean onStopJob(JobParameters params) {
+ return false;
+ }
+
+ /**
+ * Cleans up recorded program files which are not referenced from database. Cleaning up will be
+ * done periodically.
+ */
+ public static class CleanUpStorageTask extends AsyncTask<JobParameters, Void, JobParameters[]> {
+ private static final String[] mProjection = {
+ TvContract.RecordedPrograms.COLUMN_PACKAGE_NAME,
+ TvContract.RecordedPrograms.COLUMN_RECORDING_DATA_URI
+ };
+ private static final long ELAPSED_MILLIS_TO_DELETE = TimeUnit.DAYS.toMillis(1);
+
+ private final Context mContext;
+ private final DvrStorageStatusManager mDvrStorageStatusManager;
+ private final JobService mJobService;
+ private final ContentResolver mContentResolver;
+
+ /**
+ * Creates a recurring storage cleaning task.
+ *
+ * @param context {@link Context}
+ * @param jobService {@link JobService}
+ */
+ public CleanUpStorageTask(Context context, JobService jobService) {
+ mContext = context;
+ mDvrStorageStatusManager =
+ TvApplication.getSingletons(mContext).getDvrStorageStatusManager();
+ mJobService = jobService;
+ mContentResolver = mContext.getContentResolver();
+ }
+
+ private Set<String> getRecordedProgramsDirs() {
+ try (Cursor c =
+ mContentResolver.query(
+ TvContract.RecordedPrograms.CONTENT_URI,
+ mProjection,
+ null,
+ null,
+ null)) {
+ if (c == null) {
+ return null;
+ }
+ Set<String> recordedProgramDirs = new HashSet<>();
+ while (c.moveToNext()) {
+ String packageName = c.getString(0);
+ String dataUriString = c.getString(1);
+ if (dataUriString == null) {
+ continue;
+ }
+ Uri dataUri = Uri.parse(dataUriString);
+ if (!Utils.isInBundledPackageSet(packageName)
+ || dataUri == null
+ || dataUri.getPath() == null
+ || !ContentResolver.SCHEME_FILE.equals(dataUri.getScheme())) {
+ continue;
+ }
+ File recordedProgramDir = new File(dataUri.getPath());
+ try {
+ recordedProgramDirs.add(recordedProgramDir.getCanonicalPath());
+ } catch (IOException | SecurityException e) {
+ }
+ }
+ return recordedProgramDirs;
+ }
+ }
+
+ @Override
+ protected JobParameters[] doInBackground(JobParameters... params) {
+ if (mDvrStorageStatusManager.getDvrStorageStatus()
+ == DvrStorageStatusManager.STORAGE_STATUS_MISSING) {
+ return params;
+ }
+ File dvrRecordingDir = mDvrStorageStatusManager.getRecordingRootDataDirectory();
+ if (dvrRecordingDir == null || !dvrRecordingDir.isDirectory()) {
+ return params;
+ }
+ Set<String> recordedProgramDirs = getRecordedProgramsDirs();
+ if (recordedProgramDirs == null) {
+ return params;
+ }
+ File[] files = dvrRecordingDir.listFiles();
+ if (files == null || files.length == 0) {
+ return params;
+ }
+ for (File recordingDir : files) {
+ try {
+ if (!recordedProgramDirs.contains(recordingDir.getCanonicalPath())) {
+ long lastModified = recordingDir.lastModified();
+ long now = System.currentTimeMillis();
+ if (lastModified != 0 && lastModified < now - ELAPSED_MILLIS_TO_DELETE) {
+ // To prevent current recordings from being deleted,
+ // deletes recordings which was not modified for long enough time.
+ Utils.deleteDirOrFile(recordingDir);
+ }
+ }
+ } catch (IOException | SecurityException e) {
+ // would not happen
+ }
+ }
+ return params;
+ }
+
+ @Override
+ protected void onPostExecute(JobParameters[] params) {
+ for (JobParameters param : params) {
+ mJobService.jobFinished(param, false);
+ }
+ }
+ }
+}
diff --git a/src/com/android/tv/tuner/tvinput/TunerTvInputService.java b/src/com/android/tv/tuner/tvinput/TunerTvInputService.java
new file mode 100644
index 00000000..a1596e3b
--- /dev/null
+++ b/src/com/android/tv/tuner/tvinput/TunerTvInputService.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.tvinput;
+
+import android.app.job.JobInfo;
+import android.app.job.JobScheduler;
+import android.content.ComponentName;
+import android.content.Context;
+import android.media.tv.TvContract;
+import android.media.tv.TvInputService;
+import android.util.Log;
+import com.android.tv.TvApplication;
+import com.android.tv.common.feature.CommonFeatures;
+import com.google.android.exoplayer.audio.AudioCapabilities;
+import com.google.android.exoplayer.audio.AudioCapabilitiesReceiver;
+import java.util.Collections;
+import java.util.Set;
+import java.util.WeakHashMap;
+import java.util.concurrent.TimeUnit;
+
+/** {@link TunerTvInputService} serves TV channels coming from a tuner device. */
+public class TunerTvInputService extends TvInputService
+ implements AudioCapabilitiesReceiver.Listener {
+ private static final String TAG = "TunerTvInputService";
+ private static final boolean DEBUG = false;
+
+ private static final int DVR_STORAGE_CLEANUP_JOB_ID = 100;
+
+ // WeakContainer for {@link TvInputSessionImpl}
+ private final Set<TunerSession> mTunerSessions = Collections.newSetFromMap(new WeakHashMap<>());
+ private ChannelDataManager mChannelDataManager;
+ private AudioCapabilitiesReceiver mAudioCapabilitiesReceiver;
+ private AudioCapabilities mAudioCapabilities;
+
+ @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();
+ if (CommonFeatures.DVR.isEnabled(this)) {
+ JobScheduler jobScheduler =
+ (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);
+ JobInfo pendingJob = jobScheduler.getPendingJob(DVR_STORAGE_CLEANUP_JOB_ID);
+ if (pendingJob != null) {
+ // storage cleaning job is already scheduled.
+ } else {
+ JobInfo job =
+ new JobInfo.Builder(
+ DVR_STORAGE_CLEANUP_JOB_ID,
+ new ComponentName(this, TunerStorageCleanUpService.class))
+ .setPersisted(true)
+ .setPeriodic(TimeUnit.DAYS.toMillis(1))
+ .build();
+ jobScheduler.schedule(job);
+ }
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ if (DEBUG) Log.d(TAG, "onDestroy");
+ super.onDestroy();
+ mChannelDataManager.release();
+ mAudioCapabilitiesReceiver.unregister();
+ }
+
+ @Override
+ public RecordingSession onCreateRecordingSession(String inputId) {
+ return new TunerRecordingSession(this, inputId, mChannelDataManager);
+ }
+
+ @Override
+ public Session onCreateSession(String inputId) {
+ if (DEBUG) Log.d(TAG, "onCreateSession");
+ try {
+ final TunerSession session = new TunerSession(this, mChannelDataManager);
+ mTunerSessions.add(session);
+ session.setAudioCapabilities(mAudioCapabilities);
+ session.setOverlayViewEnabled(true);
+ return session;
+ } catch (RuntimeException e) {
+ // There are no available DVB devices.
+ Log.e(TAG, "Creating a session for " + inputId + " failed.", e);
+ return null;
+ }
+ }
+
+ @Override
+ public void onAudioCapabilitiesChanged(AudioCapabilities audioCapabilities) {
+ mAudioCapabilities = audioCapabilities;
+ for (TunerSession session : mTunerSessions) {
+ if (!session.isReleased()) {
+ session.setAudioCapabilities(audioCapabilities);
+ }
+ }
+ }
+
+ public static String getInputId(Context context) {
+ return TvContract.buildInputId(new ComponentName(context, TunerTvInputService.class));
+ }
+}
diff --git a/src/com/android/tv/tuner/util/ByteArrayBuffer.java b/src/com/android/tv/tuner/util/ByteArrayBuffer.java
new file mode 100644
index 00000000..c3e38443
--- /dev/null
+++ b/src/com/android/tv/tuner/util/ByteArrayBuffer.java
@@ -0,0 +1,152 @@
+/*
+ * $HeadURL: http://svn.apache.org/repos/asf/httpcomponents/httpcore/trunk/module-main/src/main/java/org/apache/http/util/ByteArrayBuffer.java $
+ * $Revision: 496070 $
+ * $Date: 2007-01-14 04:18:34 -0800 (Sun, 14 Jan 2007) $
+ *
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package com.android.tv.tuner.util;
+
+/** An expandable byte buffer built on byte array. */
+public final class ByteArrayBuffer {
+
+ private byte[] buffer;
+ private int len;
+
+ public ByteArrayBuffer(int capacity) {
+ super();
+ if (capacity < 0) {
+ throw new IllegalArgumentException("Buffer capacity may not be negative");
+ }
+ this.buffer = new byte[capacity];
+ }
+
+ private void expand(int newlen) {
+ byte newbuffer[] = new byte[Math.max(this.buffer.length << 1, newlen)];
+ System.arraycopy(this.buffer, 0, newbuffer, 0, this.len);
+ this.buffer = newbuffer;
+ }
+
+ public void append(final byte[] b, int off, int len) {
+ if (b == null) {
+ return;
+ }
+ if ((off < 0)
+ || (off > b.length)
+ || (len < 0)
+ || ((off + len) < 0)
+ || ((off + len) > b.length)) {
+ throw new IndexOutOfBoundsException();
+ }
+ if (len == 0) {
+ return;
+ }
+ int newlen = this.len + len;
+ if (newlen > this.buffer.length) {
+ expand(newlen);
+ }
+ System.arraycopy(b, off, this.buffer, this.len, len);
+ this.len = newlen;
+ }
+
+ public void append(int b) {
+ int newlen = this.len + 1;
+ if (newlen > this.buffer.length) {
+ expand(newlen);
+ }
+ this.buffer[this.len] = (byte) b;
+ this.len = newlen;
+ }
+
+ public void append(final char[] b, int off, int len) {
+ if (b == null) {
+ return;
+ }
+ if ((off < 0)
+ || (off > b.length)
+ || (len < 0)
+ || ((off + len) < 0)
+ || ((off + len) > b.length)) {
+ throw new IndexOutOfBoundsException();
+ }
+ if (len == 0) {
+ return;
+ }
+ int oldlen = this.len;
+ int newlen = oldlen + len;
+ if (newlen > this.buffer.length) {
+ expand(newlen);
+ }
+ for (int i1 = off, i2 = oldlen; i2 < newlen; i1++, i2++) {
+ this.buffer[i2] = (byte) b[i1];
+ }
+ this.len = newlen;
+ }
+
+ public void clear() {
+ this.len = 0;
+ }
+
+ public byte[] toByteArray() {
+ byte[] b = new byte[this.len];
+ if (this.len > 0) {
+ System.arraycopy(this.buffer, 0, b, 0, this.len);
+ }
+ return b;
+ }
+
+ public int byteAt(int i) {
+ return this.buffer[i];
+ }
+
+ public int capacity() {
+ return this.buffer.length;
+ }
+
+ public int length() {
+ return this.len;
+ }
+
+ public byte[] buffer() {
+ return this.buffer;
+ }
+
+ public void setLength(int len) {
+ if (len < 0 || len > this.buffer.length) {
+ throw new IndexOutOfBoundsException();
+ }
+ this.len = len;
+ }
+
+ public boolean isEmpty() {
+ return this.len == 0;
+ }
+
+ public boolean isFull() {
+ return this.len == this.buffer.length;
+ }
+}
diff --git a/src/com/android/tv/tuner/util/ConvertUtils.java b/src/com/android/tv/tuner/util/ConvertUtils.java
new file mode 100644
index 00000000..4b7fbdae
--- /dev/null
+++ b/src/com/android/tv/tuner/util/ConvertUtils.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.util;
+
+/** Utility class for converting date and time. */
+public class ConvertUtils {
+ // Time diff between 1.1.1970 00:00:00 and 6.1.1980 00:00:00
+ private static final long DIFF_BETWEEN_UNIX_EPOCH_AND_GPS = 315964800;
+
+ private ConvertUtils() {}
+
+ public static long convertGPSTimeToUnixEpoch(long gpsTime) {
+ return gpsTime + DIFF_BETWEEN_UNIX_EPOCH_AND_GPS;
+ }
+
+ public static long convertUnixEpochToGPSTime(long epochTime) {
+ return epochTime - DIFF_BETWEEN_UNIX_EPOCH_AND_GPS;
+ }
+}
diff --git a/src/com/android/tv/tuner/util/GlobalSettingsUtils.java b/src/com/android/tv/tuner/util/GlobalSettingsUtils.java
new file mode 100644
index 00000000..98463f3b
--- /dev/null
+++ b/src/com/android/tv/tuner/util/GlobalSettingsUtils.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.util;
+
+import android.content.Context;
+import android.provider.Settings;
+
+/** Utility class that get information of global settings. */
+public class GlobalSettingsUtils {
+ // Since global surround setting is hided, add the related variable here for checking surround
+ // sound setting when the audio is unavailable. Remove this workaround after b/31254857 fixed.
+ private static final String ENCODED_SURROUND_OUTPUT = "encoded_surround_output";
+ public static final int ENCODED_SURROUND_OUTPUT_NEVER = 1;
+
+ private GlobalSettingsUtils() {}
+
+ public static int getEncodedSurroundOutputSettings(Context context) {
+ return Settings.Global.getInt(context.getContentResolver(), ENCODED_SURROUND_OUTPUT, 0);
+ }
+}
diff --git a/src/com/android/tv/tuner/util/Ints.java b/src/com/android/tv/tuner/util/Ints.java
new file mode 100644
index 00000000..74e0ca8d
--- /dev/null
+++ b/src/com/android/tv/tuner/util/Ints.java
@@ -0,0 +1,26 @@
+package com.android.tv.tuner.util;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/** Static utility methods pertaining to int primitives. (Referred Guava's Ints class) */
+public class Ints {
+ private Ints() {}
+
+ public static int[] toArray(List<Integer> integerList) {
+ int[] intArray = new int[integerList.size()];
+ int i = 0;
+ for (Integer data : integerList) {
+ intArray[i++] = data;
+ }
+ return intArray;
+ }
+
+ public static List<Integer> asList(int[] intArray) {
+ List<Integer> integerList = new ArrayList<>(intArray.length);
+ for (int data : intArray) {
+ integerList.add(data);
+ }
+ return integerList;
+ }
+}
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..502c5648
--- /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;
+ }
+}
diff --git a/src/com/android/tv/tuner/util/StatusTextUtils.java b/src/com/android/tv/tuner/util/StatusTextUtils.java
new file mode 100644
index 00000000..84e2fc5a
--- /dev/null
+++ b/src/com/android/tv/tuner/util/StatusTextUtils.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.util;
+
+import java.util.Locale;
+
+/** Utility class for tuner status messages. */
+public class StatusTextUtils {
+ private static final int PACKETS_PER_SEC_YELLOW = 1500;
+ private static final int PACKETS_PER_SEC_RED = 1000;
+ private static final int AUDIO_POSITION_MS_RATE_DIFF_YELLOW = 100;
+ private static final int AUDIO_POSITION_MS_RATE_DIFF_RED = 200;
+ private static final String COLOR_RED = "red";
+ private static final String COLOR_YELLOW = "yellow";
+ private static final String COLOR_GREEN = "green";
+ private static final String COLOR_GRAY = "gray";
+
+ private StatusTextUtils() {}
+
+ /**
+ * Returns tuner status warning message in HTML.
+ *
+ * <p>This is only called for debuging and always shown in english.
+ */
+ public static String getStatusWarningInHTML(
+ long packetsPerSec,
+ int videoFrameDrop,
+ int bytesInQueue,
+ long audioPositionUs,
+ long audioPositionUsRate,
+ long audioPtsUs,
+ long audioPtsUsRate,
+ long videoPtsUs,
+ long videoPtsUsRate) {
+ StringBuffer buffer = new StringBuffer();
+
+ // audioPosition should go in rate of 1000ms.
+ long audioPositionMsRate = audioPositionUsRate / 1000;
+ String audioPositionColor;
+ if (Math.abs(audioPositionMsRate - 1000) > AUDIO_POSITION_MS_RATE_DIFF_RED) {
+ audioPositionColor = COLOR_RED;
+ } else if (Math.abs(audioPositionMsRate - 1000) > AUDIO_POSITION_MS_RATE_DIFF_YELLOW) {
+ audioPositionColor = COLOR_YELLOW;
+ } else {
+ audioPositionColor = COLOR_GRAY;
+ }
+ buffer.append(String.format(Locale.US, "<font color=%s>", audioPositionColor));
+ buffer.append(
+ String.format(
+ Locale.US,
+ "audioPositionMs: %d (%d)<br>",
+ audioPositionUs / 1000,
+ audioPositionMsRate));
+ buffer.append("</font>\n");
+ buffer.append("<font color=" + COLOR_GRAY + ">");
+ buffer.append(
+ String.format(
+ Locale.US,
+ "audioPtsMs: %d (%d, %d)<br>",
+ audioPtsUs / 1000,
+ audioPtsUsRate / 1000,
+ (audioPtsUs - audioPositionUs) / 1000));
+ buffer.append(
+ String.format(
+ Locale.US,
+ "videoPtsMs: %d (%d, %d)<br>",
+ videoPtsUs / 1000,
+ videoPtsUsRate / 1000,
+ (videoPtsUs - audioPositionUs) / 1000));
+ buffer.append("</font>\n");
+
+ appendStatusLine(buffer, "KbytesInQueue", bytesInQueue / 1000, 1, 10);
+ buffer.append("<br/>");
+ appendErrorStatusLine(buffer, "videoFrameDrop", videoFrameDrop, 0, 2);
+ buffer.append("<br/>");
+ appendStatusLine(
+ buffer,
+ "packetsPerSec",
+ packetsPerSec,
+ PACKETS_PER_SEC_RED,
+ PACKETS_PER_SEC_YELLOW);
+ return buffer.toString();
+ }
+
+ /** Returns audio unavailable warning message in HTML. */
+ public static String getAudioWarningInHTML(String msg) {
+ return String.format("<font color=%s>%s</font>\n", COLOR_YELLOW, msg);
+ }
+
+ private static void appendStatusLine(
+ StringBuffer buffer, String factorName, long value, int minRed, int minYellow) {
+ buffer.append("<font color=");
+ if (value <= minRed) {
+ buffer.append(COLOR_RED);
+ } else if (value <= minYellow) {
+ buffer.append(COLOR_YELLOW);
+ } else {
+ buffer.append(COLOR_GREEN);
+ }
+ buffer.append(">");
+ buffer.append(factorName);
+ buffer.append(" : ");
+ buffer.append(value);
+ buffer.append("</font>");
+ }
+
+ private static void appendErrorStatusLine(
+ StringBuffer buffer, String factorName, int value, int minGreen, int minYellow) {
+ buffer.append("<font color=");
+ if (value <= minGreen) {
+ buffer.append(COLOR_GREEN);
+ } else if (value <= minYellow) {
+ buffer.append(COLOR_YELLOW);
+ } else {
+ buffer.append(COLOR_RED);
+ }
+ buffer.append(">");
+ buffer.append(factorName);
+ buffer.append(" : ");
+ buffer.append(value);
+ buffer.append("</font>");
+ }
+}
diff --git a/src/com/android/tv/tuner/util/SystemPropertiesProxy.java b/src/com/android/tv/tuner/util/SystemPropertiesProxy.java
new file mode 100644
index 00000000..5c23c797
--- /dev/null
+++ b/src/com/android/tv/tuner/util/SystemPropertiesProxy.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.util;
+
+import android.util.Log;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+/**
+ * Proxy class that gives an access to a hidden API {@link android.os.SystemProperties#getBoolean}.
+ */
+public class SystemPropertiesProxy {
+ private static final String TAG = "SystemPropertiesProxy";
+
+ private SystemPropertiesProxy() {}
+
+ public static boolean getBoolean(String key, boolean def) throws IllegalArgumentException {
+ try {
+ Class SystemPropertiesClass = Class.forName("android.os.SystemProperties");
+ Method getBooleanMethod =
+ SystemPropertiesClass.getDeclaredMethod(
+ "getBoolean", String.class, boolean.class);
+ getBooleanMethod.setAccessible(true);
+ return (boolean) getBooleanMethod.invoke(SystemPropertiesClass, key, def);
+ } catch (InvocationTargetException
+ | IllegalAccessException
+ | NoSuchMethodException
+ | ClassNotFoundException e) {
+ Log.e(TAG, "Failed to invoke SystemProperties.getBoolean()", e);
+ }
+ return def;
+ }
+
+ public static int getInt(String key, int def) throws IllegalArgumentException {
+ try {
+ Class SystemPropertiesClass = Class.forName("android.os.SystemProperties");
+ Method getIntMethod =
+ SystemPropertiesClass.getDeclaredMethod("getInt", String.class, int.class);
+ getIntMethod.setAccessible(true);
+ return (int) getIntMethod.invoke(SystemPropertiesClass, key, def);
+ } catch (InvocationTargetException
+ | IllegalAccessException
+ | NoSuchMethodException
+ | ClassNotFoundException e) {
+ Log.e(TAG, "Failed to invoke SystemProperties.getInt()", e);
+ }
+ 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/TisConfiguration.java b/src/com/android/tv/tuner/util/TisConfiguration.java
new file mode 100644
index 00000000..8f1326ce
--- /dev/null
+++ b/src/com/android/tv/tuner/util/TisConfiguration.java
@@ -0,0 +1,20 @@
+package com.android.tv.tuner.util;
+
+import android.content.Context;
+
+/** A helper class of tuner configuration. */
+public class TisConfiguration {
+ private static final String LC_PACKAGE_NAME = "com.android.tv";
+
+ public static boolean isPackagedWithLiveChannels(Context context) {
+ return (LC_PACKAGE_NAME.equals(context.getPackageName()));
+ }
+
+ public static boolean isInternalTunerTvInput(Context context) {
+ return (!LC_PACKAGE_NAME.equals(context.getPackageName()));
+ }
+
+ public static int getTunerHwDeviceId(Context context) {
+ return 0; // FIXME: Make it OEM configurable
+ }
+}
diff --git a/src/com/android/tv/tuner/util/TunerInputInfoUtils.java b/src/com/android/tv/tuner/util/TunerInputInfoUtils.java
new file mode 100644
index 00000000..9a3eec2b
--- /dev/null
+++ b/src/com/android/tv/tuner/util/TunerInputInfoUtils.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.util;
+
+import android.annotation.TargetApi;
+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.util.Log;
+import android.util.Pair;
+import com.android.tv.common.feature.CommonFeatures;
+import com.android.tv.tuner.R;
+import com.android.tv.tuner.TunerHal;
+import com.android.tv.tuner.tvinput.TunerTvInputService;
+
+/** Utility class for providing tuner input info. */
+public class TunerInputInfoUtils {
+ private static final String TAG = "TunerInputInfoUtils";
+ private static final boolean DEBUG = false;
+
+ /** Builds tuner input's info. */
+ @Nullable
+ @TargetApi(Build.VERSION_CODES.N)
+ public static TvInputInfo buildTunerInputInfo(Context context) {
+ Pair<Integer, Integer> tunerTypeAndCount = TunerHal.getTunerTypeAndCount(context);
+ if (tunerTypeAndCount.first == null || tunerTypeAndCount.second == 0) {
+ return null;
+ }
+ 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 {
+ 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 (IllegalArgumentException | NullPointerException e) {
+ // TunerTvInputService is not enabled.
+ return null;
+ }
+ }
+
+ /**
+ * Updates tuner input's info.
+ *
+ * @param context {@link Context} instance
+ */
+ public static void updateTunerInputInfo(Context context) {
+ 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);
+ }
+
+ @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();
+ }
+ }
+}
diff --git a/src/com/android/tv/ui/AppLayerTvView.java b/src/com/android/tv/ui/AppLayerTvView.java
index b2be9f02..5454132a 100644
--- a/src/com/android/tv/ui/AppLayerTvView.java
+++ b/src/com/android/tv/ui/AppLayerTvView.java
@@ -21,8 +21,8 @@ import android.media.tv.TvView;
import android.util.AttributeSet;
import android.view.SurfaceView;
import android.view.View;
-import com.android.tv.common.util.CommonUtils;
-import com.android.tv.common.util.Debug;
+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.
@@ -53,7 +53,7 @@ public class AppLayerTvView extends TvView {
public void onViewAdded(View child) {
if (child instanceof SurfaceView) {
// Note: See b/29118070 for detail.
- ((SurfaceView) child).setSecure(!CommonUtils.isDeveloper());
+ ((SurfaceView) child).setSecure(!Utils.isDeveloper());
}
super.onViewAdded(child);
}
diff --git a/src/com/android/tv/ui/ChannelBannerView.java b/src/com/android/tv/ui/ChannelBannerView.java
index df487bb5..c8c3dc86 100644
--- a/src/com/android/tv/ui/ChannelBannerView.java
+++ b/src/com/android/tv/ui/ChannelBannerView.java
@@ -47,7 +47,7 @@ import android.widget.RelativeLayout;
import android.widget.TextView;
import com.android.tv.MainActivity;
import com.android.tv.R;
-import com.android.tv.TvSingletons;
+import com.android.tv.TvApplication;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.common.feature.CommonFeatures;
import com.android.tv.data.Channel;
@@ -213,12 +213,12 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage
mMainActivity, R.animator.channel_banner_program_description_fade_out);
if (CommonFeatures.DVR.isEnabled(mMainActivity)) {
- mDvrManager = TvSingletons.getSingletons(mMainActivity).getDvrManager();
+ mDvrManager = TvApplication.getSingletons(mMainActivity).getDvrManager();
} else {
mDvrManager = null;
}
mContentRatingsManager =
- TvSingletons.getSingletons(getContext())
+ TvApplication.getSingletons(getContext())
.getTvInputManagerHelper()
.getContentRatingsManager();
diff --git a/src/com/android/tv/ui/KeypadChannelSwitchView.java b/src/com/android/tv/ui/KeypadChannelSwitchView.java
index 5919dbf1..0c3613f6 100644
--- a/src/com/android/tv/ui/KeypadChannelSwitchView.java
+++ b/src/com/android/tv/ui/KeypadChannelSwitchView.java
@@ -37,12 +37,12 @@ import android.widget.ListView;
import android.widget.TextView;
import com.android.tv.MainActivity;
import com.android.tv.R;
-import com.android.tv.TvSingletons;
+import com.android.tv.TvApplication;
import com.android.tv.analytics.Tracker;
import com.android.tv.common.SoftPreconditions;
-import com.android.tv.common.util.DurationTimer;
import com.android.tv.data.Channel;
import com.android.tv.data.ChannelNumber;
+import com.android.tv.util.DurationTimer;
import java.util.ArrayList;
import java.util.List;
@@ -116,7 +116,7 @@ public class KeypadChannelSwitchView extends LinearLayout
super(context, attrs, defStyleAttr);
mMainActivity = (MainActivity) context;
- mTracker = TvSingletons.getSingletons(context).getTracker();
+ mTracker = TvApplication.getSingletons(context).getTracker();
Resources resources = getResources();
mLayoutInflater = LayoutInflater.from(context);
mShowDurationMillis = resources.getInteger(R.integer.keypad_channel_switch_show_duration);
diff --git a/src/com/android/tv/ui/SelectInputView.java b/src/com/android/tv/ui/SelectInputView.java
index 2ec498a8..aa91aa50 100644
--- a/src/com/android/tv/ui/SelectInputView.java
+++ b/src/com/android/tv/ui/SelectInputView.java
@@ -32,11 +32,12 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
+import com.android.tv.ApplicationSingletons;
import com.android.tv.R;
-import com.android.tv.TvSingletons;
+import com.android.tv.TvApplication;
import com.android.tv.analytics.Tracker;
-import com.android.tv.common.util.DurationTimer;
import com.android.tv.data.Channel;
+import com.android.tv.util.DurationTimer;
import com.android.tv.util.TvInputManagerHelper;
import java.util.ArrayList;
import java.util.Collections;
@@ -143,9 +144,9 @@ public class SelectInputView extends VerticalGridView
super(context, attrs, defStyleAttr);
setAdapter(new InputListAdapter());
- TvSingletons tvSingletons = TvSingletons.getSingletons(context);
- mTracker = tvSingletons.getTracker();
- mTvInputManagerHelper = tvSingletons.getTvInputManagerHelper();
+ ApplicationSingletons appSingletons = TvApplication.getSingletons(context);
+ mTracker = appSingletons.getTracker();
+ mTvInputManagerHelper = appSingletons.getTvInputManagerHelper();
mComparator =
new TvInputManagerHelper.HardwareInputComparator(context, mTvInputManagerHelper);
diff --git a/src/com/android/tv/ui/TunableTvView.java b/src/com/android/tv/ui/TunableTvView.java
index 36de790f..97f7c65c 100644
--- a/src/com/android/tv/ui/TunableTvView.java
+++ b/src/com/android/tv/ui/TunableTvView.java
@@ -57,18 +57,15 @@ import android.view.SurfaceView;
import android.view.View;
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.TvFeatures;
-import com.android.tv.TvSingletons;
+import com.android.tv.TvApplication;
import com.android.tv.analytics.Tracker;
import com.android.tv.common.BuildConfig;
import com.android.tv.common.feature.CommonFeatures;
-import com.android.tv.common.util.CommonUtils;
-import com.android.tv.common.util.Debug;
-import com.android.tv.common.util.DurationTimer;
-import com.android.tv.common.util.PermissionUtils;
import com.android.tv.data.Channel;
import com.android.tv.data.Program;
import com.android.tv.data.ProgramDataManager;
@@ -77,8 +74,11 @@ import com.android.tv.data.WatchedHistoryManager;
import com.android.tv.parental.ContentRatingsManager;
import com.android.tv.parental.ParentalControlSettings;
import com.android.tv.recommendation.NotificationService;
+import com.android.tv.util.Debug;
+import com.android.tv.util.DurationTimer;
import com.android.tv.util.ImageLoader;
import com.android.tv.util.NetworkUtils;
+import com.android.tv.util.PermissionUtils;
import com.android.tv.util.TvInputManagerHelper;
import com.android.tv.util.Utils;
import java.lang.annotation.Retention;
@@ -362,7 +362,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
getContext().startActivity(intent);
}
})
- .setNegativeButton(android.R.string.cancel, null)
+ .setNegativeButton(android.R.string.no, null)
.show();
}
@@ -393,7 +393,6 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
case TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING:
case TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL:
mTracker.sendChannelVideoUnavailable(mCurrentChannel, reason);
- break;
default:
// do nothing
}
@@ -455,17 +454,17 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
super(context, attrs, defStyleAttr, defStyleRes);
inflate(getContext(), R.layout.tunable_tv_view, this);
- TvSingletons tvSingletons = TvSingletons.getSingletons(context);
+ ApplicationSingletons appSingletons = TvApplication.getSingletons(context);
if (CommonFeatures.DVR.isEnabled(context)) {
- mInputSessionManager = tvSingletons.getInputSessionManager();
+ mInputSessionManager = appSingletons.getInputSessionManager();
} else {
mInputSessionManager = null;
}
- mInputManager = tvSingletons.getTvInputManagerHelper();
+ mInputManager = appSingletons.getTvInputManagerHelper();
mConnectivityManager =
(ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
mCanModifyParentalControls = PermissionUtils.hasModifyParentalControls(context);
- mTracker = tvSingletons.getTracker();
+ mTracker = appSingletons.getTracker();
mBlockScreenType = BLOCK_SCREEN_TYPE_NORMAL;
mBlockScreenView = (BlockScreenView) findViewById(R.id.block_screen);
mBlockScreenView.addInfoFadeInAnimationListener(
@@ -1082,7 +1081,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
}
private boolean closePipIfNeeded() {
- if (TvFeatures.PICTURE_IN_PICTURE.isEnabled(getContext())
+ if (Features.PICTURE_IN_PICTURE.isEnabled(getContext())
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
&& ((Activity) getContext()).isInPictureInPictureMode()
&& (mScreenBlocked
@@ -1153,7 +1152,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
}
private void updateMuteStatus() {
- // Workaround: BaseTunerTvInputService uses AC3 pass-through implementation, which disables
+ // 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
@@ -1184,7 +1183,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
private boolean isBundledInput() {
return mInputInfo != null
&& mInputInfo.getType() == TvInputInfo.TYPE_TUNER
- && CommonUtils.isBundledInput(mInputInfo.getId());
+ && Utils.isBundledInput(mInputInfo.getId());
}
/** Returns true if this view is faded out. */
diff --git a/src/com/android/tv/ui/TvOverlayManager.java b/src/com/android/tv/ui/TvOverlayManager.java
index 5daa525a..58ff8c2f 100644
--- a/src/com/android/tv/ui/TvOverlayManager.java
+++ b/src/com/android/tv/ui/TvOverlayManager.java
@@ -32,13 +32,14 @@ import android.util.Log;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.ViewGroup;
+import com.android.tv.ApplicationSingletons;
import com.android.tv.ChannelTuner;
import com.android.tv.MainActivity;
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.TvSingletons;
import com.android.tv.analytics.Tracker;
import com.android.tv.common.WeakHandler;
import com.android.tv.common.feature.CommonFeatures;
@@ -231,7 +232,7 @@ public class TvOverlayManager {
ProgramGuideSearchFragment searchFragment) {
mMainActivity = mainActivity;
mChannelTuner = channelTuner;
- TvSingletons singletons = TvSingletons.getSingletons(mainActivity);
+ ApplicationSingletons singletons = TvApplication.getSingletons(mainActivity);
mChannelDataManager = singletons.getChannelDataManager();
mInputManager = singletons.getTvInputManagerHelper();
mTvView = tvView;
@@ -708,10 +709,6 @@ public class TvOverlayManager {
}
}
- public boolean isOverlayOpened() {
- return mOpenedOverlays != OVERLAY_TYPE_NONE;
- }
-
/** Hides all the opened overlays according to the flags. */
// TODO: Add test for this method.
public void hideOverlays(@HideOverlayFlag int flags) {
@@ -1018,10 +1015,6 @@ public class TvOverlayManager {
// Do not handle media key when any pop-ups which can handle keys are active.
return MainActivity.KEY_EVENT_HANDLER_RESULT_HANDLED;
}
- if (mTvView.isScreenBlocked()) {
- // Do not handle media key when screen is blocked.
- return MainActivity.KEY_EVENT_HANDLER_RESULT_HANDLED;
- }
TimeShiftManager timeShiftManager = mMainActivity.getTimeShiftManager();
if (!timeShiftManager.isAvailable()) {
return MainActivity.KEY_EVENT_HANDLER_RESULT_HANDLED;
diff --git a/src/com/android/tv/ui/TvViewUiManager.java b/src/com/android/tv/ui/TvViewUiManager.java
index 7e354db3..23cd9718 100644
--- a/src/com/android/tv/ui/TvViewUiManager.java
+++ b/src/com/android/tv/ui/TvViewUiManager.java
@@ -42,8 +42,8 @@ import android.view.ViewGroup.LayoutParams;
import android.view.ViewGroup.MarginLayoutParams;
import android.view.animation.AnimationUtils;
import android.widget.FrameLayout;
+import com.android.tv.Features;
import com.android.tv.R;
-import com.android.tv.TvFeatures;
import com.android.tv.TvOptionsManager;
import com.android.tv.data.DisplayMode;
import com.android.tv.util.TvSettings;
@@ -93,7 +93,7 @@ public class TvViewUiManager {
mTvView.setLayoutParams(mTvViewFrame);
// Smooth PIP size change, we don't change surface size when
// isInPictureInPictureMode is true.
- if (!TvFeatures.PICTURE_IN_PICTURE.isEnabled(mContext)
+ if (!Features.PICTURE_IN_PICTURE.isEnabled(mContext)
|| (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
&& !((Activity) mContext).isInPictureInPictureMode())) {
mTvView.setFixedSurfaceSize(
diff --git a/src/com/android/tv/ui/sidepanel/CustomizeChannelListFragment.java b/src/com/android/tv/ui/sidepanel/CustomizeChannelListFragment.java
index 8660c830..ad2f13f1 100644
--- a/src/com/android/tv/ui/sidepanel/CustomizeChannelListFragment.java
+++ b/src/com/android/tv/ui/sidepanel/CustomizeChannelListFragment.java
@@ -28,7 +28,7 @@ import android.view.ViewGroup;
import android.widget.TextView;
import com.android.tv.MainActivity;
import com.android.tv.R;
-import com.android.tv.common.util.SharedPreferencesUtils;
+import com.android.tv.common.SharedPreferencesUtils;
import com.android.tv.data.Channel;
import com.android.tv.data.ChannelNumber;
import com.android.tv.ui.OnRepeatedKeyInterceptListener;
diff --git a/src/com/android/tv/ui/sidepanel/DeveloperOptionFragment.java b/src/com/android/tv/ui/sidepanel/DeveloperOptionFragment.java
index dd42a728..766e206f 100644
--- a/src/com/android/tv/ui/sidepanel/DeveloperOptionFragment.java
+++ b/src/com/android/tv/ui/sidepanel/DeveloperOptionFragment.java
@@ -16,21 +16,11 @@
package com.android.tv.ui.sidepanel;
-import android.accounts.Account;
-import android.app.Activity;
-import android.support.annotation.NonNull;
-import android.util.Log;
-import android.widget.Toast;
-
-
-
-
import com.android.tv.R;
-import com.android.tv.TvSingletons;
-import com.android.tv.common.CommonPreferences;
+import com.android.tv.TvApplication;
import com.android.tv.common.feature.CommonFeatures;
-import com.android.tv.common.util.CommonUtils;
-
+import com.android.tv.tuner.TunerPreferences;
+import com.android.tv.util.Utils;
import java.util.ArrayList;
import java.util.List;
@@ -61,7 +51,7 @@ public class DeveloperOptionFragment extends SideFragment {
}
});
}
- if (CommonUtils.isDeveloper()) {
+ if (Utils.isDeveloper()) {
items.add(
new ActionItem(getString(R.string.dev_item_watch_history)) {
@Override
@@ -78,21 +68,21 @@ public class DeveloperOptionFragment extends SideFragment {
@Override
protected void onUpdate() {
super.onUpdate();
- setChecked(CommonPreferences.getStoreTsStream(getContext()));
+ setChecked(TunerPreferences.getStoreTsStream(getContext()));
}
@Override
protected void onSelected() {
super.onSelected();
- CommonPreferences.setStoreTsStream(getContext(), isChecked());
+ TunerPreferences.setStoreTsStream(getContext(), isChecked());
}
});
- if (CommonUtils.isDeveloper()) {
+ if (Utils.isDeveloper()) {
items.add(
new ActionItem(getString(R.string.dev_item_show_performance_monitor_log)) {
@Override
protected void onSelected() {
- TvSingletons.getSingletons(getContext())
+ TvApplication.getSingletons(getContext())
.getPerformanceMonitor()
.startPerformanceMonitorEventDebugActivity(getContext());
}
diff --git a/src/com/android/tv/ui/sidepanel/SettingsFragment.java b/src/com/android/tv/ui/sidepanel/SettingsFragment.java
index 31d00fa6..2552b5dd 100644
--- a/src/com/android/tv/ui/sidepanel/SettingsFragment.java
+++ b/src/com/android/tv/ui/sidepanel/SettingsFragment.java
@@ -16,7 +16,7 @@
package com.android.tv.ui.sidepanel;
-import static com.android.tv.TvFeatures.TUNER;
+import static com.android.tv.Features.TUNER;
import android.app.ApplicationErrorReport;
import android.content.Intent;
@@ -26,13 +26,13 @@ import android.widget.Toast;
import com.android.tv.MainActivity;
import com.android.tv.R;
import com.android.tv.TvApplication;
-import com.android.tv.TvSingletons;
-import com.android.tv.common.CommonPreferences;
-import com.android.tv.common.customization.CustomizationManager;
-import com.android.tv.common.util.PermissionUtils;
+import com.android.tv.customization.TvCustomizationManager;
import com.android.tv.dialog.PinDialogFragment;
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;
@@ -82,9 +82,7 @@ public class SettingsFragment extends SideFragment {
items.add(customizeChannelListItem);
final MainActivity activity = getMainActivity();
boolean hasNewInput =
- TvSingletons.getSingletons(getContext())
- .getSetupUtils()
- .hasNewInput(activity.getTvInputManagerHelper());
+ SetupUtils.getInstance(activity).hasNewInput(activity.getTvInputManagerHelper());
items.add(
new ActionItem(
getString(R.string.settings_channel_source_item_setup),
@@ -129,7 +127,7 @@ public class SettingsFragment extends SideFragment {
boolean showTrickplaySetting = false;
if (TUNER.isEnabled(getContext())) {
for (TvInputInfo inputInfo :
- TvSingletons.getSingletons(getContext())
+ TvApplication.getSingletons(getContext())
.getTvInputManagerHelper()
.getTvInputInfos(true, true)) {
if (Utils.isInternalTvInput(getContext(), inputInfo.getId())) {
@@ -139,8 +137,8 @@ public class SettingsFragment extends SideFragment {
}
if (showTrickplaySetting) {
showTrickplaySetting =
- CustomizationManager.getTrickplayMode(getContext())
- == CustomizationManager.TRICKPLAY_MODE_ENABLED;
+ TvCustomizationManager.getTrickplayMode(getContext())
+ == TvCustomizationManager.TRICKPLAY_MODE_ENABLED;
}
}
if (showTrickplaySetting) {
@@ -154,20 +152,20 @@ public class SettingsFragment extends SideFragment {
protected void onUpdate() {
super.onUpdate();
boolean enabled =
- CommonPreferences.getTrickplaySetting(getContext())
- != CommonPreferences.TRICKPLAY_SETTING_DISABLED;
+ TunerPreferences.getTrickplaySetting(getContext())
+ != TunerPreferences.TRICKPLAY_SETTING_DISABLED;
setChecked(enabled);
}
@Override
protected void onSelected() {
super.onSelected();
- @CommonPreferences.TrickplaySetting
+ @TunerPreferences.TrickplaySetting
int setting =
isChecked()
- ? CommonPreferences.TRICKPLAY_SETTING_ENABLED
- : CommonPreferences.TRICKPLAY_SETTING_DISABLED;
- CommonPreferences.setTrickplaySetting(getContext(), setting);
+ ? TunerPreferences.TRICKPLAY_SETTING_ENABLED
+ : TunerPreferences.TRICKPLAY_SETTING_DISABLED;
+ TunerPreferences.setTrickplaySetting(getContext(), setting);
}
});
}
diff --git a/src/com/android/tv/ui/sidepanel/SideFragment.java b/src/com/android/tv/ui/sidepanel/SideFragment.java
index 2902ea7f..0660a6f9 100644
--- a/src/com/android/tv/ui/sidepanel/SideFragment.java
+++ b/src/com/android/tv/ui/sidepanel/SideFragment.java
@@ -30,13 +30,13 @@ import android.widget.FrameLayout;
import android.widget.TextView;
import com.android.tv.MainActivity;
import com.android.tv.R;
-import com.android.tv.TvSingletons;
+import com.android.tv.TvApplication;
import com.android.tv.analytics.HasTrackerLabel;
import com.android.tv.analytics.Tracker;
-import com.android.tv.common.util.DurationTimer;
-import com.android.tv.common.util.SystemProperties;
import com.android.tv.data.ChannelDataManager;
import com.android.tv.data.ProgramDataManager;
+import com.android.tv.util.DurationTimer;
+import com.android.tv.util.SystemProperties;
import com.android.tv.util.ViewCache;
import java.util.List;
@@ -85,7 +85,7 @@ public abstract class SideFragment<T extends Item> extends Fragment implements H
super.onAttach(context);
mChannelDataManager = getMainActivity().getChannelDataManager();
mProgramDataManager = getMainActivity().getProgramDataManager();
- mTracker = TvSingletons.getSingletons(context).getTracker();
+ mTracker = TvApplication.getSingletons(context).getTracker();
}
@Override
diff --git a/src/com/android/tv/ui/sidepanel/parentalcontrols/ChannelsBlockedFragment.java b/src/com/android/tv/ui/sidepanel/parentalcontrols/ChannelsBlockedFragment.java
index d70cf97a..f0396bfe 100644
--- a/src/com/android/tv/ui/sidepanel/parentalcontrols/ChannelsBlockedFragment.java
+++ b/src/com/android/tv/ui/sidepanel/parentalcontrols/ChannelsBlockedFragment.java
@@ -19,8 +19,6 @@ package com.android.tv.ui.sidepanel.parentalcontrols;
import android.database.ContentObserver;
import android.media.tv.TvContract;
import android.net.Uri;
-import android.os.Build.VERSION;
-import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.os.Handler;
import android.support.v17.leanback.widget.VerticalGridView;
@@ -32,7 +30,6 @@ import android.widget.TextView;
import com.android.tv.R;
import com.android.tv.data.Channel;
import com.android.tv.data.ChannelNumber;
-import com.android.tv.recommendation.ChannelPreviewUpdater;
import com.android.tv.ui.OnRepeatedKeyInterceptListener;
import com.android.tv.ui.sidepanel.ActionItem;
import com.android.tv.ui.sidepanel.ChannelCheckItem;
@@ -50,7 +47,6 @@ public class ChannelsBlockedFragment extends SideFragment {
private final List<Channel> mChannels = new ArrayList<>();
private long mLastFocusedChannelId = Channel.INVALID_ID;
private int mSelectedPosition = INVALID_POSITION;
- private boolean mUpdated;
private final ContentObserver mProgramUpdateObserver =
new ContentObserver(new Handler()) {
@Override
@@ -98,7 +94,6 @@ public class ChannelsBlockedFragment extends SideFragment {
.registerContentObserver(
TvContract.Programs.CONTENT_URI, true, mProgramUpdateObserver);
getMainActivity().startShrunkenTvView(true, true);
- mUpdated = false;
return view;
}
@@ -107,10 +102,6 @@ public class ChannelsBlockedFragment extends SideFragment {
getActivity().getContentResolver().unregisterContentObserver(mProgramUpdateObserver);
getChannelDataManager().applyUpdatedValuesToDb();
getMainActivity().endShrunkenTvView();
- if (VERSION.SDK_INT >= VERSION_CODES.O && mUpdated) {
- ChannelPreviewUpdater.getInstance(getMainActivity())
- .updatePreviewDataForChannelsImmediately();
- }
super.onDestroyView();
}
@@ -194,7 +185,6 @@ public class ChannelsBlockedFragment extends SideFragment {
}
mBlockedChannelCount = lock ? mChannels.size() : 0;
notifyItemsChanged();
- mUpdated = true;
}
@Override
@@ -238,7 +228,6 @@ public class ChannelsBlockedFragment extends SideFragment {
getChannelDataManager().updateLocked(getChannel().getId(), isChecked());
mBlockedChannelCount += isChecked() ? 1 : -1;
notifyItemChanged(mLockAllItem);
- mUpdated = true;
}
@Override
diff --git a/src/com/android/tv/ui/sidepanel/parentalcontrols/RatingsFragment.java b/src/com/android/tv/ui/sidepanel/parentalcontrols/RatingsFragment.java
index 128fcd1a..882843c2 100644
--- a/src/com/android/tv/ui/sidepanel/parentalcontrols/RatingsFragment.java
+++ b/src/com/android/tv/ui/sidepanel/parentalcontrols/RatingsFragment.java
@@ -26,8 +26,8 @@ import android.widget.CompoundButton;
import android.widget.ImageView;
import com.android.tv.MainActivity;
import com.android.tv.R;
-import com.android.tv.common.experiments.Experiments;
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;
diff --git a/src/com/android/tv/util/account/AccountHelperImpl.java b/src/com/android/tv/util/AccountHelper.java
index 58fbd27e..a3e6ad58 100644
--- a/src/com/android/tv/util/account/AccountHelperImpl.java
+++ b/src/com/android/tv/util/AccountHelper.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2017 The Android Open Source Project
+ * 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.
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.tv.util.account;
+package com.android.tv.util;
import android.accounts.Account;
import android.content.Context;
@@ -23,23 +23,24 @@ import android.preference.PreferenceManager;
import android.support.annotation.Nullable;
/** Helper methods for getting and selecting a user account. */
-public class AccountHelperImpl implements com.android.tv.util.account.AccountHelper {
+public class AccountHelper {
+ private static final String TAG = "AccountHelper";
+ private static final boolean DEBUG = false;
private static final String SELECTED_ACCOUNT = "android.tv.livechannels.selected_account";
- protected final Context mContext;
+ private final Context mContext;
private final SharedPreferences mDefaultPreferences;
@Nullable private Account mSelectedAccount;
- public AccountHelperImpl(Context context) {
+ public AccountHelper(Context context) {
mContext = context.getApplicationContext();
mDefaultPreferences = PreferenceManager.getDefaultSharedPreferences(mContext);
}
/** Returns the currently selected account or {@code null} if none is selected. */
- @Override
@Nullable
- public final Account getSelectedAccount() {
+ public Account getSelectedAccount() {
String accountId = mDefaultPreferences.getString(SELECTED_ACCOUNT, null);
if (accountId == null) {
return null;
@@ -56,12 +57,8 @@ public class AccountHelperImpl implements com.android.tv.util.account.AccountHel
return mSelectedAccount;
}
- /**
- * Returns all eligible accounts.
- *
- * <p>Override this method to return the accounts needed.
- */
- protected Account[] getEligibleAccounts() {
+ /** Returns all eligible accounts . */
+ private Account[] getEligibleAccounts() {
return new Account[0];
}
@@ -70,9 +67,8 @@ public class AccountHelperImpl implements com.android.tv.util.account.AccountHel
*
* @return selected account or {@code null} if none is selected.
*/
- @Override
@Nullable
- public final Account selectFirstAccount() {
+ public Account selectFirstAccount() {
Account account = getFirstEligibleAccount();
if (account != null) {
selectAccount(account);
@@ -85,9 +81,8 @@ public class AccountHelperImpl implements com.android.tv.util.account.AccountHel
*
* @return first account or {@code null} if none is eligible.
*/
- @Override
@Nullable
- public final Account getFirstEligibleAccount() {
+ public Account getFirstEligibleAccount() {
Account[] accounts = getEligibleAccounts();
return accounts.length == 0 ? null : accounts[0];
}
diff --git a/src/com/android/tv/util/AsyncDbTask.java b/src/com/android/tv/util/AsyncDbTask.java
index b575df53..376fcc70 100644
--- a/src/com/android/tv/util/AsyncDbTask.java
+++ b/src/com/android/tv/util/AsyncDbTask.java
@@ -28,7 +28,6 @@ import android.support.annotation.WorkerThread;
import android.util.Log;
import android.util.Range;
import com.android.tv.common.SoftPreconditions;
-import com.android.tv.common.concurrent.NamedThreadFactory;
import com.android.tv.data.Channel;
import com.android.tv.data.Program;
import com.android.tv.dvr.data.RecordedProgram;
@@ -48,7 +47,6 @@ import java.util.concurrent.RejectedExecutionException;
* @param <Progress> the type of the progress units published during the background computation.
* @param <Result> the type of the result of the background computation.
*/
-@SuppressWarnings("TryWithResources") // TODO(b/62143348): remove when error prone check fixed
public abstract class AsyncDbTask<Params, Progress, Result>
extends AsyncTask<Params, Progress, Result> {
private static final String TAG = "AsyncDbTask";
@@ -151,7 +149,7 @@ public abstract class AsyncDbTask<Params, Progress, Result>
return null;
}
} catch (Exception e) {
- SoftPreconditions.warn(TAG, null, e, "Error querying " + this);
+ SoftPreconditions.warn(TAG, null, "Error querying " + this, e);
return null;
}
}
diff --git a/src/com/android/tv/util/BitmapUtils.java b/src/com/android/tv/util/BitmapUtils.java
index 4c67d934..6902a6fe 100644
--- a/src/com/android/tv/util/BitmapUtils.java
+++ b/src/com/android/tv/util/BitmapUtils.java
@@ -29,7 +29,6 @@ import android.net.Uri;
import android.support.annotation.NonNull;
import android.text.TextUtils;
import android.util.Log;
-import com.android.tv.common.util.NetworkTrafficTags;
import java.io.BufferedInputStream;
import java.io.Closeable;
import java.io.IOException;
diff --git a/src/com/android/tv/util/Clock.java b/src/com/android/tv/util/Clock.java
new file mode 100644
index 00000000..0004a669
--- /dev/null
+++ b/src/com/android/tv/util/Clock.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.util;
+
+import android.os.SystemClock;
+
+/**
+ * An interface through which system clocks can be read. The {@link #SYSTEM} implementation must be
+ * used for all non-test cases.
+ */
+public interface Clock {
+ /**
+ * Returns the current time in milliseconds since January 1, 1970 00:00:00.0 UTC. See {@link
+ * System#currentTimeMillis()}.
+ */
+ long currentTimeMillis();
+
+ /**
+ * Returns milliseconds since boot, including time spent in sleep.
+ *
+ * @see SystemClock#elapsedRealtime()
+ */
+ long elapsedRealtime();
+
+ /**
+ * Waits a given number of milliseconds (of uptimeMillis) before returning.
+ *
+ * @param ms to sleep before returning, in milliseconds of uptime.
+ * @see SystemClock#sleep(long)
+ */
+ void sleep(long ms);
+
+ /** The default implementation of Clock. */
+ Clock SYSTEM =
+ new Clock() {
+ @Override
+ public long currentTimeMillis() {
+ return System.currentTimeMillis();
+ }
+
+ @Override
+ public long elapsedRealtime() {
+ return SystemClock.elapsedRealtime();
+ }
+
+ @Override
+ public void sleep(long ms) {
+ SystemClock.sleep(ms);
+ }
+ };
+}
diff --git a/src/com/android/tv/util/Debug.java b/src/com/android/tv/util/Debug.java
new file mode 100644
index 00000000..422a61e3
--- /dev/null
+++ b/src/com/android/tv/util/Debug.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.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 static final 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/util/DurationTimer.java b/src/com/android/tv/util/DurationTimer.java
new file mode 100644
index 00000000..6aabf37b
--- /dev/null
+++ b/src/com/android/tv/util/DurationTimer.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.util;
+
+import android.os.SystemClock;
+import android.util.Log;
+import com.android.tv.common.BuildConfig;
+
+/** Times a duration. */
+public final class DurationTimer {
+ private static final String TAG = "DurationTimer";
+ public static final long TIME_NOT_SET = -1;
+
+ private long 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 mStartTimeMs != TIME_NOT_SET;
+ }
+
+ /** Start the timer. */
+ public void start() {
+ mStartTimeMs = SystemClock.elapsedRealtime();
+ }
+
+ /** Returns true if timer is started. */
+ public boolean isStarted() {
+ return mStartTimeMs != TIME_NOT_SET;
+ }
+
+ /**
+ * Returns the current duration in milliseconds or {@link #TIME_NOT_SET} if the timer is not
+ * running.
+ */
+ public long getDuration() {
+ return isRunning() ? SystemClock.elapsedRealtime() - mStartTimeMs : TIME_NOT_SET;
+ }
+
+ /**
+ * Stops the timer and resets its value to {@link #TIME_NOT_SET}.
+ *
+ * @return the current duration in milliseconds or {@link #TIME_NOT_SET} if the timer is not
+ * running.
+ */
+ public long reset() {
+ long duration = getDuration();
+ 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 32ac89f0..9b4d2a70 100644
--- a/src/com/android/tv/util/ImageLoader.java
+++ b/src/com/android/tv/util/ImageLoader.java
@@ -31,7 +31,6 @@ import android.support.annotation.WorkerThread;
import android.util.ArraySet;
import android.util.Log;
import com.android.tv.R;
-import com.android.tv.common.concurrent.NamedThreadFactory;
import com.android.tv.util.BitmapUtils.ScaledBitmapInfo;
import java.lang.ref.WeakReference;
import java.util.HashMap;
diff --git a/src/com/android/tv/util/LocationUtils.java b/src/com/android/tv/util/LocationUtils.java
new file mode 100644
index 00000000..a960c616
--- /dev/null
+++ b/src/com/android/tv/util/LocationUtils.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.util;
+
+import android.content.Context;
+import android.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;
+import java.util.Locale;
+
+/** A utility class to get the current location. */
+public class LocationUtils {
+ private static final String TAG = "LocationUtils";
+ private static final boolean DEBUG = false;
+
+ private static Context sApplicationContext;
+ private static Address sAddress;
+ private static String sCountry;
+ private static IOException sError;
+
+ /** Checks the current location. */
+ public static synchronized Address getCurrentAddress(Context context)
+ throws IOException, SecurityException {
+ if (sAddress != null) {
+ return sAddress;
+ }
+ if (sError != null) {
+ throw sError;
+ }
+ if (sApplicationContext == null) {
+ sApplicationContext = context.getApplicationContext();
+ }
+ LocationUtilsHelper.startLocationUpdates();
+ 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) {
+ return;
+ }
+ Geocoder geocoder = new Geocoder(sApplicationContext, Locale.getDefault());
+ try {
+ List<Address> addresses =
+ geocoder.getFromLocation(location.getLatitude(), location.getLongitude(), 1);
+ 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");
+ }
+ sError = null;
+ } catch (IOException e) {
+ Log.w(TAG, "Error in updating address", e);
+ sError = e;
+ }
+ }
+
+ private LocationUtils() {}
+
+ private static class LocationUtilsHelper {
+ private static final LocationListener LOCATION_LISTENER =
+ new LocationListener() {
+ @Override
+ public void onLocationChanged(Location location) {
+ updateAddress(location);
+ }
+
+ @Override
+ public void onStatusChanged(String provider, int status, Bundle extras) {}
+
+ @Override
+ public void onProviderEnabled(String provider) {}
+
+ @Override
+ public void onProviderDisabled(String provider) {}
+ };
+
+ private static LocationManager sLocationManager;
+
+ public static void startLocationUpdates() {
+ if (sLocationManager == null) {
+ sLocationManager =
+ (LocationManager)
+ sApplicationContext.getSystemService(Context.LOCATION_SERVICE);
+ try {
+ sLocationManager.requestLocationUpdates(
+ LocationManager.NETWORK_PROVIDER, 1000, 10, LOCATION_LISTENER, null);
+ } catch (SecurityException e) {
+ // Enables requesting the location updates again.
+ sLocationManager = null;
+ throw e;
+ }
+ }
+ }
+ }
+}
diff --git a/src/com/android/tv/util/NamedThreadFactory.java b/src/com/android/tv/util/NamedThreadFactory.java
new file mode 100644
index 00000000..264b8b3f
--- /dev/null
+++ b/src/com/android/tv/util/NamedThreadFactory.java
@@ -0,0 +1,45 @@
+/*
+ * 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.support.annotation.NonNull;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/** A thread factory that creates threads with a suffix. */
+public class NamedThreadFactory implements ThreadFactory {
+ private final AtomicInteger mCount = new AtomicInteger(0);
+ private final ThreadFactory mDefaultThreadFactory;
+ private final String mPrefix;
+
+ public NamedThreadFactory(final String baseName) {
+ mDefaultThreadFactory = Executors.defaultThreadFactory();
+ mPrefix = baseName + "-";
+ }
+
+ @Override
+ public Thread newThread(@NonNull final Runnable runnable) {
+ final Thread thread = mDefaultThreadFactory.newThread(runnable);
+ thread.setName(mPrefix + mCount.getAndIncrement());
+ return thread;
+ }
+
+ public boolean namedWithPrefix(Thread thread) {
+ return thread.getName().startsWith(mPrefix);
+ }
+}
diff --git a/src/com/android/tv/util/NetworkTrafficTags.java b/src/com/android/tv/util/NetworkTrafficTags.java
new file mode 100644
index 00000000..85ecde5b
--- /dev/null
+++ b/src/com/android/tv/util/NetworkTrafficTags.java
@@ -0,0 +1,63 @@
+/*
+ * 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 63383aab..3b72e091 100644
--- a/src/com/android/tv/util/OnboardingUtils.java
+++ b/src/com/android/tv/util/OnboardingUtils.java
@@ -48,8 +48,8 @@ public final class OnboardingUtils {
}
/**
- * Checks if this is the first run of {@link com.android.tv.MainActivity} with the
- * current onboarding version.
+ * Checks if this is the first run of {@link com.android.tv.MainActivity} with the current
+ * onboarding version.
*/
public static boolean isFirstRunWithCurrentVersion(Context context) {
int versionCode =
@@ -59,8 +59,8 @@ public final class OnboardingUtils {
}
/**
- * Marks that the first run of {@link com.android.tv.MainActivity} with the current
- * onboarding version has been completed.
+ * Marks that the first run of {@link com.android.tv.MainActivity} with the current onboarding
+ * version has been completed.
*/
public static void setFirstRunWithCurrentVersionCompleted(Context context) {
PreferenceManager.getDefaultSharedPreferences(context)
diff --git a/src/com/android/tv/util/PermissionUtils.java b/src/com/android/tv/util/PermissionUtils.java
new file mode 100644
index 00000000..b3e4e3a2
--- /dev/null
+++ b/src/com/android/tv/util/PermissionUtils.java
@@ -0,0 +1,53 @@
+package com.android.tv.util;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+
+/** Util class to handle permissions. */
+public class PermissionUtils {
+ /** Permission to read the TV listings. */
+ public static final String PERMISSION_READ_TV_LISTINGS = "android.permission.READ_TV_LISTINGS";
+
+ private static Boolean sHasAccessAllEpgPermission;
+ private static Boolean sHasAccessWatchedHistoryPermission;
+ private static Boolean sHasModifyParentalControlsPermission;
+
+ public static boolean hasAccessAllEpg(Context context) {
+ if (sHasAccessAllEpgPermission == null) {
+ sHasAccessAllEpgPermission =
+ context.checkSelfPermission(
+ "com.android.providers.tv.permission.ACCESS_ALL_EPG_DATA")
+ == PackageManager.PERMISSION_GRANTED;
+ }
+ return sHasAccessAllEpgPermission;
+ }
+
+ public static boolean hasAccessWatchedHistory(Context context) {
+ if (sHasAccessWatchedHistoryPermission == null) {
+ sHasAccessWatchedHistoryPermission =
+ context.checkSelfPermission(
+ "com.android.providers.tv.permission.ACCESS_WATCHED_PROGRAMS")
+ == PackageManager.PERMISSION_GRANTED;
+ }
+ return sHasAccessWatchedHistoryPermission;
+ }
+
+ public static boolean hasModifyParentalControls(Context context) {
+ if (sHasModifyParentalControlsPermission == null) {
+ sHasModifyParentalControlsPermission =
+ context.checkSelfPermission("android.permission.MODIFY_PARENTAL_CONTROLS")
+ == PackageManager.PERMISSION_GRANTED;
+ }
+ return sHasModifyParentalControlsPermission;
+ }
+
+ public static boolean hasReadTvListings(Context context) {
+ 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/RecurringRunner.java b/src/com/android/tv/util/RecurringRunner.java
index 764689c2..c1b724a2 100644
--- a/src/com/android/tv/util/RecurringRunner.java
+++ b/src/com/android/tv/util/RecurringRunner.java
@@ -22,8 +22,8 @@ import android.os.AsyncTask;
import android.os.Handler;
import android.support.annotation.WorkerThread;
import android.util.Log;
+import com.android.tv.common.SharedPreferencesUtils;
import com.android.tv.common.SoftPreconditions;
-import com.android.tv.common.util.SharedPreferencesUtils;
import java.util.Date;
/**
diff --git a/src/com/android/tv/util/SetupUtils.java b/src/com/android/tv/util/SetupUtils.java
index ad5d5024..a1ff192b 100644
--- a/src/com/android/tv/util/SetupUtils.java
+++ b/src/com/android/tv/util/SetupUtils.java
@@ -28,15 +28,15 @@ import android.media.tv.TvInputManager;
import android.preference.PreferenceManager;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
-import android.support.annotation.VisibleForTesting;
import android.text.TextUtils;
import android.util.ArraySet;
import android.util.Log;
-import com.android.tv.TvSingletons;
-import com.android.tv.common.BaseApplication;
+import com.android.tv.ApplicationSingletons;
+import com.android.tv.TvApplication;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.data.Channel;
import com.android.tv.data.ChannelDataManager;
+import com.android.tv.tuner.tvinput.TunerTvInputService;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
@@ -54,8 +54,9 @@ public class SetupUtils {
// Recognized inputs means that the user already knows the inputs are installed.
private static final String PREF_KEY_RECOGNIZED_INPUTS = "recognized_inputs";
private static final String PREF_KEY_IS_FIRST_TUNE = "is_first_tune";
+ private static SetupUtils sSetupUtils;
- private final Context mContext;
+ private final TvApplication mTvApplication;
private final SharedPreferences mSharedPreferences;
private final Set<String> mKnownInputs;
private final Set<String> mSetUpInputs;
@@ -63,10 +64,9 @@ public class SetupUtils {
private boolean mIsFirstTune;
private final String mTunerInputId;
- @VisibleForTesting
- protected SetupUtils(Context context) {
- mContext = context;
- mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
+ private SetupUtils(TvApplication tvApplication) {
+ mTvApplication = tvApplication;
+ mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(tvApplication);
mSetUpInputs = new ArraySet<>();
mSetUpInputs.addAll(
mSharedPreferences.getStringSet(PREF_KEY_SET_UP_INPUTS, Collections.emptySet()));
@@ -77,16 +77,18 @@ public class SetupUtils {
mRecognizedInputs.addAll(
mSharedPreferences.getStringSet(PREF_KEY_RECOGNIZED_INPUTS, mKnownInputs));
mIsFirstTune = mSharedPreferences.getBoolean(PREF_KEY_IS_FIRST_TUNE, true);
- mTunerInputId = BaseApplication.getSingletons(context).getEmbeddedTunerInputId();
+ mTunerInputId =
+ TvContract.buildInputId(
+ new ComponentName(tvApplication, TunerTvInputService.class));
}
- /**
- * Creates an instance of {@link SetupUtils}.
- *
- * <p><b>WARNING</b> this should only be called by the top level application.
- */
- public static SetupUtils createForTvSingletons(Context context) {
- return new SetupUtils(context.getApplicationContext());
+ /** Gets an instance of {@link SetupUtils}. */
+ public static SetupUtils getInstance(Context context) {
+ if (sSetupUtils != null) {
+ return sSetupUtils;
+ }
+ sSetupUtils = new SetupUtils((TvApplication) context.getApplicationContext());
+ return sSetupUtils;
}
/** Additional work after the setup of TV input. */
@@ -97,15 +99,14 @@ public class SetupUtils {
// which one is the last callback. To reduce error prune, we update channel
// list again and make all channels of {@code inputId} browsable.
onSetupDone(inputId);
- final ChannelDataManager manager =
- TvSingletons.getSingletons(mContext).getChannelDataManager();
+ final ChannelDataManager manager = mTvApplication.getChannelDataManager();
if (!manager.isDbLoadFinished()) {
manager.addListener(
new ChannelDataManager.Listener() {
@Override
public void onLoadFinished() {
manager.removeListener(this);
- updateChannelsAfterSetup(mContext, inputId, postRunnable);
+ updateChannelsAfterSetup(mTvApplication, inputId, postRunnable);
}
@Override
@@ -115,14 +116,14 @@ public class SetupUtils {
public void onChannelBrowsableChanged() {}
});
} else {
- updateChannelsAfterSetup(mContext, inputId, postRunnable);
+ updateChannelsAfterSetup(mTvApplication, inputId, postRunnable);
}
}
private static void updateChannelsAfterSetup(
Context context, final String inputId, final Runnable postRunnable) {
- TvSingletons tvSingletons = TvSingletons.getSingletons(context);
- final ChannelDataManager manager = tvSingletons.getChannelDataManager();
+ ApplicationSingletons appSingletons = TvApplication.getSingletons(context);
+ final ChannelDataManager manager = appSingletons.getChannelDataManager();
manager.updateChannels(
new Runnable() {
@Override
@@ -158,9 +159,8 @@ public class SetupUtils {
@UiThread
public void markNewChannelsBrowsable() {
Set<String> newInputsWithChannels = new HashSet<>();
- TvSingletons singletons = TvSingletons.getSingletons(mContext);
- TvInputManagerHelper tvInputManagerHelper = singletons.getTvInputManagerHelper();
- ChannelDataManager channelDataManager = singletons.getChannelDataManager();
+ TvInputManagerHelper tvInputManagerHelper = mTvApplication.getTvInputManagerHelper();
+ ChannelDataManager channelDataManager = mTvApplication.getChannelDataManager();
SoftPreconditions.checkState(channelDataManager.isDbLoadFinished());
for (TvInputInfo input : tvInputManagerHelper.getTvInputInfos(true, true)) {
String inputId = input.getId();
@@ -340,7 +340,8 @@ public class SetupUtils {
try {
// Just after booting, input list from TvInputManager are not reliable.
// So we need to double-check package existence. b/29034900
- mContext.getPackageManager()
+ mTvApplication
+ .getPackageManager()
.getPackageInfo(
ComponentName.unflattenFromString(input).getPackageName(),
PackageManager.GET_ACTIVITIES);
diff --git a/src/com/android/tv/util/SqlParams.java b/src/com/android/tv/util/SqlParams.java
deleted file mode 100644
index c4b803b6..00000000
--- a/src/com/android/tv/util/SqlParams.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * 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.database.DatabaseUtils;
-import java.util.Arrays;
-
-/** Convenience class for SQL operations. */
-public class SqlParams {
- private String mTables;
- private String mSelection;
- private String[] mSelectionArgs;
-
- public SqlParams(String tables, String selection, String... selectionArgs) {
- setTables(tables);
- setWhere(selection, selectionArgs);
- }
-
- public String getTables() {
- return mTables;
- }
-
- public String getSelection() {
- return mSelection;
- }
-
- public String[] getSelectionArgs() {
- return mSelectionArgs;
- }
-
- public void setTables(String tables) {
- mTables = tables;
- }
-
- public void setWhere(String selection, String... selectionArgs) {
- mSelection = selection;
- mSelectionArgs = selectionArgs;
- }
-
- public void appendWhere(String selection, String... selectionArgs) {
- mSelection = DatabaseUtils.concatenateWhere(mSelection, selection);
- if (selectionArgs != null) {
- mSelectionArgs = DatabaseUtils.appendSelectionArgs(mSelectionArgs, selectionArgs);
- }
- }
-
- public void appendWhereEquals(String name, String value) {
- appendWhere(name + "=?", value);
- }
-
- @Override
- public String toString() {
- return "tables "
- + getTables()
- + " where "
- + getSelection()
- + " with "
- + Arrays.toString(getSelectionArgs());
- }
-}
diff --git a/src/com/android/tv/util/StringUtils.java b/src/com/android/tv/util/StringUtils.java
new file mode 100644
index 00000000..eeaf33a6
--- /dev/null
+++ b/src/com/android/tv/util/StringUtils.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.util;
+
+/** Utility class for handling {@link String}. */
+public final class StringUtils {
+
+ private StringUtils() {}
+
+ /** Returns compares two strings lexicographically and handles null values quietly. */
+ public static int compare(String a, String b) {
+ if (a == null) {
+ return b == null ? 0 : -1;
+ }
+ if (b == null) {
+ return 1;
+ }
+ return a.compareTo(b);
+ }
+}
diff --git a/src/com/android/tv/util/SystemProperties.java b/src/com/android/tv/util/SystemProperties.java
new file mode 100644
index 00000000..e1b8a398
--- /dev/null
+++ b/src/com/android/tv/util/SystemProperties.java
@@ -0,0 +1,53 @@
+/*
+ * 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 com.android.tv.common.BooleanSystemProperty;
+
+/** A convenience class for getting TV related system properties. */
+public final class SystemProperties {
+
+ /** Allow Google Analytics for eng builds. */
+ public static final BooleanSystemProperty ALLOW_ANALYTICS_IN_ENG =
+ new BooleanSystemProperty("tv_allow_analytics_in_eng", false);
+
+ /** Allow Strict mode for debug builds. */
+ public static final BooleanSystemProperty ALLOW_STRICT_MODE =
+ new BooleanSystemProperty("tv_allow_strict_mode", true);
+
+ /** When true {@link android.view.KeyEvent}s are logged. Defaults to false. */
+ public static final BooleanSystemProperty LOG_KEYEVENT =
+ new BooleanSystemProperty("tv_log_keyevent", false);
+ /** When true debug keys are used. Defaults to false. */
+ public static final BooleanSystemProperty USE_DEBUG_KEYS =
+ new BooleanSystemProperty("tv_use_debug_keys", false);
+
+ /** Send {@link com.android.tv.analytics.Tracker} information. Defaults to {@code true}. */
+ public static final BooleanSystemProperty USE_TRACKER =
+ new BooleanSystemProperty("tv_use_tracker", true);
+
+ static {
+ updateSystemProperties();
+ }
+
+ private SystemProperties() {}
+
+ /** Update the TV related system properties. */
+ public static void updateSystemProperties() {
+ BooleanSystemProperty.resetAll();
+ }
+}
diff --git a/src/com/android/tv/util/TvInputManagerHelper.java b/src/com/android/tv/util/TvInputManagerHelper.java
index c4feafb7..e97bc4f9 100644
--- a/src/com/android/tv/util/TvInputManagerHelper.java
+++ b/src/com/android/tv/util/TvInputManagerHelper.java
@@ -21,19 +21,17 @@ import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.graphics.drawable.Drawable;
import android.hardware.hdmi.HdmiDeviceInfo;
-import android.media.tv.TvContentRatingSystemInfo;
import android.media.tv.TvInputInfo;
import android.media.tv.TvInputManager;
import android.media.tv.TvInputManager.TvInputCallback;
import android.os.Handler;
-import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.Log;
-import com.android.tv.TvFeatures;
+import com.android.tv.Features;
import com.android.tv.common.SoftPreconditions;
-import com.android.tv.common.util.CommonUtils;
+import com.android.tv.common.TvCommonUtils;
import com.android.tv.parental.ContentRatingsManager;
import com.android.tv.parental.ParentalControlSettings;
import java.util.ArrayList;
@@ -49,58 +47,6 @@ public class TvInputManagerHelper {
private static final String TAG = "TvInputManagerHelper";
private static final boolean DEBUG = false;
- public interface TvInputManagerInterface {
- TvInputInfo getTvInputInfo(String inputId);
-
- Integer getInputState(String inputId);
-
- void registerCallback(TvInputCallback internalCallback, Handler handler);
-
- void unregisterCallback(TvInputCallback internalCallback);
-
- List<TvInputInfo> getTvInputList();
-
- List<TvContentRatingSystemInfo> getTvContentRatingSystemList();
- }
-
- private static final class TvInputManagerImpl implements TvInputManagerInterface {
- private final TvInputManager delegate;
-
- private TvInputManagerImpl(TvInputManager delegate) {
- this.delegate = delegate;
- }
-
- @Override
- public TvInputInfo getTvInputInfo(String inputId) {
- return delegate.getTvInputInfo(inputId);
- }
-
- @Override
- public Integer getInputState(String inputId) {
- return delegate.getInputState(inputId);
- }
-
- @Override
- public void registerCallback(TvInputCallback internalCallback, Handler handler) {
- delegate.registerCallback(internalCallback, handler);
- }
-
- @Override
- public void unregisterCallback(TvInputCallback internalCallback) {
- delegate.unregisterCallback(internalCallback);
- }
-
- @Override
- public List<TvInputInfo> getTvInputList() {
- return delegate.getTvInputList();
- }
-
- @Override
- public List<TvContentRatingSystemInfo> getTvContentRatingSystemList() {
- return delegate.getTvContentRatingSystemList();
- }
- }
-
/** Types of HDMI device and bundled tuner. */
public static final int TYPE_CEC_DEVICE = -2;
@@ -111,8 +57,7 @@ public class TvInputManagerHelper {
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[] mPhysicalTunerBlackList = {};
private static final String META_LABEL_SORT_KEY = "input_sort_key";
/** The default tv input priority to show. */
@@ -136,8 +81,7 @@ public class TvInputManagerHelper {
DEFAULT_TV_INPUT_PRIORITY.add(TvInputInfo.TYPE_OTHER);
}
- private static final String[] PARTNER_TUNER_INPUT_PREFIX_BLACKLIST = {
- };
+ private static final String[] PARTNER_TUNER_INPUT_PREFIX_BLACKLIST = {};
private static final String[] TESTABLE_INPUTS = {
"com.android.tv.testinput/.TestTvInputService"
@@ -145,7 +89,7 @@ public class TvInputManagerHelper {
private final Context mContext;
private final PackageManager mPackageManager;
- protected final TvInputManagerInterface mTvInputManager;
+ 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<>();
@@ -262,23 +206,10 @@ public class TvInputManagerHelper {
private final Comparator<TvInputInfo> mTvInputInfoComparator;
public TvInputManagerHelper(Context context) {
- this(context, createTvInputManagerWrapper(context));
- }
-
- @Nullable
- protected static TvInputManagerImpl createTvInputManagerWrapper(Context context) {
- TvInputManager tvInputManager =
- (TvInputManager) context.getSystemService(Context.TV_INPUT_SERVICE);
- return tvInputManager == null ? null : new TvInputManagerImpl(tvInputManager);
- }
-
- @VisibleForTesting
- protected TvInputManagerHelper(
- Context context, @Nullable TvInputManagerInterface tvInputManager) {
mContext = context.getApplicationContext();
mPackageManager = context.getPackageManager();
- mTvInputManager = tvInputManager;
- mContentRatingsManager = new ContentRatingsManager(context, tvInputManager);
+ mTvInputManager = (TvInputManager) context.getSystemService(Context.TV_INPUT_SERVICE);
+ mContentRatingsManager = new ContentRatingsManager(context);
mParentalControlSettings = new ParentalControlSettings(context);
mTvInputInfoComparator = new InputComparatorInternal(this);
}
@@ -389,7 +320,7 @@ public class TvInputManagerHelper {
/** Is the input one known bundled inputs not written by OEM/SOCs. */
public boolean isBundledInput(TvInputInfo inputInfo) {
return inputInfo != null
- && CommonUtils.isInBundledPackageSet(
+ && Utils.isInBundledPackageSet(
inputInfo.getServiceInfo().applicationInfo.packageName);
}
@@ -497,17 +428,9 @@ public class TvInputManagerHelper {
}
return size;
}
- /**
- * Returns TvInputInfo's input state.
- *
- * @param inputInfo
- * @return An Integer which stands for the input state {@link
- * TvInputManager.INPUT_STATE_DISCONNECTED} if inputInfo is null
- */
- public int getInputState(@Nullable TvInputInfo inputInfo) {
- return inputInfo == null
- ? TvInputManager.INPUT_STATE_DISCONNECTED
- : getInputState(inputInfo.getId());
+
+ public int getInputState(TvInputInfo inputInfo) {
+ return getInputState(inputInfo.getId());
}
public int getInputState(String inputId) {
@@ -578,15 +501,14 @@ public class TvInputManagerHelper {
}
private boolean isInBlackList(String inputId) {
- if (TvFeatures.USE_PARTNER_INPUT_BLACKLIST.isEnabled(mContext)) {
+ if (Features.USE_PARTNER_INPUT_BLACKLIST.isEnabled(mContext)) {
for (String disabledTunerInputPrefix : PARTNER_TUNER_INPUT_PREFIX_BLACKLIST) {
if (inputId.contains(disabledTunerInputPrefix)) {
return true;
}
}
}
- if (CommonUtils.isRoboTest()) return false;
- if (CommonUtils.isRunningInTest()) {
+ if (TvCommonUtils.isRunningInTest()) {
for (String testableInput : TESTABLE_INPUTS) {
if (testableInput.equals(inputId)) {
return false;
diff --git a/src/com/android/tv/util/Utils.java b/src/com/android/tv/util/Utils.java
index 1c8ccd5b..ac3be643 100644
--- a/src/com/android/tv/util/Utils.java
+++ b/src/com/android/tv/util/Utils.java
@@ -38,15 +38,20 @@ import android.support.annotation.VisibleForTesting;
import android.support.annotation.WorkerThread;
import android.text.TextUtils;
import android.text.format.DateUtils;
+import android.util.ArraySet;
import android.util.Log;
import android.view.View;
+import com.android.tv.ApplicationSingletons;
import com.android.tv.R;
-import com.android.tv.TvSingletons;
+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;
import java.util.ArrayList;
import java.util.Arrays;
@@ -63,11 +68,13 @@ import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
/** A class that includes convenience methods for accessing TvProvider database. */
-@SuppressWarnings("TryWithResources") // TODO(b/62143348): remove when error prone check fixed
public class Utils {
private static final String TAG = "Utils";
private static final boolean DEBUG = false;
+ private static final SimpleDateFormat ISO_8601 =
+ new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US);
+
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";
@@ -109,6 +116,15 @@ public class Utils {
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.
+ // Bundled (system) inputs not in the list will get the high priority
+ // so they and their channels come first in the UI.
+ private static final Set<String> BUNDLED_PACKAGE_SET = new ArraySet<>();
+
+ static {
+ BUNDLED_PACKAGE_SET.add("com.android.tv");
+ }
+
private enum AspectRatio {
ASPECT_RATIO_4_3(4, 3),
ASPECT_RATIO_16_9(16, 9),
@@ -645,7 +661,7 @@ public class Utils {
return null;
}
TvInputManagerHelper inputManager =
- TvSingletons.getSingletons(context).getTvInputManagerHelper();
+ TvApplication.getSingletons(context).getTvInputManagerHelper();
CharSequence customLabel = inputManager.loadCustomLabel(input);
String label = (customLabel == null) ? null : customLabel.toString();
if (TextUtils.isEmpty(label)) {
@@ -688,6 +704,11 @@ public class Utils {
return toTimeString(timeMillis, true);
}
+ /** Converts time in milliseconds to a ISO 8061 string. */
+ public static String toIsoDateTimeString(long timeMillis) {
+ return ISO_8601.format(new Date(timeMillis));
+ }
+
/**
* Returns a {@link String} object which contains the layout information of the {@code view}.
*/
@@ -754,7 +775,7 @@ public class Utils {
/** Checks where there is any internal TV input. */
public static boolean hasInternalTvInputs(Context context, boolean tunerInputOnly) {
for (TvInputInfo input :
- TvSingletons.getSingletons(context)
+ TvApplication.getSingletons(context)
.getTvInputManagerHelper()
.getTvInputInfos(true, tunerInputOnly)) {
if (isInternalTvInput(context, input.getId())) {
@@ -768,7 +789,7 @@ public class Utils {
public static List<TvInputInfo> getInternalTvInputs(Context context, boolean tunerInputOnly) {
List<TvInputInfo> inputs = new ArrayList<>();
for (TvInputInfo input :
- TvSingletons.getSingletons(context)
+ TvApplication.getSingletons(context)
.getTvInputManagerHelper()
.getTvInputInfos(true, tunerInputOnly)) {
if (isInternalTvInput(context, input.getId())) {
@@ -796,22 +817,47 @@ public class Utils {
/** Returns the TV input for the given channel ID. */
@Nullable
public static TvInputInfo getTvInputInfoForChannelId(Context context, long channelId) {
- TvSingletons tvSingletons = TvSingletons.getSingletons(context);
- Channel channel = tvSingletons.getChannelDataManager().getChannel(channelId);
+ ApplicationSingletons appSingletons = TvApplication.getSingletons(context);
+ Channel channel = appSingletons.getChannelDataManager().getChannel(channelId);
if (channel == null) {
return null;
}
- return tvSingletons.getTvInputManagerHelper().getTvInputInfo(channel.getInputId());
+ return appSingletons.getTvInputManagerHelper().getTvInputInfo(channel.getInputId());
}
/** Returns the {@link TvInputInfo} for the given input ID. */
@Nullable
public static TvInputInfo getTvInputInfoForInputId(Context context, String inputId) {
- return TvSingletons.getSingletons(context)
+ return TvApplication.getSingletons(context)
.getTvInputManagerHelper()
.getTvInputInfo(inputId);
}
+ /** Deletes a file or a directory. */
+ public static void deleteDirOrFile(File fileOrDirectory) {
+ if (fileOrDirectory.isDirectory()) {
+ for (File child : fileOrDirectory.listFiles()) {
+ deleteDirOrFile(child);
+ }
+ }
+ fileOrDirectory.delete();
+ }
+
+ /** Checks whether a given package is in our bundled package set. */
+ public static boolean isInBundledPackageSet(String packageName) {
+ return BUNDLED_PACKAGE_SET.contains(packageName);
+ }
+
+ /** Checks whether a given input is a bundled input. */
+ public static boolean isBundledInput(String inputId) {
+ for (String prefix : BUNDLED_PACKAGE_SET) {
+ if (inputId.startsWith(prefix + "/")) {
+ return true;
+ }
+ }
+ return false;
+ }
+
/** Returns the canonical genre ID's from the {@code genres}. */
public static int[] getCanonicalGenreIds(String genres) {
if (TextUtils.isEmpty(genres)) {
@@ -853,6 +899,11 @@ public class Utils {
return Genres.encode(genres);
}
+ /** Returns true if the current user is a developer. */
+ public static boolean isDeveloper() {
+ return BuildConfig.ENG || Experiments.ENABLE_DEVELOPER_FEATURES.get();
+ }
+
/**
* Runs the method in main thread. If the current thread is not main thread, block it util the
* method is finished.
diff --git a/src/com/android/tv/util/ViewCache.java b/src/com/android/tv/util/ViewCache.java
index b8bdb6b8..2d5ecfe6 100644
--- a/src/com/android/tv/util/ViewCache.java
+++ b/src/com/android/tv/util/ViewCache.java
@@ -1,18 +1,3 @@
-/*
- * 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.Context;
diff --git a/src/com/android/tv/util/account/AccountHelper.java b/src/com/android/tv/util/account/AccountHelper.java
deleted file mode 100644
index e98b42ec..00000000
--- a/src/com/android/tv/util/account/AccountHelper.java
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.tv.util.account;
-
-import android.accounts.Account;
-import android.support.annotation.Nullable;
-
-/** Helper methods for getting and selecting a user account. */
-public interface AccountHelper {
- /** Returns the currently selected account or {@code null} if none is selected. */
- @Nullable
- Account getSelectedAccount();
- /**
- * Selects the first account available.
- *
- * @return selected account or {@code null} if none is selected.
- */
- @Nullable
- Account selectFirstAccount();
-
- /** Returns all eligible accounts . */
- @Nullable
- Account getFirstEligibleAccount();
-}