From 0550a7221be0581b0bd421a9d70400ff8699a6e7 Mon Sep 17 00:00:00 2001 From: Nick Chalko Date: Tue, 9 May 2017 14:07:44 -0700 Subject: Sync to ub-tv-dev at lost+ hash 550cbec17259717c5453f6be1eb05736ba10ef1d Bug: 37849928 Test: tested on vendor branch Change-Id: I82190481d2bcef2b89e78414b6b92ed97720749d Merged-In: I4199ec04cacb4a78be58b85302a39d917658dc28 --- Android.mk | 9 +- AndroidManifest.xml | 14 +- assets/licenses.html | 1018 +++++++++++++++++++- common/res/drawable/setup_selector_background.xml | 3 +- common/res/layout/fragment_setup_multi_pane.xml | 25 +- common/res/values-af/strings.xml | 1 + common/res/values-am/strings.xml | 1 + common/res/values-ar/strings.xml | 1 + common/res/values-az-rAZ/strings.xml | 1 + common/res/values-bg/strings.xml | 1 + common/res/values-bn-rBD/strings.xml | 1 + common/res/values-ca/strings.xml | 1 + common/res/values-cs/strings.xml | 1 + common/res/values-da/strings.xml | 1 + common/res/values-de/strings.xml | 1 + common/res/values-el/strings.xml | 1 + common/res/values-en-rAU/strings.xml | 1 + common/res/values-en-rGB/strings.xml | 1 + common/res/values-en-rIN/strings.xml | 1 + common/res/values-es-rUS/strings.xml | 1 + common/res/values-es/strings.xml | 1 + common/res/values-et-rEE/strings.xml | 1 + common/res/values-eu-rES/strings.xml | 1 + common/res/values-fa/strings.xml | 1 + common/res/values-fi/strings.xml | 1 + common/res/values-fr-rCA/strings.xml | 1 + common/res/values-fr/strings.xml | 1 + common/res/values-gl-rES/strings.xml | 1 + common/res/values-hi/strings.xml | 1 + common/res/values-hr/strings.xml | 1 + common/res/values-hu/strings.xml | 1 + common/res/values-hy-rAM/strings.xml | 1 + common/res/values-in/strings.xml | 1 + common/res/values-is-rIS/strings.xml | 1 + common/res/values-it/strings.xml | 1 + common/res/values-iw/strings.xml | 1 + common/res/values-ja/strings.xml | 1 + common/res/values-ka-rGE/strings.xml | 1 + common/res/values-kk-rKZ/strings.xml | 1 + common/res/values-km-rKH/strings.xml | 1 + common/res/values-kn-rIN/strings.xml | 1 + common/res/values-ko/strings.xml | 1 + common/res/values-ky-rKG/strings.xml | 1 + common/res/values-lo-rLA/strings.xml | 1 + common/res/values-lt/strings.xml | 1 + common/res/values-lv/strings.xml | 1 + common/res/values-mk-rMK/strings.xml | 1 + common/res/values-ml-rIN/strings.xml | 1 + common/res/values-mn-rMN/strings.xml | 1 + common/res/values-mr-rIN/strings.xml | 1 + common/res/values-ms-rMY/strings.xml | 1 + common/res/values-my-rMM/strings.xml | 1 + common/res/values-nb/strings.xml | 1 + common/res/values-ne-rNP/strings.xml | 1 + common/res/values-nl/strings.xml | 1 + common/res/values-pl/strings.xml | 1 + common/res/values-pt-rPT/strings.xml | 1 + common/res/values-pt/strings.xml | 1 + common/res/values-ro/strings.xml | 1 + common/res/values-ru/strings.xml | 1 + common/res/values-si-rLK/strings.xml | 1 + common/res/values-sk/strings.xml | 1 + common/res/values-sl/strings.xml | 1 + common/res/values-sr/strings.xml | 1 + common/res/values-sv/strings.xml | 1 + common/res/values-sw/strings.xml | 1 + common/res/values-ta-rIN/strings.xml | 1 + common/res/values-te-rIN/strings.xml | 1 + common/res/values-th/strings.xml | 1 + common/res/values-tl/strings.xml | 1 + common/res/values-tr/strings.xml | 1 + common/res/values-uk/strings.xml | 1 + common/res/values-ur-rPK/strings.xml | 1 + common/res/values-uz-rUZ/strings.xml | 1 + common/res/values-vi/strings.xml | 1 + common/res/values-zh-rCN/strings.xml | 1 + common/res/values-zh-rHK/strings.xml | 1 + common/res/values-zh-rTW/strings.xml | 1 + common/res/values-zu/strings.xml | 1 + common/res/values/dimens.xml | 5 - common/res/values/strings.xml | 1 + common/res/values/styles.xml | 6 - common/res/values/themes.xml | 1 - .../android/tv/common/SharedPreferencesUtils.java | 1 + .../src/com/android/tv/common/TvCommonUtils.java | 17 +- .../android/tv/common/TvContentRatingCache.java | 5 +- .../common/feature/SharedPreferencesFeature.java | 1 - .../android/tv/common/feature/TestableFeature.java | 15 + .../tv/common/ui/setup/SetupMultiPaneFragment.java | 14 +- jni/Android.mk | 20 +- libs/exoplayer_v2.jar | Bin 0 -> 1077775 bytes libs/exoplayer_v2_ext_ffmpeg.jar | Bin 0 -> 7271 bytes proguard.flags | 3 + proto/channel.proto | 2 + res/animator/tuning_block_view_fade_out.xml | 25 + res/drawable-xhdpi/dvr_default_poster.png | Bin 766 -> 861 bytes res/drawable-xhdpi/dvr_default_program_art.png | Bin 11562 -> 0 bytes res/drawable-xhdpi/ic_error_recording.png | Bin 729 -> 0 bytes res/drawable-xhdpi/ic_fresh.png | Bin 13892 -> 0 bytes res/drawable-xhdpi/ic_pip_option_input.png | Bin 270 -> 0 bytes res/drawable-xhdpi/ic_pip_option_layout1.png | Bin 175 -> 0 bytes res/drawable-xhdpi/ic_pip_option_layout2.png | Bin 176 -> 0 bytes res/drawable-xhdpi/ic_pip_option_layout3.png | Bin 176 -> 0 bytes res/drawable-xhdpi/ic_pip_option_layout4.png | Bin 175 -> 0 bytes res/drawable-xhdpi/ic_pip_option_layout5.png | Bin 178 -> 0 bytes res/drawable-xhdpi/ic_pip_option_size.png | Bin 155 -> 0 bytes res/drawable-xhdpi/ic_pip_option_swap.png | Bin 241 -> 0 bytes res/drawable-xhdpi/ic_pip_option_swap_audio.png | Bin 479 -> 0 bytes res/drawable-xhdpi/ic_recorded_program.png | Bin 612 -> 0 bytes res/drawable-xhdpi/ic_related_actor.png | Bin 1275 -> 0 bytes res/drawable-xhdpi/ic_related_search.png | Bin 2707 -> 0 bytes res/drawable-xhdpi/ic_setup_antenna.png | Bin 1264 -> 0 bytes res/drawable-xhdpi/ic_tvoption_pip.png | Bin 192 -> 176 bytes res/drawable-xhdpi/ic_tvoption_pip_off.png | Bin 261 -> 0 bytes res/drawable-xhdpi/tv_3a_00.png | Bin 106 -> 0 bytes res/drawable-xhdpi/tv_error.png | Bin 2846 -> 0 bytes res/drawable-xhdpi/tv_usb_antenna.png | Bin 21390 -> 0 bytes res/drawable/play_controls_time_indicator.xml | 2 - res/drawable/playback_progress_bar.xml | 33 + .../priority_settings_action_item_selected.xml | 12 +- res/drawable/setup_item_background.xml | 28 - res/layout/activity_dvr_playback.xml | 6 +- res/layout/activity_tv.xml | 19 +- res/layout/dvr_main.xml | 2 +- res/layout/dvr_play.xml | 28 - res/layout/dvr_recording_card_view.xml | 37 +- res/layout/dvr_schedules_item.xml | 12 +- res/layout/guided_action_editable.xml | 41 + res/layout/input_banner.xml | 2 + res/layout/list_item_dvr_history.xml | 59 ++ res/layout/menu_card_action.xml | 57 +- res/layout/menu_card_app_link.xml | 7 - res/layout/menu_card_channel.xml | 7 - res/layout/menu_card_dvr.xml | 16 +- res/layout/menu_card_guide.xml | 16 +- res/layout/menu_card_setup.xml | 16 +- res/layout/menu_card_text.xml | 1 - res/layout/play_controls_contents.xml | 218 ++--- res/layout/select_input_item.xml | 2 + res/layout/tuning_block.xml | 30 + ...dvr_details_shared_element_enter_transition.xml | 46 + ...vr_details_shared_element_return_transition.xml | 45 + res/values-af/strings.xml | 84 +- res/values-am/strings.xml | 84 +- res/values-ar/strings.xml | 104 +- res/values-az-rAZ/strings.xml | 84 +- res/values-bg/strings.xml | 84 +- res/values-bn-rBD/strings.xml | 88 +- res/values-ca/strings.xml | 84 +- res/values-cs/strings.xml | 94 +- res/values-da/strings.xml | 84 +- res/values-de/strings.xml | 84 +- res/values-el/strings.xml | 84 +- res/values-en-rAU/strings.xml | 84 +- res/values-en-rGB/strings.xml | 84 +- res/values-en-rIN/strings.xml | 84 +- res/values-es-rUS/strings.xml | 84 +- res/values-es/strings.xml | 88 +- res/values-et-rEE/strings.xml | 84 +- res/values-eu-rES/strings.xml | 88 +- res/values-fa/strings.xml | 84 +- res/values-fi/strings.xml | 84 +- res/values-fr-rCA/strings.xml | 84 +- res/values-fr/strings.xml | 84 +- res/values-gl-rES/strings.xml | 84 +- res/values-hi/strings.xml | 84 +- res/values-hr/strings.xml | 89 +- res/values-hu/strings.xml | 84 +- res/values-hy-rAM/strings.xml | 84 +- res/values-in/strings.xml | 84 +- res/values-is-rIS/strings.xml | 84 +- res/values-it/strings.xml | 84 +- res/values-iw/strings.xml | 94 +- res/values-ja/strings.xml | 84 +- res/values-ka-rGE/strings.xml | 84 +- res/values-kk-rKZ/strings.xml | 84 +- res/values-km-rKH/strings.xml | 84 +- res/values-kn-rIN/strings.xml | 98 +- res/values-ko/strings.xml | 84 +- res/values-ky-rKG/strings.xml | 84 +- res/values-lo-rLA/strings.xml | 84 +- res/values-lt/strings.xml | 94 +- res/values-lv/strings.xml | 89 +- res/values-mk-rMK/strings.xml | 84 +- res/values-ml-rIN/strings.xml | 84 +- res/values-mn-rMN/strings.xml | 84 +- res/values-mr-rIN/strings.xml | 86 +- res/values-ms-rMY/strings.xml | 84 +- res/values-my-rMM/strings.xml | 84 +- res/values-nb/strings.xml | 84 +- res/values-ne-rNP/strings.xml | 92 +- res/values-nl/strings.xml | 90 +- res/values-pl/strings.xml | 94 +- res/values-pt-rPT/strings.xml | 84 +- res/values-pt/strings.xml | 84 +- res/values-ro/strings.xml | 89 +- res/values-ru/strings.xml | 94 +- res/values-si-rLK/strings.xml | 84 +- res/values-sk/strings.xml | 94 +- res/values-sl/strings.xml | 94 +- res/values-sr/strings.xml | 89 +- res/values-sv/strings.xml | 88 +- res/values-sw/strings.xml | 84 +- res/values-ta-rIN/strings.xml | 84 +- res/values-te-rIN/strings.xml | 84 +- res/values-th/strings.xml | 84 +- res/values-tl/strings.xml | 84 +- res/values-tr/strings.xml | 84 +- res/values-uk/strings.xml | 94 +- res/values-ur-rPK/strings.xml | 84 +- res/values-uz-rUZ/strings.xml | 86 +- res/values-v23/strings.xml | 21 - res/values-vi/strings.xml | 84 +- res/values-zh-rCN/strings.xml | 84 +- res/values-zh-rHK/strings.xml | 84 +- res/values-zh-rTW/strings.xml | 84 +- res/values-zu/strings.xml | 84 +- res/values/attr.xml | 25 - res/values/attrs.xml | 48 + res/values/colors.xml | 1 + res/values/dimens.xml | 50 +- res/values/google-services.xml | 8 - res/values/integers.xml | 2 + res/values/strings.xml | 222 ++--- res/values/styles.xml | 7 +- res/values/themes.xml | 2 + src/com/android/tv/Features.java | 64 ++ src/com/android/tv/InputSessionManager.java | 28 +- src/com/android/tv/MainActivity.java | 764 +++++---------- src/com/android/tv/SetupPassthroughActivity.java | 90 +- src/com/android/tv/TimeShiftManager.java | 9 +- src/com/android/tv/TvApplication.java | 19 +- src/com/android/tv/TvOptionsManager.java | 133 +-- src/com/android/tv/analytics/DurationTimer.java | 62 -- src/com/android/tv/data/Channel.java | 97 +- src/com/android/tv/data/ChannelDataManager.java | 42 +- src/com/android/tv/data/ChannelLogoFetcher.java | 307 ++---- src/com/android/tv/data/ChannelNumber.java | 71 +- src/com/android/tv/data/InternalDataUtils.java | 2 +- src/com/android/tv/data/StreamInfo.java | 4 + src/com/android/tv/data/epg/EpgFetcher.java | 327 +++++-- src/com/android/tv/data/epg/EpgReader.java | 29 +- src/com/android/tv/data/epg/StubEpgReader.java | 17 +- .../tv/dialog/DvrHistoryDialogFragment.java | 129 +++ .../tv/dialog/FullscreenDialogFragment.java | 2 +- .../android/tv/dialog/HalfSizedDialogFragment.java | 123 +++ .../tv/dialog/SafeDismissDialogFragment.java | 26 - src/com/android/tv/dialog/WebDialogFragment.java | 17 +- src/com/android/tv/dvr/BaseDvrDataManager.java | 41 +- src/com/android/tv/dvr/ConflictChecker.java | 277 ------ src/com/android/tv/dvr/DvrDataManager.java | 12 +- src/com/android/tv/dvr/DvrDataManagerImpl.java | 143 ++- src/com/android/tv/dvr/DvrDbSync.java | 363 ------- src/com/android/tv/dvr/DvrManager.java | 81 +- src/com/android/tv/dvr/DvrPlaybackActivity.java | 67 -- .../tv/dvr/DvrPlaybackMediaSessionHelper.java | 327 ------- src/com/android/tv/dvr/DvrPlayer.java | 425 -------- src/com/android/tv/dvr/DvrRecordingService.java | 122 --- src/com/android/tv/dvr/DvrScheduleManager.java | 139 +-- .../android/tv/dvr/DvrStartRecordingReceiver.java | 34 - .../android/tv/dvr/DvrStorageStatusManager.java | 35 +- src/com/android/tv/dvr/DvrUiHelper.java | 450 --------- .../android/tv/dvr/DvrWatchedPositionManager.java | 1 + .../android/tv/dvr/EpisodicProgramLoadTask.java | 382 -------- src/com/android/tv/dvr/IdGenerator.java | 50 - src/com/android/tv/dvr/InputTaskScheduler.java | 431 --------- src/com/android/tv/dvr/RecordedProgram.java | 868 ----------------- src/com/android/tv/dvr/RecordingTask.java | 519 ---------- src/com/android/tv/dvr/ScheduledProgramReaper.java | 67 -- src/com/android/tv/dvr/ScheduledRecording.java | 887 ----------------- src/com/android/tv/dvr/Scheduler.java | 283 ------ src/com/android/tv/dvr/SeriesInfo.java | 76 -- src/com/android/tv/dvr/SeriesRecording.java | 755 --------------- .../android/tv/dvr/SeriesRecordingScheduler.java | 579 ----------- src/com/android/tv/dvr/WritableDvrDataManager.java | 6 +- src/com/android/tv/dvr/data/IdGenerator.java | 50 + src/com/android/tv/dvr/data/RecordedProgram.java | 868 +++++++++++++++++ .../android/tv/dvr/data/ScheduledRecording.java | 902 +++++++++++++++++ .../android/tv/dvr/data/SeasonEpisodeNumber.java | 72 ++ src/com/android/tv/dvr/data/SeriesInfo.java | 76 ++ src/com/android/tv/dvr/data/SeriesRecording.java | 756 +++++++++++++++ .../android/tv/dvr/provider/AsyncDvrDbTask.java | 4 +- .../android/tv/dvr/provider/DvrDatabaseHelper.java | 4 +- src/com/android/tv/dvr/provider/DvrDbSync.java | 373 +++++++ .../tv/dvr/provider/EpisodicProgramLoadTask.java | 329 +++++++ .../android/tv/dvr/recorder/ConflictChecker.java | 280 ++++++ .../tv/dvr/recorder/DvrRecordingService.java | 154 +++ .../tv/dvr/recorder/DvrStartRecordingReceiver.java | 34 + .../tv/dvr/recorder/InputTaskScheduler.java | 435 +++++++++ src/com/android/tv/dvr/recorder/RecordingTask.java | 530 ++++++++++ .../tv/dvr/recorder/ScheduledProgramReaper.java | 70 ++ src/com/android/tv/dvr/recorder/Scheduler.java | 287 ++++++ .../tv/dvr/recorder/SeriesRecordingScheduler.java | 562 +++++++++++ .../android/tv/dvr/ui/ActionPresenterSelector.java | 138 --- src/com/android/tv/dvr/ui/BigArguments.java | 54 ++ .../ui/ChangeImageTransformWithScaledParent.java | 79 ++ .../tv/dvr/ui/CurrentRecordingDetailsFragment.java | 59 -- src/com/android/tv/dvr/ui/DetailsContent.java | 207 ---- .../android/tv/dvr/ui/DetailsContentPresenter.java | 300 ------ .../tv/dvr/ui/DetailsViewBackgroundHelper.java | 92 -- src/com/android/tv/dvr/ui/DvrActivity.java | 35 - .../tv/dvr/ui/DvrAlreadyRecordedFragment.java | 4 +- .../tv/dvr/ui/DvrAlreadyScheduledFragment.java | 5 +- src/com/android/tv/dvr/ui/DvrBrowseFragment.java | 601 ------------ .../ui/DvrChannelRecordDurationOptionFragment.java | 2 +- src/com/android/tv/dvr/ui/DvrConflictFragment.java | 7 +- src/com/android/tv/dvr/ui/DvrDetailsActivity.java | 98 -- src/com/android/tv/dvr/ui/DvrDetailsFragment.java | 344 ------- .../tv/dvr/ui/DvrForgetStorageErrorFragment.java | 87 -- .../android/tv/dvr/ui/DvrGuidedStepFragment.java | 50 +- .../tv/dvr/ui/DvrHalfSizedDialogFragment.java | 12 + .../dvr/ui/DvrInsufficientSpaceErrorFragment.java | 84 +- src/com/android/tv/dvr/ui/DvrItemPresenter.java | 80 -- .../tv/dvr/ui/DvrMissingStorageErrorFragment.java | 50 +- .../tv/dvr/ui/DvrPlaybackCardPresenter.java | 82 -- .../tv/dvr/ui/DvrPlaybackControlHelper.java | 313 ------ .../tv/dvr/ui/DvrPlaybackOverlayFragment.java | 304 ------ .../tv/dvr/ui/DvrPrioritySettingsFragment.java | 250 +++++ src/com/android/tv/dvr/ui/DvrScheduleFragment.java | 17 +- .../android/tv/dvr/ui/DvrSchedulesActivity.java | 104 -- .../tv/dvr/ui/DvrSeriesDeletionActivity.java | 5 +- .../tv/dvr/ui/DvrSeriesDeletionFragment.java | 253 +++++ .../tv/dvr/ui/DvrSeriesScheduledFragment.java | 45 +- .../tv/dvr/ui/DvrSeriesSettingsActivity.java | 20 +- .../tv/dvr/ui/DvrSeriesSettingsFragment.java | 366 +++++++ .../tv/dvr/ui/DvrStopRecordingFragment.java | 11 +- .../tv/dvr/ui/DvrStopSeriesRecordingFragment.java | 4 +- src/com/android/tv/dvr/ui/DvrUiHelper.java | 575 +++++++++++ src/com/android/tv/dvr/ui/FadeBackground.java | 70 ++ .../android/tv/dvr/ui/FullScheduleCardHolder.java | 29 - .../tv/dvr/ui/FullSchedulesCardPresenter.java | 84 -- .../android/tv/dvr/ui/HalfSizedDialogFragment.java | 117 --- .../tv/dvr/ui/PrioritySettingsFragment.java | 251 ----- .../tv/dvr/ui/RecordedProgramDetailsFragment.java | 170 ---- .../tv/dvr/ui/RecordedProgramPresenter.java | 182 ---- src/com/android/tv/dvr/ui/RecordingCardView.java | 185 ---- .../tv/dvr/ui/RecordingDetailsFragment.java | 87 -- .../dvr/ui/ScheduledRecordingDetailsFragment.java | 97 -- .../tv/dvr/ui/ScheduledRecordingPresenter.java | 177 ---- .../android/tv/dvr/ui/SeriesDeletionFragment.java | 252 ----- .../tv/dvr/ui/SeriesRecordingDetailsFragment.java | 375 ------- .../tv/dvr/ui/SeriesRecordingPresenter.java | 234 ----- .../android/tv/dvr/ui/SeriesSettingsFragment.java | 397 -------- src/com/android/tv/dvr/ui/SortedArrayAdapter.java | 90 +- .../tv/dvr/ui/browse/ActionPresenterSelector.java | 134 +++ .../ui/browse/CurrentRecordingDetailsFragment.java | 120 +++ .../android/tv/dvr/ui/browse/DetailsContent.java | 207 ++++ .../tv/dvr/ui/browse/DetailsContentPresenter.java | 299 ++++++ .../dvr/ui/browse/DetailsViewBackgroundHelper.java | 92 ++ .../tv/dvr/ui/browse/DvrBrowseActivity.java | 35 + .../tv/dvr/ui/browse/DvrBrowseFragment.java | 634 ++++++++++++ .../tv/dvr/ui/browse/DvrDetailsActivity.java | 98 ++ .../tv/dvr/ui/browse/DvrDetailsFragment.java | 344 +++++++ .../android/tv/dvr/ui/browse/DvrItemPresenter.java | 83 ++ .../tv/dvr/ui/browse/DvrListRowPresenter.java | 34 + .../tv/dvr/ui/browse/FullScheduleCardHolder.java | 29 + .../dvr/ui/browse/FullSchedulesCardPresenter.java | 88 ++ .../ui/browse/RecordedProgramDetailsFragment.java | 170 ++++ .../tv/dvr/ui/browse/RecordedProgramPresenter.java | 179 ++++ .../tv/dvr/ui/browse/RecordingCardView.java | 264 +++++ .../tv/dvr/ui/browse/RecordingDetailsFragment.java | 87 ++ .../browse/ScheduledRecordingDetailsFragment.java | 97 ++ .../dvr/ui/browse/ScheduledRecordingPresenter.java | 174 ++++ .../ui/browse/SeriesRecordingDetailsFragment.java | 369 +++++++ .../tv/dvr/ui/browse/SeriesRecordingPresenter.java | 233 +++++ .../tv/dvr/ui/list/BaseDvrSchedulesFragment.java | 2 +- .../tv/dvr/ui/list/DvrSchedulesActivity.java | 116 +++ .../tv/dvr/ui/list/DvrSchedulesFragment.java | 5 +- .../tv/dvr/ui/list/DvrSeriesSchedulesFragment.java | 72 +- .../android/tv/dvr/ui/list/EpisodicProgramRow.java | 6 +- src/com/android/tv/dvr/ui/list/ScheduleRow.java | 4 +- .../android/tv/dvr/ui/list/ScheduleRowAdapter.java | 4 +- .../tv/dvr/ui/list/ScheduleRowPresenter.java | 50 +- .../android/tv/dvr/ui/list/SchedulesHeaderRow.java | 20 +- .../dvr/ui/list/SchedulesHeaderRowPresenter.java | 26 +- .../tv/dvr/ui/list/SeriesScheduleRowAdapter.java | 24 +- .../tv/dvr/ui/list/SeriesScheduleRowPresenter.java | 17 +- .../tv/dvr/ui/playback/DvrPlaybackActivity.java | 67 ++ .../dvr/ui/playback/DvrPlaybackCardPresenter.java | 77 ++ .../dvr/ui/playback/DvrPlaybackControlHelper.java | 399 ++++++++ .../ui/playback/DvrPlaybackMediaSessionHelper.java | 335 +++++++ .../ui/playback/DvrPlaybackOverlayFragment.java | 431 +++++++++ .../dvr/ui/playback/DvrPlaybackSideFragment.java | 154 +++ src/com/android/tv/dvr/ui/playback/DvrPlayer.java | 583 +++++++++++ src/com/android/tv/experiments/ExperimentFlag.java | 32 +- src/com/android/tv/experiments/Experiments.java | 9 +- src/com/android/tv/guide/ProgramGuide.java | 2 +- src/com/android/tv/guide/ProgramItemView.java | 21 +- src/com/android/tv/guide/ProgramManager.java | 20 +- src/com/android/tv/guide/ProgramTableAdapter.java | 68 +- src/com/android/tv/guide/TimeListAdapter.java | 27 +- src/com/android/tv/menu/ActionCardView.java | 6 +- src/com/android/tv/menu/AppLinkCardView.java | 198 +++- src/com/android/tv/menu/BaseCardView.java | 50 +- src/com/android/tv/menu/ChannelCardView.java | 8 +- src/com/android/tv/menu/ChannelsRow.java | 10 +- src/com/android/tv/menu/ChannelsRowAdapter.java | 4 +- src/com/android/tv/menu/ItemListRowView.java | 14 +- src/com/android/tv/menu/Menu.java | 49 +- src/com/android/tv/menu/MenuAction.java | 69 +- src/com/android/tv/menu/MenuLayoutManager.java | 20 +- src/com/android/tv/menu/MenuRowFactory.java | 24 +- src/com/android/tv/menu/MenuUpdater.java | 41 +- src/com/android/tv/menu/OptionsRowAdapter.java | 50 +- .../android/tv/menu/PartnerOptionsRowAdapter.java | 3 +- src/com/android/tv/menu/PipOptionsRowAdapter.java | 137 --- src/com/android/tv/menu/PlayControlsButton.java | 24 +- src/com/android/tv/menu/PlayControlsRowView.java | 194 ++-- src/com/android/tv/menu/PlaybackProgressBar.java | 168 ++++ src/com/android/tv/menu/TvOptionsRowAdapter.java | 128 +-- .../tv/onboarding/SetupSourcesFragment.java | 2 + .../android/tv/receiver/BootCompletedReceiver.java | 2 +- src/com/android/tv/receiver/GlobalKeyReceiver.java | 40 +- .../tv/receiver/PackageIntentsReceiver.java | 6 + .../tv/recommendation/NotificationService.java | 1 + src/com/android/tv/search/DataManagerSearch.java | 4 +- src/com/android/tv/search/SearchInterface.java | 2 - src/com/android/tv/search/TvProviderSearch.java | 14 +- src/com/android/tv/tuner/TunerHal.java | 43 +- src/com/android/tv/tuner/TunerInputController.java | 195 +++- src/com/android/tv/tuner/TunerPreferences.java | 97 +- src/com/android/tv/tuner/UsbTunerHal.java | 6 +- src/com/android/tv/tuner/cc/Cea708Parser.java | 5 +- src/com/android/tv/tuner/data/PsipData.java | 2 +- src/com/android/tv/tuner/data/TunerChannel.java | 124 ++- .../tuner/exoplayer/ExoPlayerSampleExtractor.java | 378 +++++--- .../tv/tuner/exoplayer/FileSampleExtractor.java | 14 +- .../android/tv/tuner/exoplayer/MpegTsPlayer.java | 62 +- .../tv/tuner/exoplayer/MpegTsRendererBuilder.java | 13 +- .../exoplayer/ac3/Ac3DefaultTrackRenderer.java | 602 ++++++++++++ .../exoplayer/ac3/Ac3MediaCodecTrackRenderer.java | 97 ++ .../exoplayer/ac3/Ac3PassthroughTrackRenderer.java | 540 ----------- .../tv/tuner/exoplayer/ac3/Ac3TrackRenderer.java | 94 -- .../tv/tuner/exoplayer/ac3/AudioTrackMonitor.java | 4 +- .../tv/tuner/exoplayer/ac3/AudioTrackWrapper.java | 20 +- .../tv/tuner/exoplayer/buffer/BufferManager.java | 280 +++--- .../tuner/exoplayer/buffer/DvrStorageManager.java | 209 +++- .../exoplayer/buffer/RecordingSampleBuffer.java | 23 +- .../tv/tuner/exoplayer/buffer/SampleChunk.java | 23 +- .../exoplayer/buffer/SampleChunkIoHelper.java | 115 ++- .../tv/tuner/exoplayer/buffer/SampleQueue.java | 1 + .../tuner/exoplayer/buffer/SimpleSampleBuffer.java | 1 + .../exoplayer/buffer/TrickplayStorageManager.java | 85 +- .../tv/tuner/setup/ConnectionTypeFragment.java | 18 + .../android/tv/tuner/setup/PostalCodeFragment.java | 184 ++++ src/com/android/tv/tuner/setup/ScanFragment.java | 56 +- .../android/tv/tuner/setup/ScanResultFragment.java | 17 +- .../android/tv/tuner/setup/TunerSetupActivity.java | 238 ++++- .../android/tv/tuner/setup/WelcomeFragment.java | 38 +- .../android/tv/tuner/source/FileTsStreamer.java | 2 +- .../tv/tuner/source/TsDataSourceManager.java | 10 + .../android/tv/tuner/source/TunerTsStreamer.java | 58 +- .../tv/tuner/source/TunerTsStreamerManager.java | 26 +- src/com/android/tv/tuner/ts/SectionParser.java | 14 +- src/com/android/tv/tuner/ts/TsParser.java | 18 + .../tv/tuner/tvinput/ChannelDataManager.java | 54 +- .../android/tv/tuner/tvinput/EventDetector.java | 48 +- .../tuner/tvinput/TunerRecordingSessionWorker.java | 116 ++- src/com/android/tv/tuner/tvinput/TunerSession.java | 15 +- .../tv/tuner/tvinput/TunerSessionWorker.java | 294 +++--- .../tv/tuner/tvinput/TunerTvInputService.java | 30 +- src/com/android/tv/tuner/util/PostalCodeUtils.java | 89 ++ src/com/android/tv/tuner/util/StringUtils.java | 38 - .../tv/tuner/util/SystemPropertiesProxy.java | 16 + .../android/tv/tuner/util/TunerInputInfoUtils.java | 46 +- src/com/android/tv/ui/AppLayerTvView.java | 10 + src/com/android/tv/ui/ChannelBannerView.java | 137 +-- src/com/android/tv/ui/KeypadChannelSwitchView.java | 2 +- src/com/android/tv/ui/SelectInputView.java | 73 +- src/com/android/tv/ui/TunableTvView.java | 336 ++++--- src/com/android/tv/ui/TuningBlockView.java | 113 +++ src/com/android/tv/ui/TvOverlayManager.java | 30 +- src/com/android/tv/ui/TvViewUiManager.java | 265 +---- .../tv/ui/sidepanel/ClosedCaptionFragment.java | 9 +- .../tv/ui/sidepanel/DeveloperOptionFragment.java | 23 +- src/com/android/tv/ui/sidepanel/Item.java | 12 + .../tv/ui/sidepanel/PipInputSelectorFragment.java | 170 ---- .../android/tv/ui/sidepanel/SettingsFragment.java | 20 +- src/com/android/tv/ui/sidepanel/SideFragment.java | 47 +- .../tv/ui/sidepanel/SideFragmentManager.java | 2 - .../android/tv/ui/sidepanel/SimpleActionItem.java | 34 + src/com/android/tv/ui/sidepanel/SimpleItem.java | 34 - src/com/android/tv/util/AsyncDbTask.java | 2 +- src/com/android/tv/util/Debug.java | 60 ++ src/com/android/tv/util/DurationTimer.java | 84 ++ src/com/android/tv/util/LocationUtils.java | 24 +- src/com/android/tv/util/Partner.java | 181 ++++ src/com/android/tv/util/PipInputManager.java | 432 --------- src/com/android/tv/util/RecurringRunner.java | 9 +- src/com/android/tv/util/SearchManagerHelper.java | 61 -- src/com/android/tv/util/SetupUtils.java | 20 +- src/com/android/tv/util/StringUtils.java | 38 + src/com/android/tv/util/TvInputManagerHelper.java | 332 ++++++- src/com/android/tv/util/TvSettings.java | 148 ++- src/com/android/tv/util/TvTrackInfoUtils.java | 37 +- src/com/android/tv/util/Utils.java | 65 +- src/com/android/tv/util/ViewCache.java | 70 ++ .../android/tv/testing/dvr/RecordingTestUtils.java | 2 +- .../android/tv/tests/ui/LiveChannelsTestCase.java | 2 + .../tv/tests/ui/PlayControlsRowViewTest.java | 54 +- tests/input/res/values/strings.xml | 1 - .../tv/tests/jank/ProgramGuideJankTest.java | 13 +- .../unit/src/com/android/tv/MainActivityTest.java | 1 - .../src/com/android/tv/data/ChannelNumberTest.java | 12 +- .../unit/src/com/android/tv/data/ChannelTest.java | 24 +- .../unit/src/com/android/tv/data/ProgramTest.java | 2 - .../com/android/tv/dvr/BaseDvrDataManagerTest.java | 1 + .../com/android/tv/dvr/DvrDataManagerImplTest.java | 1 + .../android/tv/dvr/DvrDataManagerInMemoryImpl.java | 7 +- .../unit/src/com/android/tv/dvr/DvrDbSyncTest.java | 121 --- .../android/tv/dvr/DvrRecordingServiceTest.java | 68 -- .../com/android/tv/dvr/DvrScheduleManagerTest.java | 122 ++- .../tv/dvr/EpisodicProgramLoadTaskTest.java | 76 -- .../com/android/tv/dvr/InputTaskSchedulerTest.java | 221 ----- .../src/com/android/tv/dvr/RecordingTaskTest.java | 166 ---- .../android/tv/dvr/ScheduledProgramReaperTest.java | 114 --- .../com/android/tv/dvr/ScheduledRecordingTest.java | 1 + .../unit/src/com/android/tv/dvr/SchedulerTest.java | 107 -- .../tv/dvr/SeriesRecordingSchedulerTest.java | 111 --- .../com/android/tv/dvr/SeriesRecordingTest.java | 125 --- .../android/tv/dvr/data/SeriesRecordingTest.java | 125 +++ .../com/android/tv/dvr/provider/DvrDbSyncTest.java | 137 +++ .../dvr/provider/EpisodicProgramLoadTaskTest.java | 76 ++ .../tv/dvr/recorder/DvrRecordingServiceTest.java | 68 ++ .../dvr/recorder/ScheduledProgramReaperTest.java | 120 +++ .../com/android/tv/dvr/recorder/SchedulerTest.java | 110 +++ .../dvr/recorder/SeriesRecordingSchedulerTest.java | 113 +++ .../android/tv/dvr/ui/SortedArrayAdapterTest.java | 47 +- .../android/tv/experiments/ExperimentsTest.java | 50 + usbtuner-res/animator/setup_before_entry.xml | 32 - usbtuner-res/animator/setup_before_exit.xml | 34 - usbtuner-res/animator/setup_entry.xml | 35 - usbtuner-res/animator/setup_exit.xml | 35 - usbtuner-res/drawable-xhdpi/ic_setup_antenna.png | Bin 1264 -> 0 bytes usbtuner-res/drawable/ut_selector_background.xml | 28 - usbtuner-res/layout/ut_activity_playback.xml | 31 - usbtuner-res/layout/ut_guidance.xml | 48 - usbtuner-res/layout/ut_guidedactions.xml | 41 - usbtuner-res/values-af/strings.xml | 17 +- usbtuner-res/values-am/strings.xml | 17 +- usbtuner-res/values-ar/strings.xml | 17 +- usbtuner-res/values-az-rAZ/strings.xml | 17 +- usbtuner-res/values-bg/strings.xml | 17 +- usbtuner-res/values-bn-rBD/strings.xml | 21 +- usbtuner-res/values-ca/strings.xml | 17 +- usbtuner-res/values-cs/strings.xml | 17 +- usbtuner-res/values-da/strings.xml | 17 +- usbtuner-res/values-de/strings.xml | 17 +- usbtuner-res/values-el/strings.xml | 17 +- usbtuner-res/values-en-rAU/strings.xml | 17 +- usbtuner-res/values-en-rGB/strings.xml | 17 +- usbtuner-res/values-en-rIN/strings.xml | 17 +- usbtuner-res/values-es-rUS/strings.xml | 17 +- usbtuner-res/values-es/strings.xml | 17 +- usbtuner-res/values-et-rEE/strings.xml | 17 +- usbtuner-res/values-eu-rES/strings.xml | 17 +- usbtuner-res/values-fa/strings.xml | 17 +- usbtuner-res/values-fi/strings.xml | 17 +- usbtuner-res/values-fr-rCA/strings.xml | 17 +- usbtuner-res/values-fr/strings.xml | 17 +- usbtuner-res/values-gl-rES/strings.xml | 17 +- usbtuner-res/values-hi/strings.xml | 19 +- usbtuner-res/values-hr/strings.xml | 17 +- usbtuner-res/values-hu/strings.xml | 17 +- usbtuner-res/values-hy-rAM/strings.xml | 17 +- usbtuner-res/values-in/strings.xml | 17 +- usbtuner-res/values-is-rIS/strings.xml | 17 +- usbtuner-res/values-it/strings.xml | 17 +- usbtuner-res/values-iw/strings.xml | 17 +- usbtuner-res/values-ja/strings.xml | 17 +- usbtuner-res/values-ka-rGE/strings.xml | 17 +- usbtuner-res/values-kk-rKZ/strings.xml | 19 +- usbtuner-res/values-km-rKH/strings.xml | 17 +- usbtuner-res/values-kn-rIN/strings.xml | 17 +- usbtuner-res/values-ko/strings.xml | 19 +- usbtuner-res/values-ky-rKG/strings.xml | 17 +- usbtuner-res/values-lo-rLA/strings.xml | 17 +- usbtuner-res/values-lt/strings.xml | 17 +- usbtuner-res/values-lv/strings.xml | 17 +- usbtuner-res/values-mk-rMK/strings.xml | 17 +- usbtuner-res/values-ml-rIN/strings.xml | 17 +- usbtuner-res/values-mn-rMN/strings.xml | 17 +- usbtuner-res/values-mr-rIN/strings.xml | 17 +- usbtuner-res/values-ms-rMY/strings.xml | 17 +- usbtuner-res/values-my-rMM/strings.xml | 17 +- usbtuner-res/values-nb/strings.xml | 17 +- usbtuner-res/values-ne-rNP/strings.xml | 19 +- usbtuner-res/values-nl/strings.xml | 17 +- usbtuner-res/values-pl/strings.xml | 17 +- usbtuner-res/values-pt-rPT/strings.xml | 17 +- usbtuner-res/values-pt/strings.xml | 17 +- usbtuner-res/values-ro/strings.xml | 17 +- usbtuner-res/values-ru/strings.xml | 17 +- usbtuner-res/values-si-rLK/strings.xml | 17 +- usbtuner-res/values-sk/strings.xml | 17 +- usbtuner-res/values-sl/strings.xml | 17 +- usbtuner-res/values-sr/strings.xml | 17 +- usbtuner-res/values-sv/strings.xml | 17 +- usbtuner-res/values-sw/strings.xml | 17 +- usbtuner-res/values-ta-rIN/strings.xml | 17 +- usbtuner-res/values-te-rIN/strings.xml | 17 +- usbtuner-res/values-th/strings.xml | 17 +- usbtuner-res/values-tl/strings.xml | 17 +- usbtuner-res/values-tr/strings.xml | 17 +- usbtuner-res/values-uk/strings.xml | 17 +- usbtuner-res/values-ur-rPK/strings.xml | 17 +- usbtuner-res/values-uz-rUZ/strings.xml | 17 +- usbtuner-res/values-vi/strings.xml | 17 +- usbtuner-res/values-zh-rCN/strings.xml | 17 +- usbtuner-res/values-zh-rHK/strings.xml | 17 +- usbtuner-res/values-zh-rTW/strings.xml | 17 +- usbtuner-res/values-zu/strings.xml | 17 +- usbtuner-res/values/colors.xml | 6 - usbtuner-res/values/dimens.xml | 34 - usbtuner-res/values/integers.xml | 5 - usbtuner-res/values/strings.xml | 53 +- usbtuner-res/values/styles.xml | 120 --- version.mk | 4 +- 618 files changed, 27434 insertions(+), 23360 deletions(-) create mode 100644 libs/exoplayer_v2.jar create mode 100644 libs/exoplayer_v2_ext_ffmpeg.jar create mode 100644 res/animator/tuning_block_view_fade_out.xml delete mode 100644 res/drawable-xhdpi/dvr_default_program_art.png delete mode 100644 res/drawable-xhdpi/ic_error_recording.png delete mode 100644 res/drawable-xhdpi/ic_fresh.png delete mode 100644 res/drawable-xhdpi/ic_pip_option_input.png delete mode 100644 res/drawable-xhdpi/ic_pip_option_layout1.png delete mode 100644 res/drawable-xhdpi/ic_pip_option_layout2.png delete mode 100644 res/drawable-xhdpi/ic_pip_option_layout3.png delete mode 100644 res/drawable-xhdpi/ic_pip_option_layout4.png delete mode 100644 res/drawable-xhdpi/ic_pip_option_layout5.png delete mode 100644 res/drawable-xhdpi/ic_pip_option_size.png delete mode 100644 res/drawable-xhdpi/ic_pip_option_swap.png delete mode 100644 res/drawable-xhdpi/ic_pip_option_swap_audio.png delete mode 100644 res/drawable-xhdpi/ic_recorded_program.png delete mode 100644 res/drawable-xhdpi/ic_related_actor.png delete mode 100644 res/drawable-xhdpi/ic_related_search.png delete mode 100644 res/drawable-xhdpi/ic_setup_antenna.png delete mode 100644 res/drawable-xhdpi/ic_tvoption_pip_off.png delete mode 100644 res/drawable-xhdpi/tv_3a_00.png delete mode 100644 res/drawable-xhdpi/tv_error.png delete mode 100644 res/drawable-xhdpi/tv_usb_antenna.png create mode 100644 res/drawable/playback_progress_bar.xml delete mode 100644 res/drawable/setup_item_background.xml delete mode 100644 res/layout/dvr_play.xml create mode 100644 res/layout/guided_action_editable.xml create mode 100644 res/layout/list_item_dvr_history.xml create mode 100644 res/layout/tuning_block.xml create mode 100644 res/transition/dvr_details_shared_element_enter_transition.xml create mode 100644 res/transition/dvr_details_shared_element_return_transition.xml delete mode 100644 res/values-v23/strings.xml delete mode 100644 res/values/attr.xml create mode 100644 res/values/attrs.xml delete mode 100755 res/values/google-services.xml delete mode 100644 src/com/android/tv/analytics/DurationTimer.java create mode 100644 src/com/android/tv/dialog/DvrHistoryDialogFragment.java create mode 100644 src/com/android/tv/dialog/HalfSizedDialogFragment.java delete mode 100644 src/com/android/tv/dvr/ConflictChecker.java delete mode 100644 src/com/android/tv/dvr/DvrDbSync.java delete mode 100644 src/com/android/tv/dvr/DvrPlaybackActivity.java delete mode 100644 src/com/android/tv/dvr/DvrPlaybackMediaSessionHelper.java delete mode 100644 src/com/android/tv/dvr/DvrPlayer.java delete mode 100644 src/com/android/tv/dvr/DvrRecordingService.java delete mode 100644 src/com/android/tv/dvr/DvrStartRecordingReceiver.java delete mode 100644 src/com/android/tv/dvr/DvrUiHelper.java delete mode 100644 src/com/android/tv/dvr/EpisodicProgramLoadTask.java delete mode 100644 src/com/android/tv/dvr/IdGenerator.java delete mode 100644 src/com/android/tv/dvr/InputTaskScheduler.java delete mode 100644 src/com/android/tv/dvr/RecordedProgram.java delete mode 100644 src/com/android/tv/dvr/RecordingTask.java delete mode 100644 src/com/android/tv/dvr/ScheduledProgramReaper.java delete mode 100644 src/com/android/tv/dvr/ScheduledRecording.java delete mode 100644 src/com/android/tv/dvr/Scheduler.java delete mode 100644 src/com/android/tv/dvr/SeriesInfo.java delete mode 100644 src/com/android/tv/dvr/SeriesRecording.java delete mode 100644 src/com/android/tv/dvr/SeriesRecordingScheduler.java create mode 100644 src/com/android/tv/dvr/data/IdGenerator.java create mode 100644 src/com/android/tv/dvr/data/RecordedProgram.java create mode 100644 src/com/android/tv/dvr/data/ScheduledRecording.java create mode 100644 src/com/android/tv/dvr/data/SeasonEpisodeNumber.java create mode 100644 src/com/android/tv/dvr/data/SeriesInfo.java create mode 100644 src/com/android/tv/dvr/data/SeriesRecording.java create mode 100644 src/com/android/tv/dvr/provider/DvrDbSync.java create mode 100644 src/com/android/tv/dvr/provider/EpisodicProgramLoadTask.java create mode 100644 src/com/android/tv/dvr/recorder/ConflictChecker.java create mode 100644 src/com/android/tv/dvr/recorder/DvrRecordingService.java create mode 100644 src/com/android/tv/dvr/recorder/DvrStartRecordingReceiver.java create mode 100644 src/com/android/tv/dvr/recorder/InputTaskScheduler.java create mode 100644 src/com/android/tv/dvr/recorder/RecordingTask.java create mode 100644 src/com/android/tv/dvr/recorder/ScheduledProgramReaper.java create mode 100644 src/com/android/tv/dvr/recorder/Scheduler.java create mode 100644 src/com/android/tv/dvr/recorder/SeriesRecordingScheduler.java delete mode 100644 src/com/android/tv/dvr/ui/ActionPresenterSelector.java create mode 100644 src/com/android/tv/dvr/ui/BigArguments.java create mode 100644 src/com/android/tv/dvr/ui/ChangeImageTransformWithScaledParent.java delete mode 100644 src/com/android/tv/dvr/ui/CurrentRecordingDetailsFragment.java delete mode 100644 src/com/android/tv/dvr/ui/DetailsContent.java delete mode 100644 src/com/android/tv/dvr/ui/DetailsContentPresenter.java delete mode 100644 src/com/android/tv/dvr/ui/DetailsViewBackgroundHelper.java delete mode 100644 src/com/android/tv/dvr/ui/DvrActivity.java delete mode 100644 src/com/android/tv/dvr/ui/DvrBrowseFragment.java delete mode 100644 src/com/android/tv/dvr/ui/DvrDetailsActivity.java delete mode 100644 src/com/android/tv/dvr/ui/DvrDetailsFragment.java delete mode 100644 src/com/android/tv/dvr/ui/DvrForgetStorageErrorFragment.java delete mode 100644 src/com/android/tv/dvr/ui/DvrItemPresenter.java delete mode 100644 src/com/android/tv/dvr/ui/DvrPlaybackCardPresenter.java delete mode 100644 src/com/android/tv/dvr/ui/DvrPlaybackControlHelper.java delete mode 100644 src/com/android/tv/dvr/ui/DvrPlaybackOverlayFragment.java create mode 100644 src/com/android/tv/dvr/ui/DvrPrioritySettingsFragment.java delete mode 100644 src/com/android/tv/dvr/ui/DvrSchedulesActivity.java create mode 100644 src/com/android/tv/dvr/ui/DvrSeriesDeletionFragment.java create mode 100644 src/com/android/tv/dvr/ui/DvrSeriesSettingsFragment.java create mode 100644 src/com/android/tv/dvr/ui/DvrUiHelper.java create mode 100644 src/com/android/tv/dvr/ui/FadeBackground.java delete mode 100644 src/com/android/tv/dvr/ui/FullScheduleCardHolder.java delete mode 100644 src/com/android/tv/dvr/ui/FullSchedulesCardPresenter.java delete mode 100644 src/com/android/tv/dvr/ui/HalfSizedDialogFragment.java delete mode 100644 src/com/android/tv/dvr/ui/PrioritySettingsFragment.java delete mode 100644 src/com/android/tv/dvr/ui/RecordedProgramDetailsFragment.java delete mode 100644 src/com/android/tv/dvr/ui/RecordedProgramPresenter.java delete mode 100644 src/com/android/tv/dvr/ui/RecordingCardView.java delete mode 100644 src/com/android/tv/dvr/ui/RecordingDetailsFragment.java delete mode 100644 src/com/android/tv/dvr/ui/ScheduledRecordingDetailsFragment.java delete mode 100644 src/com/android/tv/dvr/ui/ScheduledRecordingPresenter.java delete mode 100644 src/com/android/tv/dvr/ui/SeriesDeletionFragment.java delete mode 100644 src/com/android/tv/dvr/ui/SeriesRecordingDetailsFragment.java delete mode 100644 src/com/android/tv/dvr/ui/SeriesRecordingPresenter.java delete mode 100644 src/com/android/tv/dvr/ui/SeriesSettingsFragment.java create mode 100644 src/com/android/tv/dvr/ui/browse/ActionPresenterSelector.java create mode 100644 src/com/android/tv/dvr/ui/browse/CurrentRecordingDetailsFragment.java create mode 100644 src/com/android/tv/dvr/ui/browse/DetailsContent.java create mode 100644 src/com/android/tv/dvr/ui/browse/DetailsContentPresenter.java create mode 100644 src/com/android/tv/dvr/ui/browse/DetailsViewBackgroundHelper.java create mode 100644 src/com/android/tv/dvr/ui/browse/DvrBrowseActivity.java create mode 100644 src/com/android/tv/dvr/ui/browse/DvrBrowseFragment.java create mode 100644 src/com/android/tv/dvr/ui/browse/DvrDetailsActivity.java create mode 100644 src/com/android/tv/dvr/ui/browse/DvrDetailsFragment.java create mode 100644 src/com/android/tv/dvr/ui/browse/DvrItemPresenter.java create mode 100644 src/com/android/tv/dvr/ui/browse/DvrListRowPresenter.java create mode 100644 src/com/android/tv/dvr/ui/browse/FullScheduleCardHolder.java create mode 100644 src/com/android/tv/dvr/ui/browse/FullSchedulesCardPresenter.java create mode 100644 src/com/android/tv/dvr/ui/browse/RecordedProgramDetailsFragment.java create mode 100644 src/com/android/tv/dvr/ui/browse/RecordedProgramPresenter.java create mode 100644 src/com/android/tv/dvr/ui/browse/RecordingCardView.java create mode 100644 src/com/android/tv/dvr/ui/browse/RecordingDetailsFragment.java create mode 100644 src/com/android/tv/dvr/ui/browse/ScheduledRecordingDetailsFragment.java create mode 100644 src/com/android/tv/dvr/ui/browse/ScheduledRecordingPresenter.java create mode 100644 src/com/android/tv/dvr/ui/browse/SeriesRecordingDetailsFragment.java create mode 100644 src/com/android/tv/dvr/ui/browse/SeriesRecordingPresenter.java create mode 100644 src/com/android/tv/dvr/ui/list/DvrSchedulesActivity.java create mode 100644 src/com/android/tv/dvr/ui/playback/DvrPlaybackActivity.java create mode 100644 src/com/android/tv/dvr/ui/playback/DvrPlaybackCardPresenter.java create mode 100644 src/com/android/tv/dvr/ui/playback/DvrPlaybackControlHelper.java create mode 100644 src/com/android/tv/dvr/ui/playback/DvrPlaybackMediaSessionHelper.java create mode 100644 src/com/android/tv/dvr/ui/playback/DvrPlaybackOverlayFragment.java create mode 100644 src/com/android/tv/dvr/ui/playback/DvrPlaybackSideFragment.java create mode 100644 src/com/android/tv/dvr/ui/playback/DvrPlayer.java delete mode 100644 src/com/android/tv/menu/PipOptionsRowAdapter.java create mode 100644 src/com/android/tv/menu/PlaybackProgressBar.java create mode 100644 src/com/android/tv/tuner/exoplayer/ac3/Ac3DefaultTrackRenderer.java create mode 100644 src/com/android/tv/tuner/exoplayer/ac3/Ac3MediaCodecTrackRenderer.java delete mode 100644 src/com/android/tv/tuner/exoplayer/ac3/Ac3PassthroughTrackRenderer.java delete mode 100644 src/com/android/tv/tuner/exoplayer/ac3/Ac3TrackRenderer.java create mode 100644 src/com/android/tv/tuner/setup/PostalCodeFragment.java create mode 100644 src/com/android/tv/tuner/util/PostalCodeUtils.java delete mode 100644 src/com/android/tv/tuner/util/StringUtils.java create mode 100644 src/com/android/tv/ui/TuningBlockView.java delete mode 100644 src/com/android/tv/ui/sidepanel/PipInputSelectorFragment.java create mode 100644 src/com/android/tv/ui/sidepanel/SimpleActionItem.java delete mode 100644 src/com/android/tv/ui/sidepanel/SimpleItem.java create mode 100644 src/com/android/tv/util/Debug.java create mode 100644 src/com/android/tv/util/DurationTimer.java create mode 100644 src/com/android/tv/util/Partner.java delete mode 100644 src/com/android/tv/util/PipInputManager.java delete mode 100644 src/com/android/tv/util/SearchManagerHelper.java create mode 100644 src/com/android/tv/util/StringUtils.java create mode 100644 src/com/android/tv/util/ViewCache.java delete mode 100644 tests/unit/src/com/android/tv/dvr/DvrDbSyncTest.java delete mode 100644 tests/unit/src/com/android/tv/dvr/DvrRecordingServiceTest.java delete mode 100644 tests/unit/src/com/android/tv/dvr/EpisodicProgramLoadTaskTest.java delete mode 100644 tests/unit/src/com/android/tv/dvr/InputTaskSchedulerTest.java delete mode 100644 tests/unit/src/com/android/tv/dvr/RecordingTaskTest.java delete mode 100644 tests/unit/src/com/android/tv/dvr/ScheduledProgramReaperTest.java delete mode 100644 tests/unit/src/com/android/tv/dvr/SchedulerTest.java delete mode 100644 tests/unit/src/com/android/tv/dvr/SeriesRecordingSchedulerTest.java delete mode 100644 tests/unit/src/com/android/tv/dvr/SeriesRecordingTest.java create mode 100644 tests/unit/src/com/android/tv/dvr/data/SeriesRecordingTest.java create mode 100644 tests/unit/src/com/android/tv/dvr/provider/DvrDbSyncTest.java create mode 100644 tests/unit/src/com/android/tv/dvr/provider/EpisodicProgramLoadTaskTest.java create mode 100644 tests/unit/src/com/android/tv/dvr/recorder/DvrRecordingServiceTest.java create mode 100644 tests/unit/src/com/android/tv/dvr/recorder/ScheduledProgramReaperTest.java create mode 100644 tests/unit/src/com/android/tv/dvr/recorder/SchedulerTest.java create mode 100644 tests/unit/src/com/android/tv/dvr/recorder/SeriesRecordingSchedulerTest.java create mode 100644 tests/unit/src/com/android/tv/experiments/ExperimentsTest.java delete mode 100644 usbtuner-res/animator/setup_before_entry.xml delete mode 100644 usbtuner-res/animator/setup_before_exit.xml delete mode 100644 usbtuner-res/animator/setup_entry.xml delete mode 100644 usbtuner-res/animator/setup_exit.xml delete mode 100644 usbtuner-res/drawable-xhdpi/ic_setup_antenna.png delete mode 100644 usbtuner-res/drawable/ut_selector_background.xml delete mode 100644 usbtuner-res/layout/ut_activity_playback.xml delete mode 100644 usbtuner-res/layout/ut_guidance.xml delete mode 100644 usbtuner-res/layout/ut_guidedactions.xml delete mode 100644 usbtuner-res/values/styles.xml diff --git a/Android.mk b/Android.mk index 5a5479b6..5884c5c3 100644 --- a/Android.mk +++ b/Android.mk @@ -56,9 +56,10 @@ LOCAL_STATIC_JAVA_LIBRARIES := \ android-support-v17-leanback \ icu4j-usbtuner \ lib-exoplayer \ + lib-exoplayer-v2 \ + lib-exoplayer-v2-ext-ffmpeg \ tv-common \ - legacy-android-test \ - junit + LOCAL_JAVACFLAGS := -Xlint:deprecation -Xlint:unchecked @@ -96,16 +97,18 @@ LOCAL_SDK_VERSION := system_current include $(BUILD_STATIC_JAVA_LIBRARY) + ############################################################# # Pre-built dependency jars ############################################################# - include $(CLEAR_VARS) LOCAL_MODULE_TAGS := optional LOCAL_PREBUILT_STATIC_JAVA_LIBRARIES := \ lib-exoplayer:libs/exoplayer.jar \ + lib-exoplayer-v2:libs/exoplayer_v2.jar \ + lib-exoplayer-v2-ext-ffmpeg:libs/exoplayer_v2_ext_ffmpeg.jar \ include $(BUILD_MULTI_PREBUILT) diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 1faa2ae3..b74fdd4d 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -113,17 +113,17 @@ android:launchMode="singleTop" android:theme="@style/Theme.Setup.GuidedStep" /> - - - @@ -138,7 +138,7 @@ - @@ -229,12 +229,14 @@ + + - - + + +

Notices for files:

+
    +
  • libhdhomerun
  • +
+
                  GNU LESSER GENERAL PUBLIC LICENSE
+                       Version 2.1, February 1999
+
+ Copyright (C) 1991, 1999 Free Software Foundation, Inc.
+ 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+(This is the first released version of the Lesser GPL.  It also counts
+ as the successor of the GNU Library Public License, version 2, hence
+ the version number 2.1.)
+
+                            Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+Licenses are intended to guarantee your freedom to share and change
+free software--to make sure the software is free for all its users.
+
+  This license, the Lesser General Public License, applies to some
+specially designated software packages--typically libraries--of the
+Free Software Foundation and other authors who decide to use it.  You
+can use it too, but we suggest you first think carefully about whether
+this license or the ordinary General Public License is the better
+strategy to use in any particular case, based on the explanations below.
+
+  When we speak of free software, we are referring to freedom of use,
+not price.  Our General Public Licenses are designed to make sure that
+you have the freedom to distribute copies of free software (and charge
+for this service if you wish); that you receive source code or can get
+it if you want it; that you can change the software and use pieces of
+it in new free programs; and that you are informed that you can do
+these things.
+
+  To protect your rights, we need to make restrictions that forbid
+distributors to deny you these rights or to ask you to surrender these
+rights.  These restrictions translate to certain responsibilities for
+you if you distribute copies of the library or if you modify it.
+
+  For example, if you distribute copies of the library, whether gratis
+or for a fee, you must give the recipients all the rights that we gave
+you.  You must make sure that they, too, receive or can get the source
+code.  If you link other code with the library, you must provide
+complete object files to the recipients, so that they can relink them
+with the library after making changes to the library and recompiling
+it.  And you must show them these terms so they know their rights.
+
+  We protect your rights with a two-step method: (1) we copyright the
+library, and (2) we offer you this license, which gives you legal
+permission to copy, distribute and/or modify the library.
+
+  To protect each distributor, we want to make it very clear that
+there is no warranty for the free library.  Also, if the library is
+modified by someone else and passed on, the recipients should know
+that what they have is not the original version, so that the original
+author's reputation will not be affected by problems that might be
+introduced by others.
+
+  Finally, software patents pose a constant threat to the existence of
+any free program.  We wish to make sure that a company cannot
+effectively restrict the users of a free program by obtaining a
+restrictive license from a patent holder.  Therefore, we insist that
+any patent license obtained for a version of the library must be
+consistent with the full freedom of use specified in this license.
+
+  Most GNU software, including some libraries, is covered by the
+ordinary GNU General Public License.  This license, the GNU Lesser
+General Public License, applies to certain designated libraries, and
+is quite different from the ordinary General Public License.  We use
+this license for certain libraries in order to permit linking those
+libraries into non-free programs.
+
+  When a program is linked with a library, whether statically or using
+a shared library, the combination of the two is legally speaking a
+combined work, a derivative of the original library.  The ordinary
+General Public License therefore permits such linking only if the
+entire combination fits its criteria of freedom.  The Lesser General
+Public License permits more lax criteria for linking other code with
+the library.
+
+  We call this license the "Lesser" General Public License because it
+does Less to protect the user's freedom than the ordinary General
+Public License.  It also provides other free software developers Less
+of an advantage over competing non-free programs.  These disadvantages
+are the reason we use the ordinary General Public License for many
+libraries.  However, the Lesser license provides advantages in certain
+special circumstances.
+
+  For example, on rare occasions, there may be a special need to
+encourage the widest possible use of a certain library, so that it becomes
+a de-facto standard.  To achieve this, non-free programs must be
+allowed to use the library.  A more frequent case is that a free
+library does the same job as widely used non-free libraries.  In this
+case, there is little to gain by limiting the free library to free
+software only, so we use the Lesser General Public License.
+
+  In other cases, permission to use a particular library in non-free
+programs enables a greater number of people to use a large body of
+free software.  For example, permission to use the GNU C Library in
+non-free programs enables many more people to use the whole GNU
+operating system, as well as its variant, the GNU/Linux operating
+system.
+
+  Although the Lesser General Public License is Less protective of the
+users' freedom, it does ensure that the user of a program that is
+linked with the Library has the freedom and the wherewithal to run
+that program using a modified version of the Library.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.  Pay close attention to the difference between a
+"work based on the library" and a "work that uses the library".  The
+former contains code derived from the library, whereas the latter must
+be combined with the library in order to run.
+
+                  GNU LESSER GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License Agreement applies to any software library or other
+program which contains a notice placed by the copyright holder or
+other authorized party saying it may be distributed under the terms of
+this Lesser General Public License (also called "this License").
+Each licensee is addressed as "you".
+
+  A "library" means a collection of software functions and/or data
+prepared so as to be conveniently linked with application programs
+(which use some of those functions and data) to form executables.
+
+  The "Library", below, refers to any such software library or work
+which has been distributed under these terms.  A "work based on the
+Library" means either the Library or any derivative work under
+copyright law: that is to say, a work containing the Library or a
+portion of it, either verbatim or with modifications and/or translated
+straightforwardly into another language.  (Hereinafter, translation is
+included without limitation in the term "modification".)
+
+  "Source code" for a work means the preferred form of the work for
+making modifications to it.  For a library, complete source code means
+all the source code for all modules it contains, plus any associated
+interface definition files, plus the scripts used to control compilation
+and installation of the library.
+
+  Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running a program using the Library is not restricted, and output from
+such a program is covered only if its contents constitute a work based
+on the Library (independent of the use of the Library in a tool for
+writing it).  Whether that is true depends on what the Library does
+and what the program that uses the Library does.
+
+  1. You may copy and distribute verbatim copies of the Library's
+complete source code as you receive it, in any medium, provided that
+you conspicuously and appropriately publish on each copy an
+appropriate copyright notice and disclaimer of warranty; keep intact
+all the notices that refer to this License and to the absence of any
+warranty; and distribute a copy of this License along with the
+Library.
+
+  You may charge a fee for the physical act of transferring a copy,
+and you may at your option offer warranty protection in exchange for a
+fee.
+
+  2. You may modify your copy or copies of the Library or any portion
+of it, thus forming a work based on the Library, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) The modified work must itself be a software library.
+
+    b) You must cause the files modified to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    c) You must cause the whole of the work to be licensed at no
+    charge to all third parties under the terms of this License.
+
+    d) If a facility in the modified Library refers to a function or a
+    table of data to be supplied by an application program that uses
+    the facility, other than as an argument passed when the facility
+    is invoked, then you must make a good faith effort to ensure that,
+    in the event an application does not supply such function or
+    table, the facility still operates, and performs whatever part of
+    its purpose remains meaningful.
+
+    (For example, a function in a library to compute square roots has
+    a purpose that is entirely well-defined independent of the
+    application.  Therefore, Subsection 2d requires that any
+    application-supplied function or table used by this function must
+    be optional: if the application does not supply it, the square
+    root function must still compute square roots.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Library,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Library, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote
+it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Library.
+
+In addition, mere aggregation of another work not based on the Library
+with the Library (or with a work based on the Library) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may opt to apply the terms of the ordinary GNU General Public
+License instead of this License to a given copy of the Library.  To do
+this, you must alter all the notices that refer to this License, so
+that they refer to the ordinary GNU General Public License, version 2,
+instead of to this License.  (If a newer version than version 2 of the
+ordinary GNU General Public License has appeared, then you can specify
+that version instead if you wish.)  Do not make any other change in
+these notices.
+
+  Once this change is made in a given copy, it is irreversible for
+that copy, so the ordinary GNU General Public License applies to all
+subsequent copies and derivative works made from that copy.
+
+  This option is useful when you wish to copy part of the code of
+the Library into a program that is not a library.
+
+  4. You may copy and distribute the Library (or a portion or
+derivative of it, under Section 2) in object code or executable form
+under the terms of Sections 1 and 2 above provided that you accompany
+it with the complete corresponding machine-readable source code, which
+must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange.
+
+  If distribution of object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the
+source code from the same place satisfies the requirement to
+distribute the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  5. A program that contains no derivative of any portion of the
+Library, but is designed to work with the Library by being compiled or
+linked with it, is called a "work that uses the Library".  Such a
+work, in isolation, is not a derivative work of the Library, and
+therefore falls outside the scope of this License.
+
+  However, linking a "work that uses the Library" with the Library
+creates an executable that is a derivative of the Library (because it
+contains portions of the Library), rather than a "work that uses the
+library".  The executable is therefore covered by this License.
+Section 6 states terms for distribution of such executables.
+
+  When a "work that uses the Library" uses material from a header file
+that is part of the Library, the object code for the work may be a
+derivative work of the Library even though the source code is not.
+Whether this is true is especially significant if the work can be
+linked without the Library, or if the work is itself a library.  The
+threshold for this to be true is not precisely defined by law.
+
+  If such an object file uses only numerical parameters, data
+structure layouts and accessors, and small macros and small inline
+functions (ten lines or less in length), then the use of the object
+file is unrestricted, regardless of whether it is legally a derivative
+work.  (Executables containing this object code plus portions of the
+Library will still fall under Section 6.)
+
+  Otherwise, if the work is a derivative of the Library, you may
+distribute the object code for the work under the terms of Section 6.
+Any executables containing that work also fall under Section 6,
+whether or not they are linked directly with the Library itself.
+
+  6. As an exception to the Sections above, you may also combine or
+link a "work that uses the Library" with the Library to produce a
+work containing portions of the Library, and distribute that work
+under terms of your choice, provided that the terms permit
+modification of the work for the customer's own use and reverse
+engineering for debugging such modifications.
+
+  You must give prominent notice with each copy of the work that the
+Library is used in it and that the Library and its use are covered by
+this License.  You must supply a copy of this License.  If the work
+during execution displays copyright notices, you must include the
+copyright notice for the Library among them, as well as a reference
+directing the user to the copy of this License.  Also, you must do one
+of these things:
+
+    a) Accompany the work with the complete corresponding
+    machine-readable source code for the Library including whatever
+    changes were used in the work (which must be distributed under
+    Sections 1 and 2 above); and, if the work is an executable linked
+    with the Library, with the complete machine-readable "work that
+    uses the Library", as object code and/or source code, so that the
+    user can modify the Library and then relink to produce a modified
+    executable containing the modified Library.  (It is understood
+    that the user who changes the contents of definitions files in the
+    Library will not necessarily be able to recompile the application
+    to use the modified definitions.)
+
+    b) Use a suitable shared library mechanism for linking with the
+    Library.  A suitable mechanism is one that (1) uses at run time a
+    copy of the library already present on the user's computer system,
+    rather than copying library functions into the executable, and (2)
+    will operate properly with a modified version of the library, if
+    the user installs one, as long as the modified version is
+    interface-compatible with the version that the work was made with.
+
+    c) Accompany the work with a written offer, valid for at
+    least three years, to give the same user the materials
+    specified in Subsection 6a, above, for a charge no more
+    than the cost of performing this distribution.
+
+    d) If distribution of the work is made by offering access to copy
+    from a designated place, offer equivalent access to copy the above
+    specified materials from the same place.
+
+    e) Verify that the user has already received a copy of these
+    materials or that you have already sent this user a copy.
+
+  For an executable, the required form of the "work that uses the
+Library" must include any data and utility programs needed for
+reproducing the executable from it.  However, as a special exception,
+the materials to be distributed need not include anything that is
+normally distributed (in either source or binary form) with the major
+components (compiler, kernel, and so on) of the operating system on
+which the executable runs, unless that component itself accompanies
+the executable.
+
+  It may happen that this requirement contradicts the license
+restrictions of other proprietary libraries that do not normally
+accompany the operating system.  Such a contradiction means you cannot
+use both them and the Library together in an executable that you
+distribute.
+
+  7. You may place library facilities that are a work based on the
+Library side-by-side in a single library together with other library
+facilities not covered by this License, and distribute such a combined
+library, provided that the separate distribution of the work based on
+the Library and of the other library facilities is otherwise
+permitted, and provided that you do these two things:
+
+    a) Accompany the combined library with a copy of the same work
+    based on the Library, uncombined with any other library
+    facilities.  This must be distributed under the terms of the
+    Sections above.
+
+    b) Give prominent notice with the combined library of the fact
+    that part of it is a work based on the Library, and explaining
+    where to find the accompanying uncombined form of the same work.
+
+  8. You may not copy, modify, sublicense, link with, or distribute
+the Library except as expressly provided under this License.  Any
+attempt otherwise to copy, modify, sublicense, link with, or
+distribute the Library is void, and will automatically terminate your
+rights under this License.  However, parties who have received copies,
+or rights, from you under this License will not have their licenses
+terminated so long as such parties remain in full compliance.
+
+  9. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Library or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Library (or any work based on the
+Library), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.
+
+  10. Each time you redistribute the Library (or any work based on the
+Library), the recipient automatically receives a license from the
+original licensor to copy, distribute, link with or modify the Library
+subject to these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties with
+this License.
+
+  11. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Library at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Library by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Library.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply,
+and the section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  12. If the distribution and/or use of the Library is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Library under this License may add
+an explicit geographical distribution limitation excluding those countries,
+so that distribution is permitted only in or among countries not thus
+excluded.  In such case, this License incorporates the limitation as if
+written in the body of this License.
+
+  13. The Free Software Foundation may publish revised and/or new
+versions of the Lesser General Public License from time to time.
+Such new versions will be similar in spirit to the present version,
+but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Library
+specifies a version number of this License which applies to it and
+"any later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation.  If the Library does not specify a
+license version number, you may choose any version ever published by
+the Free Software Foundation.
+
+  14. If you wish to incorporate parts of the Library into other free
+programs whose distribution conditions are incompatible with these,
+write to the author to ask for permission.  For software which is
+copyrighted by the Free Software Foundation, write to the Free
+Software Foundation; we sometimes make exceptions for this.  Our
+decision will be guided by the two goals of preserving the free status
+of all derivatives of our free software and of promoting the sharing
+and reuse of software generally.
+
+                            NO WARRANTY
+
+  15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+LIBRARY IS WITH YOU.  SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
+LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+                     END OF TERMS AND CONDITIONS
+
+           How to Apply These Terms to Your New Libraries
+
+  If you develop a new library, and you want it to be of the greatest
+possible use to the public, we recommend making it free software that
+everyone can redistribute and change.  You can do so by permitting
+redistribution under these terms (or, alternatively, under the terms of the
+ordinary General Public License).
+
+  To apply these terms, attach the following notices to the library.  It is
+safest to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least the
+"copyright" line and a pointer to where the full notice is found.
+
+    {description}
+    Copyright (C) {year} {fullname}
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Lesser General Public
+    License as published by the Free Software Foundation; either
+    version 2.1 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public
+    License along with this library; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301
+    USA
+
+Also add information on how to contact you by electronic and paper mail.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the library, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the
+  library `Frob' (a library for tweaking knobs) written by James Random
+  Hacker.
+
+  {signature of Ty Coon}, 1 April 1990
+  Ty Coon, President of Vice
+
+That's all there is to it!
+
+

Notices for files:

+
    +
  • FFmpeg
  • +
+
 GNU LESSER GENERAL PUBLIC LICENSE
+                       Version 2.1, February 1999
+
+ Copyright (C) 1991, 1999 Free Software Foundation, Inc.
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+[This is the first released version of the Lesser GPL.  It also counts
+ as the successor of the GNU Library Public License, version 2, hence
+ the version number 2.1.]
+
+                            Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+Licenses are intended to guarantee your freedom to share and change
+free software--to make sure the software is free for all its users.
+
+  This license, the Lesser General Public License, applies to some
+specially designated software packages--typically libraries--of the
+Free Software Foundation and other authors who decide to use it.  You
+can use it too, but we suggest you first think carefully about whether
+this license or the ordinary General Public License is the better
+strategy to use in any particular case, based on the explanations below.
+
+  When we speak of free software, we are referring to freedom of use,
+not price.  Our General Public Licenses are designed to make sure that
+you have the freedom to distribute copies of free software (and charge
+for this service if you wish); that you receive source code or can get
+it if you want it; that you can change the software and use pieces of
+it in new free programs; and that you are informed that you can do
+these things.
+
+  To protect your rights, we need to make restrictions that forbid
+distributors to deny you these rights or to ask you to surrender these
+rights.  These restrictions translate to certain responsibilities for
+you if you distribute copies of the library or if you modify it.
+
+  For example, if you distribute copies of the library, whether gratis
+or for a fee, you must give the recipients all the rights that we gave
+you.  You must make sure that they, too, receive or can get the source
+code.  If you link other code with the library, you must provide
+complete object files to the recipients, so that they can relink them
+with the library after making changes to the library and recompiling
+it.  And you must show them these terms so they know their rights.
+
+  We protect your rights with a two-step method: (1) we copyright the
+library, and (2) we offer you this license, which gives you legal
+permission to copy, distribute and/or modify the library.
+
+  To protect each distributor, we want to make it very clear that
+there is no warranty for the free library.  Also, if the library is
+modified by someone else and passed on, the recipients should know
+that what they have is not the original version, so that the original
+author's reputation will not be affected by problems that might be
+introduced by others.
+
+  Finally, software patents pose a constant threat to the existence of
+any free program.  We wish to make sure that a company cannot
+effectively restrict the users of a free program by obtaining a
+restrictive license from a patent holder.  Therefore, we insist that
+any patent license obtained for a version of the library must be
+consistent with the full freedom of use specified in this license.
+
+  Most GNU software, including some libraries, is covered by the
+ordinary GNU General Public License.  This license, the GNU Lesser
+General Public License, applies to certain designated libraries, and
+is quite different from the ordinary General Public License.  We use
+this license for certain libraries in order to permit linking those
+libraries into non-free programs.
+
+  When a program is linked with a library, whether statically or using
+a shared library, the combination of the two is legally speaking a
+combined work, a derivative of the original library.  The ordinary
+General Public License therefore permits such linking only if the
+entire combination fits its criteria of freedom.  The Lesser General
+Public License permits more lax criteria for linking other code with
+the library.
+
+  We call this license the "Lesser" General Public License because it
+does Less to protect the user's freedom than the ordinary General
+Public License.  It also provides other free software developers Less
+of an advantage over competing non-free programs.  These disadvantages
+are the reason we use the ordinary General Public License for many
+libraries.  However, the Lesser license provides advantages in certain
+special circumstances.
+
+  For example, on rare occasions, there may be a special need to
+encourage the widest possible use of a certain library, so that it becomes
+a de-facto standard.  To achieve this, non-free programs must be
+allowed to use the library.  A more frequent case is that a free
+library does the same job as widely used non-free libraries.  In this
+case, there is little to gain by limiting the free library to free
+software only, so we use the Lesser General Public License.
+
+  In other cases, permission to use a particular library in non-free
+programs enables a greater number of people to use a large body of
+free software.  For example, permission to use the GNU C Library in
+non-free programs enables many more people to use the whole GNU
+operating system, as well as its variant, the GNU/Linux operating
+system.
+
+  Although the Lesser General Public License is Less protective of the
+users' freedom, it does ensure that the user of a program that is
+linked with the Library has the freedom and the wherewithal to run
+that program using a modified version of the Library.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.  Pay close attention to the difference between a
+"work based on the library" and a "work that uses the library".  The
+former contains code derived from the library, whereas the latter must
+be combined with the library in order to run.
+
+                  GNU LESSER GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License Agreement applies to any software library or other
+program which contains a notice placed by the copyright holder or
+other authorized party saying it may be distributed under the terms of
+this Lesser General Public License (also called "this License").
+Each licensee is addressed as "you".
+
+  A "library" means a collection of software functions and/or data
+prepared so as to be conveniently linked with application programs
+(which use some of those functions and data) to form executables.
+
+  The "Library", below, refers to any such software library or work
+which has been distributed under these terms.  A "work based on the
+Library" means either the Library or any derivative work under
+copyright law: that is to say, a work containing the Library or a
+portion of it, either verbatim or with modifications and/or translated
+straightforwardly into another language.  (Hereinafter, translation is
+included without limitation in the term "modification".)
+
+  "Source code" for a work means the preferred form of the work for
+making modifications to it.  For a library, complete source code means
+all the source code for all modules it contains, plus any associated
+interface definition files, plus the scripts used to control compilation
+and installation of the library.
+
+  Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running a program using the Library is not restricted, and output from
+such a program is covered only if its contents constitute a work based
+on the Library (independent of the use of the Library in a tool for
+writing it).  Whether that is true depends on what the Library does
+and what the program that uses the Library does.
+
+  1. You may copy and distribute verbatim copies of the Library's
+complete source code as you receive it, in any medium, provided that
+you conspicuously and appropriately publish on each copy an
+appropriate copyright notice and disclaimer of warranty; keep intact
+all the notices that refer to this License and to the absence of any
+warranty; and distribute a copy of this License along with the
+Library.
+
+  You may charge a fee for the physical act of transferring a copy,
+and you may at your option offer warranty protection in exchange for a
+fee.
+
+  2. You may modify your copy or copies of the Library or any portion
+of it, thus forming a work based on the Library, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) The modified work must itself be a software library.
+
+    b) You must cause the files modified to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    c) You must cause the whole of the work to be licensed at no
+    charge to all third parties under the terms of this License.
+
+    d) If a facility in the modified Library refers to a function or a
+    table of data to be supplied by an application program that uses
+    the facility, other than as an argument passed when the facility
+    is invoked, then you must make a good faith effort to ensure that,
+    in the event an application does not supply such function or
+    table, the facility still operates, and performs whatever part of
+    its purpose remains meaningful.
+
+    (For example, a function in a library to compute square roots has
+    a purpose that is entirely well-defined independent of the
+    application.  Therefore, Subsection 2d requires that any
+    application-supplied function or table used by this function must
+    be optional: if the application does not supply it, the square
+    root function must still compute square roots.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Library,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Library, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote
+it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Library.
+
+In addition, mere aggregation of another work not based on the Library
+with the Library (or with a work based on the Library) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may opt to apply the terms of the ordinary GNU General Public
+License instead of this License to a given copy of the Library.  To do
+this, you must alter all the notices that refer to this License, so
+that they refer to the ordinary GNU General Public License, version 2,
+instead of to this License.  (If a newer version than version 2 of the
+ordinary GNU General Public License has appeared, then you can specify
+that version instead if you wish.)  Do not make any other change in
+these notices.
+
+  Once this change is made in a given copy, it is irreversible for
+that copy, so the ordinary GNU General Public License applies to all
+subsequent copies and derivative works made from that copy.
+
+  This option is useful when you wish to copy part of the code of
+the Library into a program that is not a library.
+
+  4. You may copy and distribute the Library (or a portion or
+derivative of it, under Section 2) in object code or executable form
+under the terms of Sections 1 and 2 above provided that you accompany
+it with the complete corresponding machine-readable source code, which
+must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange.
+
+  If distribution of object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the
+source code from the same place satisfies the requirement to
+distribute the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  5. A program that contains no derivative of any portion of the
+Library, but is designed to work with the Library by being compiled or
+linked with it, is called a "work that uses the Library".  Such a
+work, in isolation, is not a derivative work of the Library, and
+therefore falls outside the scope of this License.
+
+  However, linking a "work that uses the Library" with the Library
+creates an executable that is a derivative of the Library (because it
+contains portions of the Library), rather than a "work that uses the
+library".  The executable is therefore covered by this License.
+Section 6 states terms for distribution of such executables.
+
+  When a "work that uses the Library" uses material from a header file
+that is part of the Library, the object code for the work may be a
+derivative work of the Library even though the source code is not.
+Whether this is true is especially significant if the work can be
+linked without the Library, or if the work is itself a library.  The
+threshold for this to be true is not precisely defined by law.
+
+  If such an object file uses only numerical parameters, data
+structure layouts and accessors, and small macros and small inline
+functions (ten lines or less in length), then the use of the object
+file is unrestricted, regardless of whether it is legally a derivative
+work.  (Executables containing this object code plus portions of the
+Library will still fall under Section 6.)
+
+  Otherwise, if the work is a derivative of the Library, you may
+distribute the object code for the work under the terms of Section 6.
+Any executables containing that work also fall under Section 6,
+whether or not they are linked directly with the Library itself.
+
+  6. As an exception to the Sections above, you may also combine or
+link a "work that uses the Library" with the Library to produce a
+work containing portions of the Library, and distribute that work
+under terms of your choice, provided that the terms permit
+modification of the work for the customer's own use and reverse
+engineering for debugging such modifications.
+
+  You must give prominent notice with each copy of the work that the
+Library is used in it and that the Library and its use are covered by
+this License.  You must supply a copy of this License.  If the work
+during execution displays copyright notices, you must include the
+copyright notice for the Library among them, as well as a reference
+directing the user to the copy of this License.  Also, you must do one
+of these things:
+
+    a) Accompany the work with the complete corresponding
+    machine-readable source code for the Library including whatever
+    changes were used in the work (which must be distributed under
+    Sections 1 and 2 above); and, if the work is an executable linked
+    with the Library, with the complete machine-readable "work that
+    uses the Library", as object code and/or source code, so that the
+    user can modify the Library and then relink to produce a modified
+    executable containing the modified Library.  (It is understood
+    that the user who changes the contents of definitions files in the
+    Library will not necessarily be able to recompile the application
+    to use the modified definitions.)
+
+    b) Use a suitable shared library mechanism for linking with the
+    Library.  A suitable mechanism is one that (1) uses at run time a
+    copy of the library already present on the user's computer system,
+    rather than copying library functions into the executable, and (2)
+    will operate properly with a modified version of the library, if
+    the user installs one, as long as the modified version is
+    interface-compatible with the version that the work was made with.
+
+    c) Accompany the work with a written offer, valid for at
+    least three years, to give the same user the materials
+    specified in Subsection 6a, above, for a charge no more
+    than the cost of performing this distribution.
+
+    d) If distribution of the work is made by offering access to copy
+    from a designated place, offer equivalent access to copy the above
+    specified materials from the same place.
+
+    e) Verify that the user has already received a copy of these
+    materials or that you have already sent this user a copy.
+
+  For an executable, the required form of the "work that uses the
+Library" must include any data and utility programs needed for
+reproducing the executable from it.  However, as a special exception,
+the materials to be distributed need not include anything that is
+normally distributed (in either source or binary form) with the major
+components (compiler, kernel, and so on) of the operating system on
+which the executable runs, unless that component itself accompanies
+the executable.
+
+  It may happen that this requirement contradicts the license
+restrictions of other proprietary libraries that do not normally
+accompany the operating system.  Such a contradiction means you cannot
+use both them and the Library together in an executable that you
+distribute.
+
+  7. You may place library facilities that are a work based on the
+Library side-by-side in a single library together with other library
+facilities not covered by this License, and distribute such a combined
+library, provided that the separate distribution of the work based on
+the Library and of the other library facilities is otherwise
+permitted, and provided that you do these two things:
+
+    a) Accompany the combined library with a copy of the same work
+    based on the Library, uncombined with any other library
+    facilities.  This must be distributed under the terms of the
+    Sections above.
+
+    b) Give prominent notice with the combined library of the fact
+    that part of it is a work based on the Library, and explaining
+    where to find the accompanying uncombined form of the same work.
+
+  8. You may not copy, modify, sublicense, link with, or distribute
+the Library except as expressly provided under this License.  Any
+attempt otherwise to copy, modify, sublicense, link with, or
+distribute the Library is void, and will automatically terminate your
+rights under this License.  However, parties who have received copies,
+or rights, from you under this License will not have their licenses
+terminated so long as such parties remain in full compliance.
+
+  9. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Library or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Library (or any work based on the
+Library), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.
+
+  10. Each time you redistribute the Library (or any work based on the
+Library), the recipient automatically receives a license from the
+original licensor to copy, distribute, link with or modify the Library
+subject to these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties with
+this License.
+
+  11. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Library at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Library by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Library.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply,
+and the section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  12. If the distribution and/or use of the Library is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Library under this License may add
+an explicit geographical distribution limitation excluding those countries,
+so that distribution is permitted only in or among countries not thus
+excluded.  In such case, this License incorporates the limitation as if
+written in the body of this License.
+
+  13. The Free Software Foundation may publish revised and/or new
+versions of the Lesser General Public License from time to time.
+Such new versions will be similar in spirit to the present version,
+but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Library
+specifies a version number of this License which applies to it and
+"any later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation.  If the Library does not specify a
+license version number, you may choose any version ever published by
+the Free Software Foundation.
+
+  14. If you wish to incorporate parts of the Library into other free
+programs whose distribution conditions are incompatible with these,
+write to the author to ask for permission.  For software which is
+copyrighted by the Free Software Foundation, write to the Free
+Software Foundation; we sometimes make exceptions for this.  Our
+decision will be guided by the two goals of preserving the free status
+of all derivatives of our free software and of promoting the sharing
+and reuse of software generally.
+
+                            NO WARRANTY
+
+  15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+LIBRARY IS WITH YOU.  SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
+LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+                     END OF TERMS AND CONDITIONS
+
+           How to Apply These Terms to Your New Libraries
+
+  If you develop a new library, and you want it to be of the greatest
+possible use to the public, we recommend making it free software that
+everyone can redistribute and change.  You can do so by permitting
+redistribution under these terms (or, alternatively, under the terms of the
+ordinary General Public License).
+
+  To apply these terms, attach the following notices to the library.  It is
+safest to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least the
+"copyright" line and a pointer to where the full notice is found.
+
+    
+    Copyright (C)   
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Lesser General Public
+    License as published by the Free Software Foundation; either
+    version 2.1 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public
+    License along with this library; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+Also add information on how to contact you by electronic and paper mail.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the library, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the
+  library `Frob' (a library for tweaking knobs) written by James Random Hacker.
+
+  , 1 April 1990
+  Ty Coon, President of Vice
+
+That's all there is to it!
+
- \ No newline at end of file + diff --git a/common/res/drawable/setup_selector_background.xml b/common/res/drawable/setup_selector_background.xml index 7351270b..b73c5813 100644 --- a/common/res/drawable/setup_selector_background.xml +++ b/common/res/drawable/setup_selector_background.xml @@ -17,8 +17,7 @@ - - + diff --git a/common/res/layout/fragment_setup_multi_pane.xml b/common/res/layout/fragment_setup_multi_pane.xml index 45aff132..e3ff8d83 100644 --- a/common/res/layout/fragment_setup_multi_pane.xml +++ b/common/res/layout/fragment_setup_multi_pane.xml @@ -40,11 +40,13 @@ android:clipChildren="false" android:clipToPadding="false" /> - @@ -54,7 +56,6 @@ android:layout_height="45dp" android:layout_marginStart="24dp" android:layout_marginEnd="40dp" - android:layout_marginTop="190dp" android:elevation="0dp" android:focusable="true" android:fontFamily="sans-serif-condensed" @@ -65,5 +66,23 @@ android:text="@string/action_text_done" android:textColor="#EEEEEE" android:textSize="14sp" /> - + + diff --git a/common/res/values-af/strings.xml b/common/res/values-af/strings.xml index 7a114849..4320af5c 100644 --- a/common/res/values-af/strings.xml +++ b/common/res/values-af/strings.xml @@ -18,6 +18,7 @@ "Klaar" + "Slaan oor" "S.%1$s Ep.%2$s" "Ep.%1$s" "S.%1$s: Ep. %2$s %3$s" diff --git a/common/res/values-am/strings.xml b/common/res/values-am/strings.xml index 45a1b7cb..9a2ae701 100644 --- a/common/res/values-am/strings.xml +++ b/common/res/values-am/strings.xml @@ -18,6 +18,7 @@ "ተከናውኗል" + "ዝለል" "ም%1$s፦ ክ.%2$s" "ክ.%1$s" "ም%1$s፦ ክፍል %2$s %3$s" diff --git a/common/res/values-ar/strings.xml b/common/res/values-ar/strings.xml index bedd137b..816af76b 100644 --- a/common/res/values-ar/strings.xml +++ b/common/res/values-ar/strings.xml @@ -18,6 +18,7 @@ "تم" + "تخطٍ" "الموسم %1$s: الحلقة %2$s" "الحلقة %1$s" "الموسم %1$s: الحلقة %2$s %3$s" diff --git a/common/res/values-az-rAZ/strings.xml b/common/res/values-az-rAZ/strings.xml index c844cd60..47cdb9e8 100644 --- a/common/res/values-az-rAZ/strings.xml +++ b/common/res/values-az-rAZ/strings.xml @@ -18,6 +18,7 @@ "Hazırdır" + "Keçin" "S%1$s: Ep.%2$s" "Epizod%1$s" "S%1$s: Ep. %2$s %3$s" diff --git a/common/res/values-bg/strings.xml b/common/res/values-bg/strings.xml index 264a1118..6a709576 100644 --- a/common/res/values-bg/strings.xml +++ b/common/res/values-bg/strings.xml @@ -18,6 +18,7 @@ "Готово" + "Пропускане" "Сезон %1$s: епизод %2$s" "Епизод %1$s" "Сезон %1$s: Еп. %2$s – „%3$s“" diff --git a/common/res/values-bn-rBD/strings.xml b/common/res/values-bn-rBD/strings.xml index 836a62a5..a4ae1b00 100644 --- a/common/res/values-bn-rBD/strings.xml +++ b/common/res/values-bn-rBD/strings.xml @@ -18,6 +18,7 @@ "সম্পন্ন" + "এড়িয়ে যান" "সিজন %1$s , পর্ব %2$s" "পর্ব%1$s" "সিঃ%1$s: এপিঃ %2$s %3$s" diff --git a/common/res/values-ca/strings.xml b/common/res/values-ca/strings.xml index 6382648e..42b2899f 100644 --- a/common/res/values-ca/strings.xml +++ b/common/res/values-ca/strings.xml @@ -18,6 +18,7 @@ "Fet" + "Omet" "Temporada %1$s, episodi %2$s" "Episodi %1$s" "Temporada %1$s, episodi %2$s: %3$s" diff --git a/common/res/values-cs/strings.xml b/common/res/values-cs/strings.xml index 0be8026c..6cae0a06 100644 --- a/common/res/values-cs/strings.xml +++ b/common/res/values-cs/strings.xml @@ -18,6 +18,7 @@ "Hotovo" + "Přeskočit" "S%1$sE%2$s" "E%1$s" "S%1$s: Ep. %2$s%3$s" diff --git a/common/res/values-da/strings.xml b/common/res/values-da/strings.xml index 4685b818..b5cab122 100644 --- a/common/res/values-da/strings.xml +++ b/common/res/values-da/strings.xml @@ -18,6 +18,7 @@ "Udført" + "Spring over" "S. %1$s, afsn. %2$s" "Afsn. %1$s" "S. %1$s: Afsnit %2$s, %3$s" diff --git a/common/res/values-de/strings.xml b/common/res/values-de/strings.xml index a487ff8a..30edbbd9 100644 --- a/common/res/values-de/strings.xml +++ b/common/res/values-de/strings.xml @@ -18,6 +18,7 @@ "Fertig" + "Überspringen" "S%1$s: F%2$s" "Folge %1$s" "Staffel %1$s: Folge %2$s, %3$s" diff --git a/common/res/values-el/strings.xml b/common/res/values-el/strings.xml index dba894cb..fda2792e 100644 --- a/common/res/values-el/strings.xml +++ b/common/res/values-el/strings.xml @@ -18,6 +18,7 @@ "Τέλος" + "Παράβλεψη" "Σεζ. %1$s: Επ.%2$s" "Επ.%1$s" "Σεζ.%1$s: Επ. %2$s %3$s" diff --git a/common/res/values-en-rAU/strings.xml b/common/res/values-en-rAU/strings.xml index 0ea27edc..caff8bd7 100644 --- a/common/res/values-en-rAU/strings.xml +++ b/common/res/values-en-rAU/strings.xml @@ -18,6 +18,7 @@ "Finished" + "Skip" "S%1$s: Ep.%2$s" "Ep.%1$s" "S%1$s: Ep. %2$s %3$s" diff --git a/common/res/values-en-rGB/strings.xml b/common/res/values-en-rGB/strings.xml index 0ea27edc..caff8bd7 100644 --- a/common/res/values-en-rGB/strings.xml +++ b/common/res/values-en-rGB/strings.xml @@ -18,6 +18,7 @@ "Finished" + "Skip" "S%1$s: Ep.%2$s" "Ep.%1$s" "S%1$s: Ep. %2$s %3$s" diff --git a/common/res/values-en-rIN/strings.xml b/common/res/values-en-rIN/strings.xml index 0ea27edc..caff8bd7 100644 --- a/common/res/values-en-rIN/strings.xml +++ b/common/res/values-en-rIN/strings.xml @@ -18,6 +18,7 @@ "Finished" + "Skip" "S%1$s: Ep.%2$s" "Ep.%1$s" "S%1$s: Ep. %2$s %3$s" diff --git a/common/res/values-es-rUS/strings.xml b/common/res/values-es-rUS/strings.xml index 87c881b8..d9b3d403 100644 --- a/common/res/values-es-rUS/strings.xml +++ b/common/res/values-es-rUS/strings.xml @@ -18,6 +18,7 @@ "Listo" + "Omitir" "Temporada %1$s: episodio %2$s" "Episodio %1$s" "Temporada %1$s, episodio %2$s: %3$s" diff --git a/common/res/values-es/strings.xml b/common/res/values-es/strings.xml index 32536d93..9f1cae91 100644 --- a/common/res/values-es/strings.xml +++ b/common/res/values-es/strings.xml @@ -18,6 +18,7 @@ "Listo" + "Saltar" "Temporada %1$s: episodio %2$s" "Episodio %1$s" "Temporada %1$s: episodio %2$s %3$s" diff --git a/common/res/values-et-rEE/strings.xml b/common/res/values-et-rEE/strings.xml index 9052178a..974a8a86 100644 --- a/common/res/values-et-rEE/strings.xml +++ b/common/res/values-et-rEE/strings.xml @@ -18,6 +18,7 @@ "Valmis" + "Jäta vahele" "%1$s. hooaeg: %2$s. jagu" "%1$s. jagu" "%1$s. hooaeg: %2$s. jagu – %3$s" diff --git a/common/res/values-eu-rES/strings.xml b/common/res/values-eu-rES/strings.xml index d7ab52a8..3d4e1618 100644 --- a/common/res/values-eu-rES/strings.xml +++ b/common/res/values-eu-rES/strings.xml @@ -18,6 +18,7 @@ "Eginda" + "Saltatu" "%1$s. denboraldiko %2$s. atala" "%1$s. atala" "%1$s. denboraldiko %2$s. atala: %3$s" diff --git a/common/res/values-fa/strings.xml b/common/res/values-fa/strings.xml index 67e9c4e6..dfa221f9 100644 --- a/common/res/values-fa/strings.xml +++ b/common/res/values-fa/strings.xml @@ -18,6 +18,7 @@ "تمام" + "رد شدن" "فصل%1$s: قسمت%2$s" "اپیزود %1$s" "ف.%1$s: ق. %2$s %3$s" diff --git a/common/res/values-fi/strings.xml b/common/res/values-fi/strings.xml index 8c6b99bf..f498a8e4 100644 --- a/common/res/values-fi/strings.xml +++ b/common/res/values-fi/strings.xml @@ -18,6 +18,7 @@ "Valmis" + "Ohita" "Kausi %1$s, jakso %2$s" "Jakso %1$s" "Kausi %1$s, jakso %2$s: %3$s" diff --git a/common/res/values-fr-rCA/strings.xml b/common/res/values-fr-rCA/strings.xml index ed5678e2..cc5eb003 100644 --- a/common/res/values-fr-rCA/strings.xml +++ b/common/res/values-fr-rCA/strings.xml @@ -18,6 +18,7 @@ "Terminé" + "Ignorer" "Saison %1$s, ép. %2$s" "Ép. %1$s" "Saison %1$s, épisode %2$s, « %3$s »" diff --git a/common/res/values-fr/strings.xml b/common/res/values-fr/strings.xml index b5b28104..4c965508 100644 --- a/common/res/values-fr/strings.xml +++ b/common/res/values-fr/strings.xml @@ -18,6 +18,7 @@ "OK" + "Passer" "Saison %1$s, épisode %2$s" "Épisode %1$s" "S%1$s, ép. %2$s : %3$s" diff --git a/common/res/values-gl-rES/strings.xml b/common/res/values-gl-rES/strings.xml index 356ac562..4efcc838 100644 --- a/common/res/values-gl-rES/strings.xml +++ b/common/res/values-gl-rES/strings.xml @@ -18,6 +18,7 @@ "Feito" + "Omitir" "T %1$s: Ep. %2$s" "Ep. %1$s" "T%1$s: Ep. %2$s %3$s" diff --git a/common/res/values-hi/strings.xml b/common/res/values-hi/strings.xml index 27cae84d..71e273c9 100644 --- a/common/res/values-hi/strings.xml +++ b/common/res/values-hi/strings.xml @@ -18,6 +18,7 @@ "हो गया" + "अभी नहीं" "सीज़न%1$s: एपिसोड%2$s" "एपिसोड %1$s" "सीज़न%1$s: एपिसोड %2$s %3$s" diff --git a/common/res/values-hr/strings.xml b/common/res/values-hr/strings.xml index 66127784..8239b247 100644 --- a/common/res/values-hr/strings.xml +++ b/common/res/values-hr/strings.xml @@ -18,6 +18,7 @@ "Gotovo" + "Preskoči" "S. %1$s: Ep. %2$s" "Ep. %1$s" "S. %1$s: ep. %2$s %3$s" diff --git a/common/res/values-hu/strings.xml b/common/res/values-hu/strings.xml index 138ad857..778d9fa2 100644 --- a/common/res/values-hu/strings.xml +++ b/common/res/values-hu/strings.xml @@ -18,6 +18,7 @@ "Kész" + "Kihagyás" "%1$s. évad, %2$s. rész" "%1$s. rész" "%1$s. évad, %2$s. rész: %3$s" diff --git a/common/res/values-hy-rAM/strings.xml b/common/res/values-hy-rAM/strings.xml index aaa7470f..2af99e03 100644 --- a/common/res/values-hy-rAM/strings.xml +++ b/common/res/values-hy-rAM/strings.xml @@ -18,6 +18,7 @@ "Պատրաստ է" + "Բաց թողնել" %1$s՝ դրվ. %2$s" "Դրվ. %1$s" %1$s՝ դրվ. %2$s %3$s" diff --git a/common/res/values-in/strings.xml b/common/res/values-in/strings.xml index da4b8203..ae16a0e5 100644 --- a/common/res/values-in/strings.xml +++ b/common/res/values-in/strings.xml @@ -18,6 +18,7 @@ "Selesai" + "Lewati" "S%1$s: Ep.%2$s" "Ep.%1$s" "S%1$s: Ep. %2$s %3$s" diff --git a/common/res/values-is-rIS/strings.xml b/common/res/values-is-rIS/strings.xml index 47529403..9f402529 100644 --- a/common/res/values-is-rIS/strings.xml +++ b/common/res/values-is-rIS/strings.xml @@ -18,6 +18,7 @@ "Lokið" + "Sleppa" "S.%1$s: Þ.%2$s" "%1$s. þáttur" "S.%1$s: Þ. %2$s %3$s" diff --git a/common/res/values-it/strings.xml b/common/res/values-it/strings.xml index f61e02f7..ce6543dd 100644 --- a/common/res/values-it/strings.xml +++ b/common/res/values-it/strings.xml @@ -18,6 +18,7 @@ "Fine" + "Ignora" "Stagione %1$s, puntata %2$s" "Ep. %1$s" "Stag. %1$s: punt. %2$s %3$s" diff --git a/common/res/values-iw/strings.xml b/common/res/values-iw/strings.xml index 7ad220ad..b4b3b191 100644 --- a/common/res/values-iw/strings.xml +++ b/common/res/values-iw/strings.xml @@ -18,6 +18,7 @@ "סיום" + "דלג" "עונה %1$s: פרק %2$s" "פרק %1$s" "עונה%1$s: פרק %2$s %3$s" diff --git a/common/res/values-ja/strings.xml b/common/res/values-ja/strings.xml index 682fd0d8..82853c92 100644 --- a/common/res/values-ja/strings.xml +++ b/common/res/values-ja/strings.xml @@ -18,6 +18,7 @@ "完了" + "スキップ" "シーズン %1$s: エピソード %2$s" "エピソード %1$s" "シーズン %1$s: エピソード %2$s%3$s」" diff --git a/common/res/values-ka-rGE/strings.xml b/common/res/values-ka-rGE/strings.xml index 3c7062ff..03db3fa9 100644 --- a/common/res/values-ka-rGE/strings.xml +++ b/common/res/values-ka-rGE/strings.xml @@ -18,6 +18,7 @@ "მზადაა" + "გამოტოვება" "სეზ. %1$s: ეპ. %2$s" "ეპ. %1$s" "სეზ. %1$s, ეპ. %2$s%3$s" diff --git a/common/res/values-kk-rKZ/strings.xml b/common/res/values-kk-rKZ/strings.xml index 12de7d34..bfb4ef19 100644 --- a/common/res/values-kk-rKZ/strings.xml +++ b/common/res/values-kk-rKZ/strings.xml @@ -18,6 +18,7 @@ "Орындалды" + "Өткізіп жіберу" "%1$s-маусым, %2$s-серия" "%1$s-серия" "%1$s-маусым: %2$s-серия, \"%3$s\"" diff --git a/common/res/values-km-rKH/strings.xml b/common/res/values-km-rKH/strings.xml index b8353897..c084ef39 100644 --- a/common/res/values-km-rKH/strings.xml +++ b/common/res/values-km-rKH/strings.xml @@ -18,6 +18,7 @@ "រួចរាល់" + "រំលង" "រដូវកាល%1$s៖ ភាគ%2$s" "ភាគ៖ %1$s" "រដូវកាលទី %1$s៖ វគ្គ %2$s %3$s" diff --git a/common/res/values-kn-rIN/strings.xml b/common/res/values-kn-rIN/strings.xml index 7cb096b7..1b414300 100644 --- a/common/res/values-kn-rIN/strings.xml +++ b/common/res/values-kn-rIN/strings.xml @@ -18,6 +18,7 @@ "ಮುಗಿದಿದೆ" + "ಸ್ಕಿಪ್‌ ಮಾಡಿ" "ಕಾಲ%1$sಭಾಗ%2$s" "ಭಾಗ%1$s" "ಸೀ%1$s: ಸಂ. %2$s %3$s" diff --git a/common/res/values-ko/strings.xml b/common/res/values-ko/strings.xml index 923831f3..af5286ff 100644 --- a/common/res/values-ko/strings.xml +++ b/common/res/values-ko/strings.xml @@ -18,6 +18,7 @@ "완료" + "건너뛰기" "시즌 %1$s: 에피소드 %2$s" "에피소드 %1$s" "시즌 %1$s: 에피소드 %2$s \'%3$s\'" diff --git a/common/res/values-ky-rKG/strings.xml b/common/res/values-ky-rKG/strings.xml index 8bdfadde..8be45528 100644 --- a/common/res/values-ky-rKG/strings.xml +++ b/common/res/values-ky-rKG/strings.xml @@ -18,6 +18,7 @@ "Бүттү" + "Өткөрүп жиберүү" "%1$s-мезгил: %2$s-серия" "%1$s-серия" %1$s: %3$s %2$s-серия" diff --git a/common/res/values-lo-rLA/strings.xml b/common/res/values-lo-rLA/strings.xml index a6dad515..b6badeb0 100644 --- a/common/res/values-lo-rLA/strings.xml +++ b/common/res/values-lo-rLA/strings.xml @@ -18,6 +18,7 @@ "ສຳເລັດແລ້ວ" + "ຂ້າມ" "S%1$s: Ep.%2$s" "Ep.%1$s" "S%1$s: Ep. %2$s %3$s" diff --git a/common/res/values-lt/strings.xml b/common/res/values-lt/strings.xml index 6d6c0ee8..434a7a6e 100644 --- a/common/res/values-lt/strings.xml +++ b/common/res/values-lt/strings.xml @@ -18,6 +18,7 @@ "Atlikta" + "Praleisti" "%1$s sezonas: %2$s serija" "%1$s serija" "%1$s sezonas: %2$s serija „%3$s“" diff --git a/common/res/values-lv/strings.xml b/common/res/values-lv/strings.xml index f0366a48..e6466138 100644 --- a/common/res/values-lv/strings.xml +++ b/common/res/values-lv/strings.xml @@ -18,6 +18,7 @@ "Gatavs" + "Izlaist" "%1$s. sez., %2$s. sēr." "%1$s. sēr." "%1$s. sezona: %2$s. sērija “%3$s”" diff --git a/common/res/values-mk-rMK/strings.xml b/common/res/values-mk-rMK/strings.xml index 73045d3d..673b17e2 100644 --- a/common/res/values-mk-rMK/strings.xml +++ b/common/res/values-mk-rMK/strings.xml @@ -18,6 +18,7 @@ "Готово" + "Прескокни" "Сез.%1$s: Еп.%2$s" "Еп.%1$s" "Сез. %1$s: Еп. %2$s %3$s" diff --git a/common/res/values-ml-rIN/strings.xml b/common/res/values-ml-rIN/strings.xml index 07727684..217fa52c 100644 --- a/common/res/values-ml-rIN/strings.xml +++ b/common/res/values-ml-rIN/strings.xml @@ -18,6 +18,7 @@ "പൂർത്തിയായി" + "ഒഴിവാക്കുക" "സീസൺ%1$s: എപ്പി.%2$s" "എപ്പി.%1$s" "സീസൺ%1$s: എപ്പിസോഡ് %2$s %3$s" diff --git a/common/res/values-mn-rMN/strings.xml b/common/res/values-mn-rMN/strings.xml index ff1552e0..20d4de81 100644 --- a/common/res/values-mn-rMN/strings.xml +++ b/common/res/values-mn-rMN/strings.xml @@ -18,6 +18,7 @@ "Дууссан" + "Алгасах" "Бүлэг%1$s: Цуврал.%2$s" "Анги.%1$s" "Бүлэг%1$s: Анги. %2$s %3$s" diff --git a/common/res/values-mr-rIN/strings.xml b/common/res/values-mr-rIN/strings.xml index adc669ae..873b955c 100644 --- a/common/res/values-mr-rIN/strings.xml +++ b/common/res/values-mr-rIN/strings.xml @@ -18,6 +18,7 @@ "पूर्ण झाले" + "वगळा" "हंगाम%1$s: भाग%2$s" "भाग%1$s" "हंगाम%1$s: भाग. %2$s %3$s" diff --git a/common/res/values-ms-rMY/strings.xml b/common/res/values-ms-rMY/strings.xml index 06eb6173..7b19eeb4 100644 --- a/common/res/values-ms-rMY/strings.xml +++ b/common/res/values-ms-rMY/strings.xml @@ -18,6 +18,7 @@ "Selesai" + "Langkau" "M%1$s: Ep.%2$s" "Ep.%1$s" "M%1$s: Ep. %2$s %3$s" diff --git a/common/res/values-my-rMM/strings.xml b/common/res/values-my-rMM/strings.xml index 977c4315..53503d51 100644 --- a/common/res/values-my-rMM/strings.xml +++ b/common/res/values-my-rMM/strings.xml @@ -18,6 +18,7 @@ "ပြီးပါပြီ" + "ကျော်ရန်" "အတွဲ%1$s− အပိုင်း%2$s" "အပိုင်း %1$s" "အတွဲ%1$s− အပိုင်း %2$s %3$s" diff --git a/common/res/values-nb/strings.xml b/common/res/values-nb/strings.xml index 8344a76b..7716d086 100644 --- a/common/res/values-nb/strings.xml +++ b/common/res/values-nb/strings.xml @@ -18,6 +18,7 @@ "Ferdig" + "Hopp over" "Sesong %1$s episode %2$s" "Episode %1$s" "Sesong %1$s: episode %2$s%3$s" diff --git a/common/res/values-ne-rNP/strings.xml b/common/res/values-ne-rNP/strings.xml index 4f3d24c1..37489a5d 100644 --- a/common/res/values-ne-rNP/strings.xml +++ b/common/res/values-ne-rNP/strings.xml @@ -18,6 +18,7 @@ "सम्पन्न भयो" + "छाड्नुहोस्" "सिजन %1$s: एपिसोड %2$s" "एपिसोड %1$s" "सिजन %1$s: एपिसोड %2$s %3$s" diff --git a/common/res/values-nl/strings.xml b/common/res/values-nl/strings.xml index 4e5a1011..e7cab5c7 100644 --- a/common/res/values-nl/strings.xml +++ b/common/res/values-nl/strings.xml @@ -18,6 +18,7 @@ "Gereed" + "Overslaan" "S %1$s afl. %2$s" "Afl. %1$s" "S. %1$s: afl. %2$s %3$s" diff --git a/common/res/values-pl/strings.xml b/common/res/values-pl/strings.xml index 5161e3ba..74a7dd89 100644 --- a/common/res/values-pl/strings.xml +++ b/common/res/values-pl/strings.xml @@ -18,6 +18,7 @@ "Gotowe" + "Pomiń" "Sez. %1$s: odc. %2$s" "Odc. %1$s" "Sez. %1$s, odc. %2$s: %3$s" diff --git a/common/res/values-pt-rPT/strings.xml b/common/res/values-pt-rPT/strings.xml index badaae27..cd370aeb 100644 --- a/common/res/values-pt-rPT/strings.xml +++ b/common/res/values-pt-rPT/strings.xml @@ -18,6 +18,7 @@ "Concluído" + "Ignorar" "T. %1$s: ep. %2$s" "Ep. %1$s" "T%1$s: ep. %2$s %3$s" diff --git a/common/res/values-pt/strings.xml b/common/res/values-pt/strings.xml index 0fc3e097..2cdde289 100644 --- a/common/res/values-pt/strings.xml +++ b/common/res/values-pt/strings.xml @@ -18,6 +18,7 @@ "Concluído" + "Pular" "T%1$s: Ep.%2$s" "Ep.%1$s" "T%1$s: Ep. %2$s %3$s" diff --git a/common/res/values-ro/strings.xml b/common/res/values-ro/strings.xml index ae6fc497..4b7cd74f 100644 --- a/common/res/values-ro/strings.xml +++ b/common/res/values-ro/strings.xml @@ -18,6 +18,7 @@ "Terminat" + "Omiteți" "Sezonul %1$s, episodul %2$s" "Episodul %1$s" "Sezonul %1$s, episodul %2$s: %3$s" diff --git a/common/res/values-ru/strings.xml b/common/res/values-ru/strings.xml index 53c341aa..d44ccaa7 100644 --- a/common/res/values-ru/strings.xml +++ b/common/res/values-ru/strings.xml @@ -18,6 +18,7 @@ "Готово" + "Пропустить" "Сезон %1$s, серия %2$s" "Серия %1$s" "Сезон %1$s: серия %2$s, \"%3$s\"" diff --git a/common/res/values-si-rLK/strings.xml b/common/res/values-si-rLK/strings.xml index 85fc1f94..ecaf24fd 100644 --- a/common/res/values-si-rLK/strings.xml +++ b/common/res/values-si-rLK/strings.xml @@ -18,6 +18,7 @@ "නිමයි" + "මඟ හරින්න" "වාරය%1$sකථාංගය%2$s" "කථාංගය %1$s" "වාරය%1$s: කථාංගය %2$s %3$s" diff --git a/common/res/values-sk/strings.xml b/common/res/values-sk/strings.xml index 1d1a673f..e917306d 100644 --- a/common/res/values-sk/strings.xml +++ b/common/res/values-sk/strings.xml @@ -18,6 +18,7 @@ "Hotovo" + "Preskočiť" "S%1$s: ep.%2$s" "E%1$s" "S %1$s: ep. %2$s%3$s" diff --git a/common/res/values-sl/strings.xml b/common/res/values-sl/strings.xml index 26ec614d..061b416d 100644 --- a/common/res/values-sl/strings.xml +++ b/common/res/values-sl/strings.xml @@ -18,6 +18,7 @@ "Končano" + "Preskoči" "%1$s. sezona: %2$s. epizoda" "%1$s. epizoda" "%1$s. sezona – %2$s. epizoda: %3$s" diff --git a/common/res/values-sr/strings.xml b/common/res/values-sr/strings.xml index ef7f9ee5..3617a3d9 100644 --- a/common/res/values-sr/strings.xml +++ b/common/res/values-sr/strings.xml @@ -18,6 +18,7 @@ "Готово" + "Прескочи" "Серијал: %1$s: Епизода: %2$s" "%1$s. епизода" "%1$s. серијал: %2$s. епизода, %3$s" diff --git a/common/res/values-sv/strings.xml b/common/res/values-sv/strings.xml index 9911a0de..6394a107 100644 --- a/common/res/values-sv/strings.xml +++ b/common/res/values-sv/strings.xml @@ -18,6 +18,7 @@ "Klar" + "Hoppa över" "Säsong %1$s, avsnitt %2$s" "Avsnitt %1$s" "Säsong %1$s: avsnitt %2$s%3$s" diff --git a/common/res/values-sw/strings.xml b/common/res/values-sw/strings.xml index 7d44cedf..3416b95a 100644 --- a/common/res/values-sw/strings.xml +++ b/common/res/values-sw/strings.xml @@ -18,6 +18,7 @@ "Nimemaliza" + "Ruka" "Msimu wa %1$s: Kipindi cha %2$s" "Kipindi cha %1$s" "Msimu wa %1$s: Kipindi cha %2$s %3$s" diff --git a/common/res/values-ta-rIN/strings.xml b/common/res/values-ta-rIN/strings.xml index bc2eed62..558bd66f 100644 --- a/common/res/values-ta-rIN/strings.xml +++ b/common/res/values-ta-rIN/strings.xml @@ -18,6 +18,7 @@ "முடிந்தது" + "தவிர்" "சீ%1$s: எபி.%2$s" "எபி.%1$s" "சீ%1$s: எபி. %2$s %3$s" diff --git a/common/res/values-te-rIN/strings.xml b/common/res/values-te-rIN/strings.xml index 80df229d..ac7e3987 100644 --- a/common/res/values-te-rIN/strings.xml +++ b/common/res/values-te-rIN/strings.xml @@ -18,6 +18,7 @@ "పూర్తయింది" + "దాటవేయి" "సీ%1$s: ఎపి.%2$s" "ఎపి.%1$s" "సీ%1$s: ఎపి. %2$s %3$s" diff --git a/common/res/values-th/strings.xml b/common/res/values-th/strings.xml index 5ff9f1ff..584c2703 100644 --- a/common/res/values-th/strings.xml +++ b/common/res/values-th/strings.xml @@ -18,6 +18,7 @@ "เสร็จสิ้น" + "ข้าม" "ซีซัน %1$s: ตอนที่ %2$s" "ตอนที่ %1$s" "ซีซัน %1$s: ตอนที่ %2$s %3$s" diff --git a/common/res/values-tl/strings.xml b/common/res/values-tl/strings.xml index e6b8bbbc..230c3448 100644 --- a/common/res/values-tl/strings.xml +++ b/common/res/values-tl/strings.xml @@ -18,6 +18,7 @@ "Tapos Na" + "Laktawan" "S%1$s: Ep.%2$s" "Ep.%1$s" "S%1$s: Ep. %2$s %3$s" diff --git a/common/res/values-tr/strings.xml b/common/res/values-tr/strings.xml index c25c834f..1356a1b8 100644 --- a/common/res/values-tr/strings.xml +++ b/common/res/values-tr/strings.xml @@ -18,6 +18,7 @@ "Bitti" + "Atla" "S%1$s: Böl.%2$s" "Böl.%1$s" "S%1$s: %2$s. Bölüm %3$s" diff --git a/common/res/values-uk/strings.xml b/common/res/values-uk/strings.xml index 9b67cf4a..b8de0ee1 100644 --- a/common/res/values-uk/strings.xml +++ b/common/res/values-uk/strings.xml @@ -18,6 +18,7 @@ "Готово" + "Пропустити" "Сезон %1$s: серія %2$s" "Серія %1$s" "Сезон %1$s: серія %2$s \"%3$s\"" diff --git a/common/res/values-ur-rPK/strings.xml b/common/res/values-ur-rPK/strings.xml index b04fb3c3..0cfa3b5e 100644 --- a/common/res/values-ur-rPK/strings.xml +++ b/common/res/values-ur-rPK/strings.xml @@ -18,6 +18,7 @@ "ہو گیا" + "نظر انداز کریں" "سیزن%1$s: ایپی سوڈ۔%2$s" "ایپی سوڈ %1$s" "سیزن %1$s: ایپی سوڈ %2$s %3$s" diff --git a/common/res/values-uz-rUZ/strings.xml b/common/res/values-uz-rUZ/strings.xml index ffa1cf9c..241e15a4 100644 --- a/common/res/values-uz-rUZ/strings.xml +++ b/common/res/values-uz-rUZ/strings.xml @@ -18,6 +18,7 @@ "Tayyor" + "Tashlab ketish" "%1$s-fasl: %2$s-qism" "%1$s-qism" "%1$s-fasl: %2$s-qism – “%3$s”" diff --git a/common/res/values-vi/strings.xml b/common/res/values-vi/strings.xml index 2852bb10..dcc25f00 100644 --- a/common/res/values-vi/strings.xml +++ b/common/res/values-vi/strings.xml @@ -18,6 +18,7 @@ "Xong" + "Bỏ qua" "Phần%1$s: Tập%2$s" "Tập%1$s" "Phần %1$s: Tập %2$s %3$s" diff --git a/common/res/values-zh-rCN/strings.xml b/common/res/values-zh-rCN/strings.xml index 25e13379..519a1ef8 100644 --- a/common/res/values-zh-rCN/strings.xml +++ b/common/res/values-zh-rCN/strings.xml @@ -18,6 +18,7 @@ "完成" + "跳过" "第 %1$s 季:第 %2$s 集" "第 %1$s 集" "第 %1$s 季第 %2$s 集:%3$s" diff --git a/common/res/values-zh-rHK/strings.xml b/common/res/values-zh-rHK/strings.xml index ed323c6a..56249edb 100644 --- a/common/res/values-zh-rHK/strings.xml +++ b/common/res/values-zh-rHK/strings.xml @@ -18,6 +18,7 @@ "完成" + "略過" "第 %1$s 季:第 %2$s 集" "第 %1$s 集" "第 %1$s 季:第 %2$s%3$s" diff --git a/common/res/values-zh-rTW/strings.xml b/common/res/values-zh-rTW/strings.xml index 25e13379..7bbb81fa 100644 --- a/common/res/values-zh-rTW/strings.xml +++ b/common/res/values-zh-rTW/strings.xml @@ -18,6 +18,7 @@ "完成" + "略過" "第 %1$s 季:第 %2$s 集" "第 %1$s 集" "第 %1$s 季第 %2$s 集:%3$s" diff --git a/common/res/values-zu/strings.xml b/common/res/values-zu/strings.xml index 4574ac01..c8e6653d 100644 --- a/common/res/values-zu/strings.xml +++ b/common/res/values-zu/strings.xml @@ -18,6 +18,7 @@ "Kwenziwe" + "Yeqa" "S%1$s: Ep.%2$s" "Ep.%1$s" "S%1$s: Ep. %2$s %3$s" diff --git a/common/res/values/dimens.xml b/common/res/values/dimens.xml index 9d6e0e67..746b12c9 100644 --- a/common/res/values/dimens.xml +++ b/common/res/values/dimens.xml @@ -16,10 +16,6 @@ --> - - 300dp - -300dp - 600dp 476dp @@ -30,7 +26,6 @@ 40dp 40dp 12dp - 5dp 1 10dp diff --git a/common/res/values/strings.xml b/common/res/values/strings.xml index e5b9b625..97fbea97 100644 --- a/common/res/values/strings.xml +++ b/common/res/values/strings.xml @@ -17,6 +17,7 @@ Done + Skip S%1$s: Ep.%2$s diff --git a/common/res/values/styles.xml b/common/res/values/styles.xml index 3c3c71c9..50ef08f1 100644 --- a/common/res/values/styles.xml +++ b/common/res/values/styles.xml @@ -57,15 +57,9 @@ 16sp - - diff --git a/common/res/values/themes.xml b/common/res/values/themes.xml index 598ae9a5..6016daf9 100644 --- a/common/res/values/themes.xml +++ b/common/res/values/themes.xml @@ -31,7 +31,6 @@ @style/Widget.Setup.GuidedActionItemDescriptionStyle @color/common_setup_action_background 0dp - @style/Widget.Setup.GuidedActionsListStyle @drawable/setup_selector_background @android:color/transparent @android:color/transparent diff --git a/common/src/com/android/tv/common/SharedPreferencesUtils.java b/common/src/com/android/tv/common/SharedPreferencesUtils.java index fb3d9b56..1b929d3b 100644 --- a/common/src/com/android/tv/common/SharedPreferencesUtils.java +++ b/common/src/com/android/tv/common/SharedPreferencesUtils.java @@ -35,6 +35,7 @@ public final class SharedPreferencesUtils { public static final String SHARED_PREF_RECURRING_RUNNER = "sharedPreferencesRecurringRunner"; public static final String SHARED_PREF_EPG = "epg_preferences"; public static final String SHARED_PREF_SERIES_RECORDINGS = "seriesRecordings"; + public static final String SHARED_PREF_CHANNEL_LOGO_URIS = "channelLogoUris"; private static boolean sInitializeCalled; diff --git a/common/src/com/android/tv/common/TvCommonUtils.java b/common/src/com/android/tv/common/TvCommonUtils.java index a88dd3a8..c391ad24 100644 --- a/common/src/com/android/tv/common/TvCommonUtils.java +++ b/common/src/com/android/tv/common/TvCommonUtils.java @@ -23,6 +23,8 @@ import android.media.tv.TvInputInfo; * Util class for common use in TV app and inputs. */ public final class TvCommonUtils { + private static Boolean sRunningInTest; + private TvCommonUtils() { } /** @@ -58,12 +60,15 @@ public final class TvCommonUtils { * the usual devices even the application is running in tests. We need to figure it out by * checking whether the class in tv-tests-common module can be loaded or not. */ - public static boolean isRunningInTest() { - try { - Class.forName("com.android.tv.testing.Utils"); - return true; - } catch (ClassNotFoundException e) { - return false; + public static synchronized boolean isRunningInTest() { + if (sRunningInTest == null) { + try { + Class.forName("com.android.tv.testing.Utils"); + sRunningInTest = true; + } catch (ClassNotFoundException e) { + sRunningInTest = false; + } } + return sRunningInTest; } } diff --git a/common/src/com/android/tv/common/TvContentRatingCache.java b/common/src/com/android/tv/common/TvContentRatingCache.java index 5694cda7..8b3c06f1 100644 --- a/common/src/com/android/tv/common/TvContentRatingCache.java +++ b/common/src/com/android/tv/common/TvContentRatingCache.java @@ -43,6 +43,7 @@ public final class TvContentRatingCache implements MemoryManageable { return INSTANCE; } + // @GuardedBy("TvContentRatingCache.this") private final Map mRatingsMultiMap = new ArrayMap<>(); /** @@ -51,7 +52,7 @@ public final class TvContentRatingCache implements MemoryManageable { * Returns {@code null} if the string is empty or contains no valid ratings. */ @Nullable - public TvContentRating[] getRatings(String commaSeparatedRatings) { + public synchronized TvContentRating[] getRatings(String commaSeparatedRatings) { if (TextUtils.isEmpty(commaSeparatedRatings)) { return null; } @@ -136,7 +137,7 @@ public final class TvContentRatingCache implements MemoryManageable { } @Override - public void performTrimMemory(int level) { + public synchronized void performTrimMemory(int level) { mRatingsMultiMap.clear(); } diff --git a/common/src/com/android/tv/common/feature/SharedPreferencesFeature.java b/common/src/com/android/tv/common/feature/SharedPreferencesFeature.java index a4a79b38..881f53d6 100644 --- a/common/src/com/android/tv/common/feature/SharedPreferencesFeature.java +++ b/common/src/com/android/tv/common/feature/SharedPreferencesFeature.java @@ -19,7 +19,6 @@ package com.android.tv.common.feature; import android.content.Context; import android.content.SharedPreferences; import android.util.Log; - import com.android.tv.common.SharedPreferencesUtils; /** diff --git a/common/src/com/android/tv/common/feature/TestableFeature.java b/common/src/com/android/tv/common/feature/TestableFeature.java index a02877ec..d7e707a1 100644 --- a/common/src/com/android/tv/common/feature/TestableFeature.java +++ b/common/src/com/android/tv/common/feature/TestableFeature.java @@ -36,14 +36,29 @@ public class TestableFeature implements Feature { private final Feature mDelegate; private Boolean mTestValue = null; + /** + * Creates testable feature. + */ public static TestableFeature createTestableFeature(Feature delegate) { return new TestableFeature(delegate); } + /** + * Creates testable feature with initial value. + */ + public static TestableFeature createTestableFeature(Feature delegate, Boolean initialValue) { + return new TestableFeature(delegate, initialValue); + } + private TestableFeature(Feature delegate) { mDelegate = delegate; } + private TestableFeature(Feature delegate, Boolean initialValue) { + mDelegate = delegate; + mTestValue = initialValue; + } + @VisibleForTesting public void enableForTest() { if (!TvCommonUtils.isRunningInTest()) { diff --git a/common/src/com/android/tv/common/ui/setup/SetupMultiPaneFragment.java b/common/src/com/android/tv/common/ui/setup/SetupMultiPaneFragment.java index 63247481..b9ad4657 100644 --- a/common/src/com/android/tv/common/ui/setup/SetupMultiPaneFragment.java +++ b/common/src/com/android/tv/common/ui/setup/SetupMultiPaneFragment.java @@ -34,6 +34,7 @@ public abstract class SetupMultiPaneFragment extends SetupFragment { private static final boolean DEBUG = false; public static final int ACTION_DONE = Integer.MAX_VALUE; + public static final int ACTION_SKIP = ACTION_DONE - 1; private static final String CONTENT_FRAGMENT_TAG = "content_fragment"; @@ -53,7 +54,12 @@ public abstract class SetupMultiPaneFragment extends SetupFragment { } if (needsDoneButton()) { setOnClickAction(view.findViewById(R.id.button_done), getActionCategory(), ACTION_DONE); - } else { + } + if (needsSkipButton()) { + view.findViewById(R.id.button_skip).setVisibility(View.VISIBLE); + setOnClickAction(view.findViewById(R.id.button_skip), getActionCategory(), ACTION_SKIP); + } + if (!needsDoneButton() && !needsSkipButton()) { View doneButtonContainer = view.findViewById(R.id.done_button_container); // Use content view to check layout direction while view is being created. if (getResources().getConfiguration().getLayoutDirection() @@ -90,6 +96,10 @@ public abstract class SetupMultiPaneFragment extends SetupFragment { return true; } + protected boolean needsSkipButton() { + return false; + } + @Override protected int[] getParentIdsForDelay() { return new int[] {R.id.content_fragment, R.id.guidedactions_list}; @@ -99,4 +109,4 @@ public abstract class SetupMultiPaneFragment extends SetupFragment { public int[] getSharedElementIds() { return new int[] {R.id.action_fragment_background, R.id.done_button_container}; } -} +} \ No newline at end of file diff --git a/jni/Android.mk b/jni/Android.mk index 684830c9..9cb3d42e 100644 --- a/jni/Android.mk +++ b/jni/Android.mk @@ -1,12 +1,30 @@ +# +# 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. +# + LOCAL_PATH := $(call my-dir) # -------------------------------------------------------------- include $(CLEAR_VARS) -LOCAL_MODULE := libtunertvinput_jni +LOCAL_MODULE := libtunertvinput_jni LOCAL_SRC_FILES += tunertvinput_jni.cpp DvbManager.cpp LOCAL_SDK_VERSION := 21 LOCAL_NDK_STL_VARIANT := stlport_static LOCAL_LDLIBS := -llog + include $(BUILD_SHARED_LIBRARY) +include $(call all-makefiles-under,$(LOCAL_PATH)) diff --git a/libs/exoplayer_v2.jar b/libs/exoplayer_v2.jar new file mode 100644 index 00000000..6cfcde9c Binary files /dev/null and b/libs/exoplayer_v2.jar differ diff --git a/libs/exoplayer_v2_ext_ffmpeg.jar b/libs/exoplayer_v2_ext_ffmpeg.jar new file mode 100644 index 00000000..65117267 Binary files /dev/null and b/libs/exoplayer_v2_ext_ffmpeg.jar differ diff --git a/proguard.flags b/proguard.flags index 6ffed3e2..272f21a1 100644 --- a/proguard.flags +++ b/proguard.flags @@ -38,6 +38,9 @@ long getSize(); void close(); } +-keepclasseswithmembers class com.google.android.exoplayer2.ext.ffmpeg { + native ; +} # Keep method which is used for reflection. -keep @com.android.tv.common.annotation.UsedByReflection class * {*;} diff --git a/proto/channel.proto b/proto/channel.proto index 982a1aa1..67e415de 100644 --- a/proto/channel.proto +++ b/proto/channel.proto @@ -45,6 +45,8 @@ message TunerChannelProto { repeated AtscCaptionTrack caption_tracks = 20; optional bool has_caption_track = 21; optional AtscServiceType service_type = 22 [default = SERVICE_TYPE_ATSC_DIGITAL_TELEVISION]; + optional bool recording_prohibited = 23; + optional string video_format = 24; } // Enum describing the types of tuner. diff --git a/res/animator/tuning_block_view_fade_out.xml b/res/animator/tuning_block_view_fade_out.xml new file mode 100644 index 00000000..eb40a7b5 --- /dev/null +++ b/res/animator/tuning_block_view_fade_out.xml @@ -0,0 +1,25 @@ + + + + + + diff --git a/res/drawable-xhdpi/dvr_default_poster.png b/res/drawable-xhdpi/dvr_default_poster.png index 683a693d..f42c08dd 100644 Binary files a/res/drawable-xhdpi/dvr_default_poster.png and b/res/drawable-xhdpi/dvr_default_poster.png differ diff --git a/res/drawable-xhdpi/dvr_default_program_art.png b/res/drawable-xhdpi/dvr_default_program_art.png deleted file mode 100644 index 6a8d68ee..00000000 Binary files a/res/drawable-xhdpi/dvr_default_program_art.png and /dev/null differ diff --git a/res/drawable-xhdpi/ic_error_recording.png b/res/drawable-xhdpi/ic_error_recording.png deleted file mode 100644 index 5878c3b3..00000000 Binary files a/res/drawable-xhdpi/ic_error_recording.png and /dev/null differ diff --git a/res/drawable-xhdpi/ic_fresh.png b/res/drawable-xhdpi/ic_fresh.png deleted file mode 100644 index c1bc9583..00000000 Binary files a/res/drawable-xhdpi/ic_fresh.png and /dev/null differ diff --git a/res/drawable-xhdpi/ic_pip_option_input.png b/res/drawable-xhdpi/ic_pip_option_input.png deleted file mode 100644 index 47c5006f..00000000 Binary files a/res/drawable-xhdpi/ic_pip_option_input.png and /dev/null differ diff --git a/res/drawable-xhdpi/ic_pip_option_layout1.png b/res/drawable-xhdpi/ic_pip_option_layout1.png deleted file mode 100644 index 14b2602a..00000000 Binary files a/res/drawable-xhdpi/ic_pip_option_layout1.png and /dev/null differ diff --git a/res/drawable-xhdpi/ic_pip_option_layout2.png b/res/drawable-xhdpi/ic_pip_option_layout2.png deleted file mode 100644 index e5d77278..00000000 Binary files a/res/drawable-xhdpi/ic_pip_option_layout2.png and /dev/null differ diff --git a/res/drawable-xhdpi/ic_pip_option_layout3.png b/res/drawable-xhdpi/ic_pip_option_layout3.png deleted file mode 100644 index dfe110d0..00000000 Binary files a/res/drawable-xhdpi/ic_pip_option_layout3.png and /dev/null differ diff --git a/res/drawable-xhdpi/ic_pip_option_layout4.png b/res/drawable-xhdpi/ic_pip_option_layout4.png deleted file mode 100644 index 8ab5fa45..00000000 Binary files a/res/drawable-xhdpi/ic_pip_option_layout4.png and /dev/null differ diff --git a/res/drawable-xhdpi/ic_pip_option_layout5.png b/res/drawable-xhdpi/ic_pip_option_layout5.png deleted file mode 100644 index d6b53641..00000000 Binary files a/res/drawable-xhdpi/ic_pip_option_layout5.png and /dev/null differ diff --git a/res/drawable-xhdpi/ic_pip_option_size.png b/res/drawable-xhdpi/ic_pip_option_size.png deleted file mode 100644 index 96fb0b0c..00000000 Binary files a/res/drawable-xhdpi/ic_pip_option_size.png and /dev/null differ diff --git a/res/drawable-xhdpi/ic_pip_option_swap.png b/res/drawable-xhdpi/ic_pip_option_swap.png deleted file mode 100644 index fa2088ed..00000000 Binary files a/res/drawable-xhdpi/ic_pip_option_swap.png and /dev/null differ diff --git a/res/drawable-xhdpi/ic_pip_option_swap_audio.png b/res/drawable-xhdpi/ic_pip_option_swap_audio.png deleted file mode 100644 index a5f5431b..00000000 Binary files a/res/drawable-xhdpi/ic_pip_option_swap_audio.png and /dev/null differ diff --git a/res/drawable-xhdpi/ic_recorded_program.png b/res/drawable-xhdpi/ic_recorded_program.png deleted file mode 100644 index fe22714e..00000000 Binary files a/res/drawable-xhdpi/ic_recorded_program.png and /dev/null differ diff --git a/res/drawable-xhdpi/ic_related_actor.png b/res/drawable-xhdpi/ic_related_actor.png deleted file mode 100644 index 9b726b93..00000000 Binary files a/res/drawable-xhdpi/ic_related_actor.png and /dev/null differ diff --git a/res/drawable-xhdpi/ic_related_search.png b/res/drawable-xhdpi/ic_related_search.png deleted file mode 100644 index aa5cd0d2..00000000 Binary files a/res/drawable-xhdpi/ic_related_search.png and /dev/null differ diff --git a/res/drawable-xhdpi/ic_setup_antenna.png b/res/drawable-xhdpi/ic_setup_antenna.png deleted file mode 100644 index bb6d416e..00000000 Binary files a/res/drawable-xhdpi/ic_setup_antenna.png and /dev/null differ diff --git a/res/drawable-xhdpi/ic_tvoption_pip.png b/res/drawable-xhdpi/ic_tvoption_pip.png index 0f78d834..e5d77278 100644 Binary files a/res/drawable-xhdpi/ic_tvoption_pip.png and b/res/drawable-xhdpi/ic_tvoption_pip.png differ diff --git a/res/drawable-xhdpi/ic_tvoption_pip_off.png b/res/drawable-xhdpi/ic_tvoption_pip_off.png deleted file mode 100644 index 6001677b..00000000 Binary files a/res/drawable-xhdpi/ic_tvoption_pip_off.png and /dev/null differ diff --git a/res/drawable-xhdpi/tv_3a_00.png b/res/drawable-xhdpi/tv_3a_00.png deleted file mode 100644 index ad8f256d..00000000 Binary files a/res/drawable-xhdpi/tv_3a_00.png and /dev/null differ diff --git a/res/drawable-xhdpi/tv_error.png b/res/drawable-xhdpi/tv_error.png deleted file mode 100644 index 718f203b..00000000 Binary files a/res/drawable-xhdpi/tv_error.png and /dev/null differ diff --git a/res/drawable-xhdpi/tv_usb_antenna.png b/res/drawable-xhdpi/tv_usb_antenna.png deleted file mode 100644 index ff6c5cc1..00000000 Binary files a/res/drawable-xhdpi/tv_usb_antenna.png and /dev/null differ diff --git a/res/drawable/play_controls_time_indicator.xml b/res/drawable/play_controls_time_indicator.xml index 16acb107..85fee1fb 100644 --- a/res/drawable/play_controls_time_indicator.xml +++ b/res/drawable/play_controls_time_indicator.xml @@ -17,7 +17,5 @@ - diff --git a/res/drawable/playback_progress_bar.xml b/res/drawable/playback_progress_bar.xml new file mode 100644 index 00000000..2a70ec82 --- /dev/null +++ b/res/drawable/playback_progress_bar.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/drawable/priority_settings_action_item_selected.xml b/res/drawable/priority_settings_action_item_selected.xml index a1ab18ae..3e017313 100644 --- a/res/drawable/priority_settings_action_item_selected.xml +++ b/res/drawable/priority_settings_action_item_selected.xml @@ -15,12 +15,8 @@ ~ limitations under the License. --> - - - - - - - - + + + + diff --git a/res/drawable/setup_item_background.xml b/res/drawable/setup_item_background.xml deleted file mode 100644 index fb3899aa..00000000 --- a/res/drawable/setup_item_background.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - diff --git a/res/layout/activity_dvr_playback.xml b/res/layout/activity_dvr_playback.xml index 204001c7..02641d84 100644 --- a/res/layout/activity_dvr_playback.xml +++ b/res/layout/activity_dvr_playback.xml @@ -34,7 +34,11 @@ + + diff --git a/res/layout/activity_tv.xml b/res/layout/activity_tv.xml index f766ae00..4e8afbb7 100644 --- a/res/layout/activity_tv.xml +++ b/res/layout/activity_tv.xml @@ -28,28 +28,13 @@ android:layout_height="match_parent" android:layout_gravity="start|center_vertical" /> + + - - - - \ No newline at end of file diff --git a/res/layout/dvr_play.xml b/res/layout/dvr_play.xml deleted file mode 100644 index 4df13686..00000000 --- a/res/layout/dvr_play.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/res/layout/dvr_recording_card_view.xml b/res/layout/dvr_recording_card_view.xml index d3808a31..53a7cf3d 100644 --- a/res/layout/dvr_recording_card_view.xml +++ b/res/layout/dvr_recording_card_view.xml @@ -16,12 +16,12 @@ - + android:layout_height="@dimen/dvr_library_card_folded_title_height"> + + + + + + + + android:layout_height="match_parent" + android:focusable="true" + android:descendantFocusability="afterDescendants"> + android:clickable="true"> + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/layout/input_banner.xml b/res/layout/input_banner.xml index cd8770c8..f4035fcb 100644 --- a/res/layout/input_banner.xml +++ b/res/layout/input_banner.xml @@ -34,6 +34,7 @@ + + + + + + + + + + + + + + + diff --git a/res/layout/menu_card_action.xml b/res/layout/menu_card_action.xml index f17c5b6a..e8114b52 100644 --- a/res/layout/menu_card_action.xml +++ b/res/layout/menu_card_action.xml @@ -19,50 +19,43 @@ xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="@dimen/action_card_width" android:layout_height="@dimen/action_card_height" - android:orientation="vertical" android:focusable="true" android:background="@drawable/action_card_background"> - + android:layout_alignParentTop="true" + android:layout_marginStart="12dp" + android:layout_marginEnd="12dp" + android:layout_marginTop="6dp" + android:fontFamily="@string/condensed_font" + android:maxLines="1" + android:textColor="@color/action_card_label_color" + android:textSize="@dimen/action_card_label_font_size" /> - - - - - + + android:scaleType="fitCenter" /> diff --git a/res/layout/menu_card_app_link.xml b/res/layout/menu_card_app_link.xml index 918cb788..a7e3bd4a 100644 --- a/res/layout/menu_card_app_link.xml +++ b/res/layout/menu_card_app_link.xml @@ -33,13 +33,6 @@ android:layout_gravity="top" android:scaleType="centerCrop"/> - - - - diff --git a/res/layout/play_controls_contents.xml b/res/layout/play_controls_contents.xml index ac61a2d4..9afc5f3d 100644 --- a/res/layout/play_controls_contents.xml +++ b/res/layout/play_controls_contents.xml @@ -16,6 +16,7 @@ --> @@ -28,153 +29,118 @@ android:layout_alignStart="@+id/body" android:layout_marginBottom="@dimen/play_controls_time_bottom_margin" android:gravity="center" - android:singleLine="true" + android:maxLines="1" android:textColor="@color/play_controls_time_text_color" android:textSize="@dimen/play_controls_time_text_size" android:fontFamily="@string/font" /> - - - - - - - - - - - + + - - - - - - - - - - - - - - + android:layout_alignParentStart="true" + android:layout_below="@id/progress" + android:layout_marginStart="@dimen/play_controls_program_time_margin_start" + android:layout_marginTop="@dimen/play_controls_program_time_margin_top" + android:maxLines="1" + android:textColor="@color/play_controls_rec_time_text_color" + android:textSize="@dimen/play_controls_rec_time_text_size" + android:fontFamily="@string/font" /> + - - - + + + + + + + - diff --git a/res/layout/select_input_item.xml b/res/layout/select_input_item.xml index 1ff6df29..12fedca6 100644 --- a/res/layout/select_input_item.xml +++ b/res/layout/select_input_item.xml @@ -28,6 +28,7 @@ + + + + + + \ No newline at end of file diff --git a/res/transition/dvr_details_shared_element_enter_transition.xml b/res/transition/dvr_details_shared_element_enter_transition.xml new file mode 100644 index 00000000..d3fc0651 --- /dev/null +++ b/res/transition/dvr_details_shared_element_enter_transition.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/transition/dvr_details_shared_element_return_transition.xml b/res/transition/dvr_details_shared_element_return_transition.xml new file mode 100644 index 00000000..ceabca46 --- /dev/null +++ b/res/transition/dvr_details_shared_element_return_transition.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/values-af/strings.xml b/res/values-af/strings.xml index 7778e751..ebcafa99 100644 --- a/res/values-af/strings.xml +++ b/res/values-af/strings.xml @@ -20,9 +20,8 @@ "mono" "stereo" "Speelkontroles" - "Onlangse kanale" + "Kanale" "TV-opsies" - "PIP-opsies" "Speelkontroles onbeskikbaar vir hierdie kanaal" "Speel of laat wag" "Vinnig vorentoe" @@ -35,33 +34,15 @@ "Onderskrifte" "Vertoonmodus" "PIP" - "Aan" - "Af" "Multi-oudio" "Kry meer kanale" "Instellings" - "Bron" - "Ruil" - "Aan" - "Af" - "Klank" - "Hoof" - "PIP-venster" - "Uitleg" - "Regs onder" - "Regs bo" - "Links bo" - "Links onder" - "Langs mekaar" - "Grootte" - "Groot" - "Klein" - "Invoerbron" "TV (antenna/kabel)" "Geen programinligting nie" "Geen inligting nie" "Geblokkeerde kanaal" - "Onbekende taal" + "Onbekende taal" + "Onderskrifte %1$d" "Onderskrifte" "Af" "Pasmaak formatering" @@ -135,6 +116,10 @@ "Die PIN is verkeerd. Probeer weer." "Probeer weer. PIN stem nie ooreen nie" + "Voer jou poskode in." + "Regstreekse kanale-program sal die poskode gebruik om \'n volledige programgids vir die TV-kanale te voorsien." + "Voer jou poskode in" + "Ongeldige poskode" "Instellings" "Pasmaak kanaallys" "Kies kanale vir jou programgids" @@ -143,6 +128,7 @@ "Ouerkontroles" "Oopbronlisensies" "Oopbronlisensies" + "Stuur terugvoer" "Weergawe" "Druk Regs en voer jou PIN in om hierdie kanaal te kyk" "Druk Regs en voer jou PIN in om hierdie program te kyk" @@ -181,8 +167,6 @@ "Druk KIES"" om na die TV-kieslys te gaan." "Geen TV-invoer gevind nie" "Kan nie die TV-invoer vind nie" - "PIP word nie gesteun nie" - "Daar is geen beskikbare invoer om met PIP te wys nie" "Instemmertipe is nie geskik nie. Begin asseblief die Live TV-program vir instemmertipe-TV-invoer." "Kon nie instel nie" "Geen program is gevind om hierdie handeling te behartig nie." @@ -259,8 +243,6 @@ "Stoor" "Eenmalige opnames het die hoogste prioriteit" "Kanselleer" - "Kanselleer" - "Vergeet" "Stop" "Bekyk opnameskedule" "Net hierdie een program" @@ -270,25 +252,29 @@ "Neem eerder hierdie een op" "Kanselleer hierdie opname" "Kyk nou" + "Vee opnames uit …" "Kan opgeneem word" "Opname geskeduleer" "Opneemkonflik" "Neem tans op" "Kon nie opneem nie" "Lees tans programme om opneemskedules te skep" - "Leesprogramme" - - + "Leesprogramme" + "Bekyk onlangse opnames" + "Die opname van %1$s is nie volledig nie." + "Die opnames van %1$s en %2$s is onvolledig." + "Die opnames van %1$s, %2$s en %3$s is onvolledig." + "Die opname van %1$s het nie klaargemaak nie weens onvoldoende bergingspasie." + "Die opnames van %1$s en %2$s het nie klaargemaak nie weens onvoldoende bergingspasie." + "Die opnames van %1$s, %2$s en %3$s het nie klaargemaak nie weens onvoldoende bergingspasie." "DVR het meer berging nodig" "Jy sal programme met DVR kan opneem. DVR werk egter nie op die oomblik nie omdat daar nie genoeg berging op jou toestel beskikbaar is nie. Koppel asseblief \'n eksterne hardeskryf wat %1$s GB of groter is en volg die stappe om dit as toestelberging te formateer." + "Te min bergingspasie" + "Hierdie program sal nie opgeneem word nie omdat daar te min bergingspasie is. Probeer \'n paar bestaande opnames uitvee." "Berging ontbreek" - "Van die berging wat DVR gebruik, ontbreek. Koppel asseblief die eksterne skyf wat jy vroeër gebruik het om DVR te heraktiveer. Andersins kan jy kies om die berging te vergeet as dit nie meer beskikbaar is nie." - "Vergeet berging?" - "Al jou opgeneemde inhoud en skedules sal verloor word." "Stop opname?" "Die opgeneemde inhoud sal gestoor word." - - + "Die opname van %1$s sal gestaak word omdat dit met hierdie program bots. Die inhoud wat opgeneem is, sal gestoor word." "Opname is geskeduleer, maar daar is botsings" "Opname het begin, maar daar is konflikte" "%1$s sal opgeneem word." @@ -306,14 +292,27 @@ "Dieselfde program is reeds geskeduleer om om %1$s opgeneem te word." "Reeds opgeneem" "Hierdie program is reeds opgeneem. Dit is in die DVR-biblioteek beskikbaar." - - - - - - - - + "Reeksopname geskeduleer" + + %1$d opnames is geskeduleer vir %2$s. + %1$d opname is geskeduleer vir %2$s. + + + %1$d opnames is geskeduleer vir %2$s. %3$d van hulle sal weens oorvleueling nie opgeneem word nie. + %1$d opname is geskeduleer vir %2$s. Dit sal weens oorvleueling nie opgeneem word nie. + + + %1$d opnames is geskeduleer vir %2$s. %3$d episodes van hierdie reeks en ander reekse sal weens oorvleueling nie opgeneem word nie. + %1$d opname is geskeduleer vir %2$s. %3$d episodes van hierdie reeks en ander reekse sal weens oorvleueling nie opgeneem word nie. + + + %1$d opnames is geskeduleer vir %2$s. 1 episode van \'n ander reeks sal weens oorvleueling nie opgeneem word nie. + %1$d opname is geskeduleer vir %2$s. 1 episode van \'n ander reeks sal weens oorvleueling nie opgeneem word nie. + + + %1$d opnames is geskeduleer vir %2$s. %3$d episodes van \'n ander reeks sal weens oorvleueling nie opgeneem word nie. + %1$d opname is geskeduleer vir %2$s. %3$d episodes van \'n ander reeks sal weens oorvleueling nie opgeneem word nie. + "Kon nie opgeneemde program vind nie." "Verwante opnames" "(Geen programbeskrywing nie)" @@ -336,6 +335,7 @@ "Stop om reeks op te neem?" "Opgeneemde episodes sal in die DVR-biblioteek beskikbaar bly." "Stop" + "Geen episodes is tans op die lug nie." "Geen episodes is beskikbaar nie.\nHulle sal opgeneem word sodra hulle beskikbaar is." (%1$d minute) diff --git a/res/values-am/strings.xml b/res/values-am/strings.xml index bd63879b..19950ce0 100644 --- a/res/values-am/strings.xml +++ b/res/values-am/strings.xml @@ -20,9 +20,8 @@ "ሞኖ" "ስቲሪዮ" "የማጫወቻ መቆጣጠሪያዎች" - "የቅርብ ጊዜ ሰርጦች" + "ሰርጦች" "የቴሌቪዥን አማራጮች" - "የፒአይፒ አማራጮች" "ለዚህ ሰርጥ የማጫወቻ መቆጣጠሪያዎች አይገኝም" "አጫውት ወይም ለአፍታ አቁም" "በፍጥነት አሳልፍ" @@ -35,33 +34,15 @@ "የተዘጉ የስዕል መግለጫዎች" "የማሳያ ሁኔታ" "ፒአይፒ" - "በርቷል" - "ጠፍቷል" "ባለብዙ ተሰሚ" "ተጨማሪ ሰርጦችን ያግኙ" "ቅንብሮች" - "ምንጭ" - "ማገላበጥ" - "በርቷል" - "ጠፍቷል" - "ድምፅ" - "ዋና" - "የፒአይፒ መስኮት" - "አቀማመጥ" - "ከታች በስተቀኝ" - "ከላይ በስተቀኝ" - "ከላይ በስተግራ" - "ከታች በስተግራ" - "ጎን ለጎን" - "መጠን" - "ትልቅ" - "ትንሽ" - "የግብዓት ምንጭ" "ቴሌቪዥን (አንቴና/ገመድ)" "ምንም የፕሮግራም መረጃ የለም" "ምንም መረጃ የለም" "የታገደ ሰርጥ" - "ያልታወቀ ቋንቋ" + "ያልታወቀ ቋንቋ" + "ዝግ መግለጫ ጽሑፎች %1$d" "የተዘጉ መግለጫ ጽሑፎች" "ጠፍቷል" "ቅርጸትን አብጅ" @@ -135,6 +116,10 @@ "ይህ ፒን የተሳሳተ ነበር። እንደገና ይሞክሩ።" "እንደገና ይሞክሩ፣ ፒኑ አይዛመድም" + "ዚፕ ኮድዎን ያስገቡ።" + "የLive TV መተግበሪያ ለቴሌቪዥን ሰርጦቹ ሙሉ የፕሮግራም ዝርዝር ለማቅረብ ዚፕ ኮዱን ይጠቀማል።" + "ዚፕ ኮድዎን ያስገቡ" + "ልክ ያልኾነ ዚፕ ኮድ" "ቅንብሮች" "የሰርጥ ዝርዝር አብጅ" "ለፕሮግራሙ መመሪያዎ ሰርጦችን ይምረጡ" @@ -143,6 +128,7 @@ "የወላጅ መቆጣጠሪያዎች" "የክፍት ምንጭ ፍቃዶች" "የክፍት ምንጭ ፍቃዶች" + "ግብረ ምላሽ ይላኩ" "ስሪት" "ይህን ሰርጥ ለመመልከት ቀኝን ይጫኑ እና የእርስዎን ፒን ያስገቡ" "ይህን ፕሮግራም ለመመልከት ቀኝን ይጫኑ እና የእርስዎን ፒን ያስገቡ" @@ -181,8 +167,6 @@ "የቴሌቪዥን ምናሌውን ለመድረስ ""ምረጥን ይጫኑ""።" "ምንም የቴሌቪዥን ግብዓት አልተገኘም" "የቴሌቪዥን ግብዓቱን ማግኘት አልተቻለም" - "ፒአይፒ አይደገፍም" - "ከፒአይፒው ጋር አብሮ ሊታይ የሚችል ግቤት አይገኝም" "የቃኚ አይነት ተገቢ አይደለም። እባክዎ የቃኚ አይነት ቴሌቪዥን ግብዓት ለማግኘት የቀጥተኛ ሰርጦች መተግበሪያውን ያስጀምሩት።" "መቃኘት አልተሳካም" "ይህን እርምጃ የሚያከናውን ምንም መተግበሪያ አልተገኘም።" @@ -259,8 +243,6 @@ "አስቀምጥ" "የአንድ-ጊዜ ቀረጻዎች ከፍተኛ ቅድሚያ ተሰጭነት አላቸው" "ይቅር" - "ተወው" - "እርሳ" "አቁም" "የምዝገባ መርሐግብርን ይመልከቱ" "ይህ ነጠላ ፕሮግራም" @@ -270,25 +252,29 @@ "በምትኩ ይሄኛውን ቅረጽ" "ይህን ቀረጻ ተወው" "አሁን ይመልከቱ" + "ቀረጻዎችን ሰርዝ…" "ሊቀረጽ የሚችል" "ቀረጻ መርሐግብር ተይዞለታል" "የቀረጻ ግጭት" "መቅዳት" "መቅረጽ አልተሳካም" "የቀረጻ መርሐግብሮችን ለመፍጠር ፕሮግራሞችን በማንበብ ላይ" - "ፕሮግራሞችን በማንበብ ላይ" - - + "ፕሮግራሞችን በማንበብ ላይ" + "የቅርብ ጊዜ ቅጂዎችን አሳይ" + "የ%1$s ቅጂ አልተጠናቀቀም።" + "የ%1$s እና %2$s ቅጂዎች አልተጠናቀቁም።" + "የ%1$s%2$s እና %3$s ቅጂዎች አልተጠናቀቁም።" + "የ%1$s ቅጂ ባለው በቂ ያልሆነ ማከማቻ ምክንያት አልተጠናቀቀም።" + "የ%1$s እና %2$s ቅጂዎች ባለው በቂ ያልሆነ ማከማቻ ምክንያት አልተጠናቀቁም።" + "የ%1$s%2$s እና %3$s ቅጂዎች ባለው በቂ ያልሆነ ማከማቻ ምክንያት አልተጠናቀቁም።" "ዲቪአር ተጨማሪ ማከማቻ ያስፈልገዋል" "ፕሮግራሞችን በዲቪአር መቅረጽ ይችላሉ። ይሁንና አሁን ዲቪአር እንዲሰራ በመሣሪያዎ ላይ በቂ የማከማቻ ቦታ የለም። እባክዎ %1$s ጊባ ወይም ከዚያ በላይ የሆነ ውጫዊ አንጻፊ ይሰኩና እንደ የመሣሪያ ማከማቻ ቅርጸት ለመስራት ያሉትን ደረጃዎች ይከተሉ።" + "በቂ ማከማቻ የለም" + "በቂ ማከማቻ ስለሌለ ይህ ፕሮግራም አይቀረጽም። አሁን ያሉ አንዳንድ ቀረጻዎችን ለመሰረዝ ይሞክሩ።" "የሚጎድል ማከማቻ" - "በDVR ጥቅም ላይ የዋለ አንዳንድ ማከማቻ ይጎድላል። DVRን ዳግም ለማንቃት ከዚህ በፊት የሚጠቀሙበትን ውጫዊ አንጻፊ እባክዎ ያገናኙ። በአማራጭነት፣ ከእንግዲህ የማይገኝ ከሆነ ማከማቻውን ለመርሳት መምረጥ ይችላሉ።" - "ማከማቻ ይረሳ?" - "ሁሉም የእርስዎ የተቀዳ ይዘት እና መርሐግብሮች ይጠፋሉ።" "መቅረጽ ይቁም?" "የተቀረጸው ይዘት ይቀመጣል።" - - + "የ%1$s ቀረጻ ከዚህ ፕሮግራም ጋር ስለሚጋጭ ይቆማል። የተቀረጸው ይዘት ይቀመጣል።" "ምዝገባ መርሐግብር ተይዞለታል፣ ነገር ግን ግጭቶች አሉ" "ቀረጻ ተጀምሯል፣ ነገር ግን ግጭቶች አሉት" "%1$s ይቀረጻል።" @@ -306,14 +292,27 @@ "ተመሳሳዩ ፕሮግራም አስቀድሞ በ%1$s ላይ እንዲቀረጽ መርሐግብር ተይዞለታል።" "አስቀድሞ ተቀርጿል" "ይህ ፕሮግራም አስቀድሞ ተቀርጿል። በዲቪአር ቤተ-መጽሐፍት ውስጥ ይገኛል።" - - - - - - - - + "የተከታታዮች ቀረጻ መርሐግብር ተይዞለታል" + + %1$d ቀረጻዎች ለ%2$s የጊዜ መርሐግብር ተይዞላቸዋል። + %1$d ቀረጻዎች ለ%2$s የጊዜ መርሐግብር ተይዞላቸዋል። + + + %1$d ቀረጻዎች ለ%2$s የጊዜ መርሐግብር ተይዞላቸዋል። ከእነሱ ውስጥ %3$d በግጭቶች ምክንያት አይቀረጹም። + %1$d ቀረጻዎች ለ%2$s የጊዜ መርሐግብር ተይዞላቸዋል። ከእነሱ ውስጥ %3$d በግጭቶች ምክንያት አይቀረጹም። + + + %1$d ቀረጻዎች ለ%2$s የጊዜ መርሐግብር ተይዞላቸዋል። %3$d የዚህ ተከታታይ እና የሌሎች ተከታታዮች ትርዒት ክፍሎች በግጭቶች ምክንያት አይቀረጹም። + %1$d ቀረጻዎች ለ%2$s የጊዜ መርሐግብር ተይዞላቸዋል። %3$d የዚህ ተከታታይ እና የሌሎች ተከታታዮች ትርዒት ክፍሎች በግጭቶች ምክንያት አይቀረጹም። + + + %1$d ቀረጻዎች ለ%2$s የጊዜ መርሐግብር ተይዞላቸዋል። 1 የሌሎች ተከታታዮች የትርዒት ክፍል በግጭት ምክንያት አይቀረጽም። + %1$d ቀረጻዎች ለ%2$s የጊዜ መርሐግብር ተይዞላቸዋል። 1 የሌሎች ተከታታዮች የትርዒት ክፍል በግጭት ምክንያት አይቀረጽም። + + + %1$d ቀረጻዎች ለ%2$s የጊዜ መርሐግብር ተይዞላቸዋል። %3$d የሌላ ተከታታይ የትዕይንት ክፍሎች በግጭቶች ምክንያት አይቀረጹም። + %1$d ቀረጻዎች ለ%2$s የጊዜ መርሐግብር ተይዞላቸዋል። %3$d የሌላ ተከታታይ የትዕይንት ክፍሎች በግጭቶች ምክንያት አይቀረጹም። + "የተቀረጸ ፕሮግራም አልተገኘም።" "ተዛማጅ ቀረጻዎች" "(ምንም የፕሮግራም መግለጫ የለም)" @@ -336,6 +335,7 @@ "የተከታታይ ቀረጻ ይቆም?" "የተቀረጹ ክፍሎች በዲቪአር ቤተ-መጽሐፍቱ ላይ የሚገኙ እንደሆኑ ይቆያሉ።" "አቁም" + "ምንም ተከታታይ የትዕይንት ክፍሎች የሉም።" "ምንም ክፍሎች አይገኙም።\nልክ የሚገኙ ሲሆኑ ይቀረጻሉ።" (%1$d ደቂቃዎች) diff --git a/res/values-ar/strings.xml b/res/values-ar/strings.xml index f91139b9..2b380242 100644 --- a/res/values-ar/strings.xml +++ b/res/values-ar/strings.xml @@ -20,9 +20,8 @@ "أحادية" "ستيريو" "عناصر التحكم في التشغيل" - "أحدث القنوات" + "القنوات" "خيارات التلفزيون" - "‏خيارات PIP" "عناصر تحكم التشغيل غير متاحة لهذه القناة" "تشغيل الفيديو أو إيقافه مؤقتًا" "تقديم سريع" @@ -35,33 +34,15 @@ "التسميات التوضيحية المغلقة" "وضع العرض" "PIP" - "تشغيل" - "إيقاف" "إعدادات صوتية متعددة" "جلب قنوات أخرى" "الإعدادات" - "المصدر" - "تبديل" - "تشغيل" - "إيقاف" - "الصوت" - "الرئيسي" - "‏نافذة PIP" - "التنسيق" - "أسفل اليمين" - "أعلى اليمين" - "أعلى اليسار" - "أسفل اليسار" - "جنبًا إلى جنب" - "الحجم" - "كبيرة" - "صغيرة" - "مصدر الإدخال" "تلفزيون (هوائي/كابل)" "لا تتوفّر معلومات عن البرنامج" "لا توجد معلومات" "القناة المحظورة" - "لغة غير معروفة" + "لغة غير معروفة" + "‏مقاطع ترجمة وشرح %1$d" "التسميات التوضيحية المغلقة" "إيقاف" "تخصيص التنسيق" @@ -143,6 +124,10 @@ "رقم التعريف الشخصي هذا خاطئ. أعد المحاولة." "أعد المحاولة، رقم التعريف الشخصي غير مطابق" + "أدخل الرمز البريدي." + "‏سيستخدم تطبيق Live TV الرمز البريدي لتوفير دليل برامج كامل لقنوات التلفزيون." + "أدخل الرمز البريدي" + "الرمز البريدي غير صالح" "الإعدادات" "تخصيص قائمة قنوات" "اختر القنوات الخاصة بدليل البرامج." @@ -151,6 +136,7 @@ "أدوات الرقابة الأبوية" "تراخيص البرامج المفتوحة المصدر" "تراخيص البرامج المفتوحة المصدر" + "إرسال تعليقات" "الإصدار" "لمشاهدة هذه القناة، اضغط على اليمين وأدخل رقم التعريف الشخصي" "لمشاهدة هذا البرنامج، اضغط على اليمين وأدخل رقم التعريف الشخصي" @@ -197,8 +183,6 @@ "‏""اضغط على \"تحديد\""" للوصول إلى قائمة TV." "لم يتم العثور على إدخال تلفزيون" "لا يمكن العثور على إدخال تلفزيون" - "‏خدمة PIP ليست متوافقة" - "‏ليس هناك إدخال متاح يمكن عرضه باستخدام PIP" "نوع الموالف غير مناسب، لذلك يُرجى تشغيل تطبيق القنوات المباشرة لنوع الموالف إدخال التلفزيون." "أخفق التوليف" "لم يتم العثور على تطبيق يمكنه مباشرة هذا الإجراء." @@ -299,8 +283,6 @@ "حفظ" "تسجيلات المرة الواحدة لها الأولوية القصوى" "إلغاء" - "إلغاء" - "حذف" "إيقاف" "عرض جدول عمليات التسجيل الزمني" "هذا البرنامج وحده" @@ -310,25 +292,29 @@ "تسجيل هذا بدلاً من ذلك" "إلغاء هذا التسجيل" "المشاهدة الآن" + "حذف التسجيلات…" "قابل للتسجيل" "تمتّ جدولة التسجيل" "تعارض في التسجيل" "جارٍ التسجيل" "أخفق التسجيل" "جارٍ قراءة البرامج لإنشاء جداول زمنية للتسجيل" - "جارٍ قراءة البرامج" - - + "جارٍ قراءة البرامج" + "عرض التسجيلات الأخيرة" + "لم يكتمل تسجيل %1$s." + "لم يكتمل تسجيل كل من %1$s و%2$s." + "لم يكتمل تسجيل كل من %1$s و%2$s و%3$s." + "لم يكتمل تسجيل %1$s نظرًا لأن سعة التخزين غير كافية." + "لم يكتمل تسجيل كل من %1$s و%2$s نظرًا لأن سعة التخزين غير كافية." + "لم يكتمل تسجيل كل من %1$s و%2$s و%3$s نظرًا لأن سعة التخزين غير كافية." "يحتاج مسجِّل الفيديو الرقمي إلى المزيد من السعة التخزينية" "ستتمكن من تسجيل البرامج باستخدام مسجّل الفيديو الرقمي؛ ولكن ليست هناك سعة تخزينية كافية على جهازك الآن ليعمل مسجِّل الفيديو الرقمي. يُرجى توصيل محرك أقراص خارجي بسعة تخزين %1$sغيغابايت أو أكبر واتباع الخطوات لتهيئته كوحدة تخزين للجهاز." + "السعة التخزينية غير كافية" + "لن يتم تسجيل هذا البرنامج لأنه ليست هناك سعة تخزين كافية. جرِّب حذف بعض التسجيلات الحالية." "سعة التخزين المفقودة" - "‏بعض سعة التخزين المستخدمة في جهاز DVR مفقود. يُرجى توصيل محرك الأقراص الخارجي الذي سبق لك استخدامه لإعادة تمكين جهاز DVR. بدلاً من ذلك، يمكنك اختيار حذف سعة التخزين إذا لم تعد متاحة." - "هل تريد حذف سعة التخزين؟" - "سيتم فقد جميع المحتويات والجداول الزمنية المسجَّلة." "هل تريد إيقاف التسجيل؟" "سيتم حفظ المحتوى الذي تم تسجيله." - - + "سيتم إيقاف تسجيل %1$s لأنه يتعارض مع هذا البرنامج. وسيتم حفظ المحتوى الذي تم تسجيله." "تمت إضافة جدول زمني لإجراء التسجيل ولكنه يتعارض مع جداول زمنية أخرى" "تم بدء التسجيل ولكنه يتعارض مع جداول زمنية أخرى" "لن يتم تسجيل %1$s." @@ -350,14 +336,47 @@ "تم إعداد جدول زمني من قبل لتسجيل البرنامج نفسه في %1$s." "تم التسجيل من قبل" "‏تم تسجيل هذا البرنامج من قبل. وسيكون متاحًا في مكتبة DVR." - - - - - - - - + "تمت جدولة تسجيل المسلسل" + + تمت جدولة %1$d تسجيل لمسلسل %2$s. + تمت جدولة تسجيلين (%1$d) لمسلسل %2$s. + تمت جدولة %1$d تسجيلات لمسلسل %2$s. + تمت جدولة %1$d تسجيلاً لمسلسل %2$s. + تمت جدولة %1$d تسجيل لمسلسل %2$s. + تمت جدولة تسجيل واحد (%1$d) لمسلسل %2$s. + + + تمت جدولة %1$d تسجيل لمسلسل %2$s. ولن يتم تسجيل %3$d من الحلقات بسبب تعارض المواعيد. + تمت جدولة تسجيلين (%1$d) لمسلسل %2$s. ولن يتم تسجيل %3$d من الحلقات بسبب تعارض المواعيد. + تمت جدولة %1$d تسجيلات لمسلسل %2$s. ولن يتم تسجيل %3$d من الحلقات بسبب تعارض المواعيد. + تمت جدولة %1$d تسجيلاً لمسلسل %2$s. ولن يتم تسجيل %3$d من الحلقات بسبب تعارض المواعيد. + تمت جدولة %1$d تسجيل لمسلسل %2$s. ولن يتم تسجيل %3$d من الحلقات بسبب تعارض المواعيد. + تمت جدولة تسجيل واحد (%1$d) لمسلسل %2$s. ولن يتم إجراء هذا التسجيل بسبب تعارض المواعيد. + + + تمت جدولة %1$d تسجيل لمسلسل %2$s. ولن يتم تسجيل %3$d من حلقات هذا المسلسل والمسلسل الآخر بسبب تعارض المواعيد. + تمت جدولة تسجيلين (%1$d) لمسلسل %2$s. ولن يتم تسجيل %3$d من حلقات هذا المسلسل والمسلسل الآخر بسبب تعارض المواعيد. + تمت جدولة %1$d تسجيلات لمسلسل %2$s. ولن يتم تسجيل %3$d من حلقات هذا المسلسل والمسلسل الآخر بسبب تعارض المواعيد. + تمت جدولة %1$d تسجيلاً لمسلسل %2$s. ولن يتم تسجيل %3$d من حلقات هذا المسلسل والمسلسل الآخر بسبب تعارض المواعيد. + تمت جدولة %1$d تسجيل لمسلسل %2$s. ولن يتم تسجيل %3$d من حلقات هذا المسلسل والمسلسل الآخر بسبب تعارض المواعيد. + تمت جدولة تسجيل واحد (%1$d) لمسلسل %2$s. ولن يتم تسجيل %3$d من حلقات هذا المسلسل والمسلسل الآخر بسبب تعارض المواعيد. + + + تمت جدولة %1$d تسجيل لمسلسل %2$s. ولن يتم تسجيل حلقة واحدة من المسلسل الآخر بسبب تعارض المواعيد. + تمت جدولة تسجيلين (%1$d) لمسلسل %2$s. ولن يتم تسجيل حلقة واحدة من المسلسل الآخر بسبب تعارض المواعيد. + تمت جدولة %1$d تسجيلات لمسلسل %2$s. ولن يتم تسجيل حلقة واحدة من المسلسل الآخر بسبب تعارض المواعيد. + تمت جدولة %1$d تسجيلاً لمسلسل %2$s. ولن يتم تسجيل حلقة واحدة من المسلسل الآخر بسبب تعارض المواعيد. + تمت جدولة %1$d تسجيل لمسلسل %2$s. ولن يتم تسجيل حلقة واحدة من المسلسل الآخر بسبب تعارض المواعيد. + تمت جدولة تسجيل واحد (%1$d) لمسلسل %2$s. ولن يتم تسجيل حلقة واحدة من المسلسل الآخر بسبب تعارض المواعيد. + + + تمت جدولة %1$d تسجيل لمسلسل %2$s. ولن يتم تسجيل %3$d من حلقات المسلسل الآخر بسبب تعارض المواعيد. + تمت جدولة تسجيلين (%1$d) لمسلسل %2$s. ولن يتم تسجيل %3$d من حلقات المسلسل الآخر بسبب تعارض المواعيد. + تمت جدولة %1$d تسجيلات لمسلسل %2$s. ولن يتم تسجيل %3$d من حلقات المسلسل الآخر بسبب تعارض المواعيد. + تمت جدولة %1$d تسجيلاً لمسلسل %2$s. ولن يتم تسجيل %3$d من حلقات المسلسل الآخر بسبب تعارض المواعيد. + تمت جدولة %1$d تسجيل لمسلسل %2$s. ولن يتم تسجيل %3$d من حلقات المسلسل الآخر بسبب تعارض المواعيد. + تمت جدولة تسجيل واحد (%1$d) لمسلسل %2$s. ولن يتم تسجيل %3$d من حلقات المسلسل الآخر بسبب تعارض المواعيد. + "لم يتم العثور على البرنامج المُسّجل" "تسجيلات ذات صلة" "(لا يتوفر وصف للبرنامج)" @@ -388,6 +407,7 @@ "هل تريد إيقاف تسجيل السلسلة؟" "‏ستظل الحلقات المسجّلة متاحة في مكتبة DVR." "إيقاف" + "ليست هناك حلقات يتم بثها على الهواء حاليًا." "لا تتوفر أي حلقات.\nسيتم تسجيلها بعد توفرها." ‏(%1$d دقيقة) diff --git a/res/values-az-rAZ/strings.xml b/res/values-az-rAZ/strings.xml index 6ccb6d24..7fef1469 100644 --- a/res/values-az-rAZ/strings.xml +++ b/res/values-az-rAZ/strings.xml @@ -20,9 +20,8 @@ "mono" "stereo" "Oyun kontrolu" - "Son kanallar" + "Kanallar" "TV seçənəkləri" - "PIP seçənəklər" "Oxutma idarə elementləri bu kanal üçün əlçatan deyil" "Oxudun və ya durdurun" "Sürətlə irəli" @@ -35,33 +34,15 @@ "Qapalı başlıqlar" "Ekran rejimi" "PIP" - "Aktiv" - "Qeyri-aktiv" "Multi-audio" "Daha çox kanal əldə edin" "Ayarlar" - "Mənbə" - "Dəyişdirin" - "Aktiv" - "Qeyri-aktiv" - "Səs" - "Əsas" - "PIP pəncərəsi" - "Etiket" - "Aşağı sağ" - "Yuxarı sağ" - "Yuxarı sol" - "Aşağı sol" - "Yan-yana" - "Ölçü" - "Böyük" - "Kiçik" - "Mənbəni daxil edin" "TV (antena/kabel)" "No proqram informasiya" "Məlumat yoxdur" "bloklanmış kanallar" - "Naməlum dil" + "Naməlum dil" + "Subtitr %1$d" "Qapalı başlıqlar" "Deaktiv" "Formatı fərdiləşdirin" @@ -137,6 +118,10 @@ "Həmin PİN kod səhv idi. Yenidən cəhd edin." "Yenidən cəhd edin, PİN uyğun deyil" + "Poçt İndeksi daxil edin." + "Canlı Kanal tətbiqi TV kanallarını tam proqram təlimatı ilə təmin etmək üçün Poçt İndeksi istifadə edəcək." + "Poçt İndeksi daxil edin" + "Yanlış Poçt İndeksi" "Ayarlar" "Kanal siyahısını fərdiləşdirin" "Proqram təlimatınız üçün kanal seçin" @@ -145,6 +130,7 @@ "Valideyn nəzarəti" "Açıq mənbə lisenziyaları" "Açıq mənbə lisenziyaları" + "Cavab rəyi göndərin" "Versiya" "Bu kanalı izləmək üçün Sağa basın və PİN kodunuzu daxil edin" "Bu proqramı izləmək üçün Sağa basın və PİN kodunuzu daxil edin" @@ -183,8 +169,6 @@ "TV menyusuna giriş üçün ""SEÇİN basın""." "TV daxiletmə tapılmadı" "TV daxiletmə tapılmır" - "PIP dəstəklənmir" - "PIP ilə göstərilə biləcək əlçatan daxiletmə yoxdur" "Sazlama növü uyğun deyil. TV daxiletmə sazlama növü üçün Canlı Kanallar tətbiqini işə salın." "Sazlama alınmadı" "Bu əməliyyatı idarə etmək üçün heç bir tətbiq tapılmadı." @@ -261,8 +245,6 @@ "Yadda saxlayın" "Bir dəfəlik qeydiyyatlar yüksək prioritetlidir" "Ləğv edin" - "Ləğv edin" - "Unudun" "Dayandırın" "Qeydiyyat cədvəlinə baxın" "Tək bu proqram" @@ -272,25 +254,29 @@ "Əvəzinə bunu qeydə alın" "Bu qeydə almanı ləğv edin" "İndi baxın" + "Qeydə almaları silin..." "Qeydə alınabilən" "Qeydiyyat planlaşdırılıb" "Qeydiyyat münaqişəsi" "Qeydə alınır" "Qeydə alma uğursuz oldu" "Qeyd etmə cədvəli yaratmaq üçün proqramlar oxunur" - "Oxuma proqramları" - - + "Oxuma proqramları" + "Son yazılara baxın" + "%1$s qeydə alınması tamamlanmadı." + "%1$s%2$s qeydə alınması tamamlanmadı." + "%1$s, %2$s%3$s qeydə alınması tamamlanmadı." + "Yetərsiz yaddaş səbəbi ilə %1$s qeydə alınması tamamlanmadı." + "Yetərsiz yaddaş səbəbi ilə %1$s%2$s qeydə alınması tamamlanmadı." + "Yetərsiz yaddaş səbəbi ilə %1$s, %2$s%3$s qeydə alınması tamamlanmadı." "DVR üçün əlavə yaddaş tələb olunur" "DVR ilə proqram qeydə ala biləcəksiniz. Hazırda DVR-ın işləməsi üçün cihazda kifayət qədər yaddaş yoxdur. %1$sGB və ya daha böyük həcmli xarici yaddaşı qoşun və cihaz yaddaşı olaraq format etmək üçün mərhələlərə riayət edin." + "Kifayət qədər yer yoxdur" + "Kifayət qədər boş yer olmadığı üçün bu proqram qeydə alınmayacaq. Bəzi mövcud qeydə almaları silməyə çalışın." "Yaddaş catışmır" - "DVR tərəfindən istifadə olunan yaddaşın bir hissəsi əlçatan deyil. DVR\'ı yenidən aktiv etməmişdən əvvəl istifadə etdiyiniz xarici diskə qoşulun. Bundan başqa, artıq əlçatan deyilsə, yaddaşı unutmağı seçə bilərsiniz." - "Yaddaş ehtiyyatını unutmusunuz?" - "Qeydə alınmış bütün məzmun və cədvəlləriniz itəcək." "Qeydetmə dayandırılsın?" "Qeydə alınan məzmun yadda saxlanacaq." - - + "Bu proqram ilə ziddiyyəti olduğu üçün %1$s proqramının qeydə alınması dayandırılacaq. Qeydə alınmış məzmun yadda saxlanacaq." "Qeydiyyat vaxtı təyin edilib lakin ziddiyətlər var" "Qeydə alma başladı, lakin ziddiyətlər var." "%1$s qeydə alınacaq." @@ -308,14 +294,27 @@ "Eyni proqramın qeydə alınması üçün artıq %1$s radələrində vaxt təyin edilib." "Artıq qeydə alınıb" "Bu proqram artıq qeydə alınıb. O, DVR kitabxanasında əlçatandır." - - - - - - - - + "Seriyaların qeydiyyatı planlaşdırıldı" + + %1$d qeydə alma %2$s üçün təyin edildi. + %1$d qeydə alma %2$s üçün təyin edildi. + + + %1$d qeydə alma %2$s üçün təyin edildi. Onlardan %3$d ədədi ziddiyyət səbəbilə qəydə alınmayacaq. + %1$d qeydə alma %2$s üçün təyin edildi. Bu, ziddiyyət səbəbilə qəydə alınmayacaq. + + + %1$d qeydə alma %2$s üçün təyin edildi. Bu seriyaların %3$d epizodu və digər seriyalar ziddiyyət səbəbilə qeydə alınmayacaq. + %1$d qeydə alma %2$s üçün təyin edildi. Bu seriyaların %3$d epizodu və digər seriyalar ziddiyyət səbəbilə qeydə alınmayacaq. + + + %1$d qeydə alma %2$s üçün təyin edildi. Digər seriyaların 1 epizodu ziddiyyət səbəbilə qeydə alınmayacaq. + %1$d qeydə alma %2$s üçün təyin edildi. Digər seriyaların 1 epizodu ziddiyyət səbəbilə qeydə alınmayacaq. + + + %1$d qeydə alma %2$s üçün təyin edildi. Digər seriyaların %3$d epizodu ziddiyyət səbəbilə qeydə alınmayacaq. + %1$d qeydə alma %2$s üçün təyin edildi. Digər seriyaların %3$d epizodu ziddiyyət səbəbilə qeydə alınmayacaq. + "Qeyd edilmiş proqram tapılmadı." "Əlaqədar qeydetmələr" "(Proqramın təsviri yoxdur)" @@ -338,6 +337,7 @@ "Ardıcıl qeydə alma dayandırılsın?" "Qeydə alınmış epizodlar DVR kitabxanasında əlçatan olacaq." "Dayandırın" + "Heç bir epizod indi canlı yayımda deyil." "Heç bir epizod əlçatan deyil.\nOnlar, əlçatan olduqda qeydə alınacaq." (%1$d dəqiqə) diff --git a/res/values-bg/strings.xml b/res/values-bg/strings.xml index ab202fe2..01364be3 100644 --- a/res/values-bg/strings.xml +++ b/res/values-bg/strings.xml @@ -20,9 +20,8 @@ "моно" "стерео" "Контроли за пускане" - "Скорошни канали" + "Канали" "Опции за TV" - "Опции за PIP" "За този канал няма налични контроли за възпроизвеждане" "Пускане или поставяне на пауза" "Превъртане напред" @@ -35,33 +34,15 @@ "Надписи" "Показв.: Режим" "PIP" - "Вкл." - "Изкл." "Много записи" "Още канали" "Настройки" - "Източник" - "Размяна" - "Вкл." - "Изкл." - "Звук" - "Основен" - "Прозорец на PIP" - "Оформление" - "Долу вдясно" - "Горе вдясно" - "Горе вляво" - "Долу вляво" - "Редом" - "Размер" - "Голям" - "Малък" - "Вход" "Телевизор (антена/кабел)" "Няма информация за програмите" "Няма информация" "Блокиран канал" - "Неизвестен език" + "Неизвестен език" + "Надписи: %1$d" "Надписи" "Изключване" "Форматиране: Персон." @@ -135,6 +116,10 @@ "Този ПИН код бе грешен. Опитайте отново." "Опитайте отново, ПИН кодът не е идентичен" + "Въведете пощенския си код." + "Приложението Телевизия онлайн ще използва пощенския код, за да предоставя пълния програмен справочник за телевизионните канали." + "Въведете пощенския си код" + "Пощенският код е невалиден" "Настройки" "Персон. на списъка с канали" "Изберете канали за програмния си справочник" @@ -143,6 +128,7 @@ "Родителски контроли" "Лицензи за отворен код" "Лицензи за отворен код" + "Изпращане на отзиви" "Версия" "За да гледате този канал, натиснете стрелката за надясно и въведете ПИН кода си" "За да гледате тази програма, натиснете стрелката за надясно и въведете ПИН кода си" @@ -181,8 +167,6 @@ "Натиснете „ИЗБИРАНЕ“"" за достъп до менюто на телевизора." "Няма намерен вход на телевизора" "Входът на телевизора не може да бъде намерен" - "Функцията „Картина в картината“ не се поддържа" - "Няма наличен вход, който да може да се показва с PIP" "Типът тунер не е подходящ. Моля, стартирайте приложението Live TV за телевизионен вход от типа „тунер“." "Настройването не бе успешно" "Не бе намерено приложение за извършване на това действие." @@ -259,8 +243,6 @@ "Запазване" "Най-висок приоритет имат еднократните записи" "Отказ" - "Отказ" - "Забравяне" "Спиране" "Вижте графика за записване" "Само тази програма" @@ -270,25 +252,29 @@ "Записване на тази програма" "Анулиране на този запис" "Гледайте сега" + "Изтриване на записи…" "С възможност за запис" "Записът е насрочен" "Конфликт със записа" "Записва се" "Записването не бе успешно" "Програмите се четат с цел създаване на графици за записване" - "Програмите се четат" - - + "Програмите се четат" + "Преглед на скорошните записи" + "Записът на „%1$s“ е непълен." + "Записите на „%1$s“ и „%2$s“ са непълни." + "Записите на „%1$s“, „%2$s“ и „%3$s“ са непълни." + "Записването на „%1$s“ не завърши поради недостатъчно място в хранилището." + "Записването на „%1$s“ и „%2$s“ не завърши поради недостатъчно място в хранилището." + "Записването на „%1$s“, „%2$s“ и „%3$s“ не завърши поради недостатъчно място в хранилището." "Дигиталният видеорекордер се нуждае от още място за съхранение" "Ще сте в състояние да записвате програми посредством дигиталния видеорекордер. В момента обаче той не може да работи, тъй като няма достатъчно място в хранилището на устройството ви. Моля, свържете външен диск с размер от поне %1$s ГБ и изпълнете стъпките, за да го форматирате като хранилище на устройството." + "Няма достатъчно място в хранилището" + "Тази програма няма да бъде записана, защото няма достатъчно място в хранилището. Пробвайте да изтриете някои съществуващи записи." "Хранилището липсва" - "Част от използваното от цифровия видеорекордер хранилище липсва. Моля, свържете по-рано ползвания от вас външен диск, за да активирате отново видеорекордера. Друга възможност е да изберете хранилището да се забрави, ако вече не е налично." - "Да се забрави ли хранилището?" - "Цялото ви записано съдържание и графици ще бъдат изгубени." "Да се спре ли записването?" "Записаното съдържание ще бъде запазено." - - + "Записването на „%1$s“ ще бъде спряно, защото е в конфликт с тази програма. Записаното съдържание ще бъде запазено." "Записът е насрочен, но има конфликти" "Записването започна, но има конфликти" "„%1$s“ ще се запише." @@ -306,14 +292,27 @@ "Същата програма вече е насрочена за записване в %1$s." "Вече записахте" "Тази програма вече е записана. Тя е налична в библиотеката на устройството за дигитален видеозапис." - - - - - - - - + "Записването на поредицата е насрочено" + + За „%2$s“ са насрочени %1$d записа. + За „%2$s“ е насрочен %1$d запис. + + + За „%2$s“ са насрочени %1$d записа. %3$d епизода няма да бъдат записани поради конфликти. + За „%2$s“ е насрочен %1$d запис. Епизодът няма да бъде записан поради конфликти. + + + За „%2$s“ са насрочени %1$d записа. %3$d епизода от тази и от друга поредица няма да бъдат записани поради конфликти. + За „%2$s“ е насрочен %1$d запис. %3$d епизода от тази и от друга поредица няма да бъдат записани поради конфликти. + + + За „%2$s“ са насрочени %1$d записа. 1 епизод от друга поредица няма да бъде записан поради конфликти. + За „%2$s“ е насрочен %1$d запис. 1 епизод от друга поредица няма да бъде записан поради конфликти. + + + За „%2$s“ са насрочени %1$d записа. %3$d епизода от друга поредица няма да бъдат записани поради конфликти. + За „%2$s“ е насрочен %1$d запис. %3$d епизода от друга поредица няма да бъдат записани поради конфликти. + "Записаната програма не е намерена." "Сродни записи" "(Няма описание на програмата)" @@ -336,6 +335,7 @@ "Да се спре ли записването на поредицата?" "Записаните епизоди ще останат налице в библиотеката на устройството за дигитален видеозапис." "Спиране" + "В момента не се излъчва нито един епизод." "Няма налични епизоди.\nТе ще бъдат записани, когато са налице." (%1$d минути) diff --git a/res/values-bn-rBD/strings.xml b/res/values-bn-rBD/strings.xml index 52938ab2..d9dcf01e 100644 --- a/res/values-bn-rBD/strings.xml +++ b/res/values-bn-rBD/strings.xml @@ -20,9 +20,8 @@ "মোনো" "স্টিরিও" "খেলার নিয়ন্ত্রণগুলি" - "সাম্প্রতিক চ্যানেলগুলি" + "চ্যানেলগুলি" "টিভি বিকল্পগুলি" - "PIP বিকল্পগুলি" "এই চ্যানেলটির জন্য প্লে নিয়ন্ত্রণগুলি অনুপলব্ধ" "প্লে করুন বা বিরাম দিন" "দ্রুত ফরওয়ার্ড" @@ -35,33 +34,15 @@ "সাবটাইটেলগুলি" "প্রদর্শন মোড" "PIP" - "চালু" - "বন্ধ" "একাধিক-অডিও" "আরো চ্যানেল পান" "সেটিংস" - "উৎস" - "সোয়াইপ করুন" - "চালু" - "বন্ধ" - "আওয়াজ" - "প্রধান" - "PIP উইন্ডো" - "লেআউট" - "ডানদিকে নীচে" - "ডানদিকে শীর্ষে" - "বামদিকে শীর্ষে" - "বামদিকে নীচে" - "পাশাপাশি" - "আকার" - "বড়" - "ক্ষুদ্র" - "ইনপুট উৎস" "TV (অ্যান্টেনা/কেবল)" "কোনো প্রোগ্রাম তথ্য নেই" "কোনো তথ্য নেই" "অবরুদ্ধ চ্যানেল" - "অজানা ভাষা" + "অজানা ভাষা" + "সাবটাইটেল %1$d" "সাবটাইটেলগুলি" "বন্ধ করুন" "ফর্ম্যাটিং কাস্টমাইজ করুন" @@ -135,6 +116,10 @@ "এই PINটি ভুল ছিল৷ আবার চেষ্টা করুন৷" "আবার চেষ্টা করুন, পিন মেলেনি" + "আপনার ডাক পিন কোড লিখুন৷" + "Live TV অ্যাপটি টিভি চ্যানেলগুলির প্রোগ্রামের সম্পূর্ণ নির্দেশিকা প্রদান করার জন্য ডাক পিন কোড ব্যবহার করবে৷" + "আপনার ডাক পিন কোড লিখুন" + "অবৈধ ডাক পিন কোড" "সেটিংস" "চ্যানেল তালিকা কাস্টমাইজ করুন" "আপনার প্রোগ্রাম গাইডের জন্য চ্যানেলগুলি নির্বাচন করুন" @@ -143,6 +128,7 @@ "অভিভাবকীয় নিয়ন্ত্রণগুলি" "মুক্ত উৎস লাইসেন্সগুলি" "মুক্ত উৎস লাইসেন্সগুলি" + "প্রতিক্রিয়া পাঠান" "সংস্করণ" "এই চ্যানেলটিকে দেখতে, ডানদিকে চাপুন এবং আপনার পিন লিখুন" "এই প্রোগ্রামটি দেখতে, ডানদিকে চাপুন এবং আপনার পিন লিখুন" @@ -181,8 +167,6 @@ "টিভি মেনু অ্যাক্সেস করতে ""নির্বাচন করুন টিপুন""৷" "কোনো TV ইনপুট খুঁজে পাওয়া যায়নি" "TV ইনপুট খুঁজে পাওয়া যায়নি" - "PIP সমর্থিত নয়" - "PIP এর সাথে দেখানো যেতে পারে এমন কোনো উপলব্ধ ইনপুট নেই" "টিউনারের প্রকারটি উপযুক্ত নয়; টিউনার প্রকারের টিভি ইনপুটের জন্য দয়া করে লইভ চ্যানেলগুলি অ্যাপ্লিকেশানটি লঞ্চ করুন৷" "টিউন করা ব্যর্থ হয়েছে" "এই ক্রিয়াটিকে চালনা করার জন্য কোনো অ্যাপ্লিকেশান পাওয়া যায়নি৷" @@ -228,10 +212,10 @@ "দেখুন" "শুরু থেকে প্লে করুন" - "পুনরায় প্লে করুন" + "আবার প্লে করুন" "মুছুন" "রেকডিংগুলি মুছুন" - "পুনরায় শুরু করুন" + "আবার শুরু করুন" "সিজন %1$s" "সময়সূচী দেখুন" "আরো পড়ুন" @@ -259,8 +243,6 @@ "সংরক্ষণ করুন" "একবার করা রেকর্ডিংগুলিতে সর্বোচ্চ অগ্রাধিকার রয়েছে" "বাতিল করুন" - "বাতিল করুন" - "মুছে দিন" "থামান" "রেকডিং এর সময়সূচী দেখুন" "শুধুমাত্র এই প্রোগ্রামটি" @@ -270,25 +252,29 @@ "বরং এটি রেকর্ড করুন" "এই রেকর্ডিং বাতিল করুন" "এখনই দেখুন" + "রেকডিংগুলি মুছুন..." "রেকর্ড করা যাবে" "রেকর্ডিংএর সময় নির্ধারিত হয়েছে" "রেকর্ডিং দ্বন্দ্ব" "রেকর্ড করা হচ্ছে" "রেকডিং করা গেল না" "রেকর্ডিংয়ের সময়সূচীগুলি তৈরি করতে প্রোগ্রামগুলি পড়া হচ্ছে" - "প্রোগ্রামগুলি পড়া হচ্ছে" - - + "প্রোগ্রামগুলি পড়া হচ্ছে" + "সাম্প্রতিক রেকর্ডিংগুলি দেখুন" + "%1$s এর রেকর্ডিং অসম্পূর্ণ।" + "%1$s এবং %2$s এর রেকর্ডিং সম্পূর্ণ।" + "%1$s, %2$s এবং %3$s এর রেকর্ডিং অসম্পূর্ণ।" + "অপর্যাপ্ত সঞ্চয়স্থান থাকার কারণে %1$s এর রেকর্ডিং সম্পূর্ণ হয়নি।" + "অপর্যাপ্ত সঞ্চয়স্থান থাকার কারণে %1$s এবং %2$s এর রেকর্ডিং সম্পূর্ণ হয়নি।" + "অপর্যাপ্ত সঞ্চয়স্থান থাকার কারণে %1$s, %2$s এবং %3$s এর রেকর্ডিং সম্পূর্ণ হয়নি।" "DVR এর আরো সঞ্চয়স্থান দরকার" "আপনি DVR এর মাধ্যমে প্রোগ্রাম রেকর্ড করতে পারবেন৷ তবে DVR কাজ করার জন্য আপনার ডিভাইসে এখন যথেষ্ঠ সঞ্চয়স্থান নেই৷ অনুগ্রহ করে %1$sGB বা তার থেকে বড় আকারের কোনো বাহ্যিক ডিভাইসের সাথে সংযোগ করুন এবং ডিভাইসের সঞ্চয়স্থান হিসাবে ফর্ম্যাট করতে পদক্ষেপগুলি অনুসরণ করুন৷" + "পর্যাপ্ত সঞ্চয়স্থান নেই" + "এখানে পর্যাপ্ত সঞ্চয়স্থান না থাকার কারণে এই প্রোগ্রামটিকে রেকর্ড করা যাবে না৷ বিদ্যমান কিছু রেকর্ডিং মোছার চেষ্টা করুন৷" "সঞ্চয়স্থান অনুপস্থিত" - "DVR দ্বারা ব্যবহৃত কিছু সঞ্চয়স্থান অনুপস্থিত৷ DVR পুনরায় সক্ষম করার আগে অনুগ্রহ করে আপনার আগে ব্যবহার করা বাহ্যিক ড্রাইভ সংযোগ করুন৷ অথবা, যদি সঞ্চয়স্থানটি আর উপলব্ধ না থাকে তবে আপনি সেটিকে মুছে দিতে পারবেন৷" - "সঞ্চয়স্থান মুছতে চান?" - "আপনার রেকর্ড করা সমস্ত সামগ্রী এবং সময়সূচী মুছে যাবে৷" "রেকর্ড করা থামাবেন?" "রেকর্ড করা সামগ্রী সংরক্ষণ করা হবে৷" - - + "এই প্রোগ্রামের সাথে রেকডিং দ্বন্দ্ব থাকায় %1$s এর রেকর্ডিং বন্ধ হবে। রেকর্ড করা সামগ্রী সংরক্ষিত হবে।" "রেকর্ডিংয়ের যে সময় নির্ধারিত করা হয়েছে তাতে অন্যদের সমসয়ের সাথে বিরোধ ঘটাতে পারে।" "রেকর্ডিং শুরু হয়েছে কিন্তু দ্বন্দ্বগুলি রয়েছে" "%1$s রেকর্ড করা হবে৷" @@ -306,14 +292,27 @@ "একই প্রোগ্রাম ইতিমধ্যেই %1$s এ রেকর্ড করার জন্য নির্ধারণ করা হয়েছে।" "ইতিমধ্যে রেকর্ড করা হয়েছে" "এই প্রোগ্রামটি ইতিমধ্যে রেকর্ড করা হয়েছে। এটি DVR লাইব্রেরিতে উপলব্ধ।" - - - - - - - - + "সিরিজ রেকর্ডিংয়ের সময় নির্ধারিত হয়েছে" + + %2$s এর জন্য %1$dটি রেকর্ডিংয়ের সময়সূচি নির্ধারণ করা হয়েছে। + %2$s এর জন্য %1$dটি রেকর্ডিংয়ের সময়সূচি নির্ধারণ করা হয়েছে। + + + %2$s এর জন্য %1$dটি রেকর্ডিংয়ের সময়সূচি নির্ধারণ করা হয়েছে। বিরোধগুলির কারণে এগুলির মধ্যে %3$dটি পর্ব রেকর্ড করা যাবে না৷ + %2$s এর জন্য %1$dটি রেকর্ডিংয়ের সময়সূচি নির্ধারণ করা হয়েছে। বিরোধগুলির কারণে এগুলির মধ্যে %3$dটি পর্ব রেকর্ড করা যাবে না৷ + + + %2$s এর জন্য %1$dটি রেকর্ডিংয়ের সময়সূচি নির্ধারণ করা হয়েছে। বিরোধগুলির কারণে এটি এবং অন্য সিরিজের %3$dটি পর্ব রেকর্ড করা যাবে না৷ + %2$s এর জন্য %1$dটি রেকর্ডিংয়ের সময়সূচি নির্ধারণ করা হয়েছে। বিরোধগুলির কারণে এটি এবং অন্য সিরিজের %3$dটি পর্ব রেকর্ড করা যাবে না৷ + + + %2$s এর জন্য %1$dটি রেকর্ডিংয়ের সময়সূচি নির্ধারণ করা হয়েছে৷ বিরোধগুলির কারণে অন্য সিরিজের ১টি পর্ব রেকর্ড করা যাবে না৷ + %2$s এর জন্য %1$dটি রেকর্ডিংয়ের সময়সূচি নির্ধারণ করা হয়েছে৷ বিরোধগুলির কারণে অন্য সিরিজের ১টি পর্ব রেকর্ড করা যাবে না৷ + + + %2$s এর জন্য %1$dটি রেকর্ডিংয়ের সময়সূচি নির্ধারণ করা হয়েছে। বিরোধগুলির কারণে অন্য সিরিজের %3$dটি পর্ব রেকর্ড করা যাবে না৷ + %2$s এর জন্য %1$dটি রেকর্ডিংয়ের সময়সূচি নির্ধারণ করা হয়েছে। বিরোধগুলির কারণে অন্য সিরিজের %3$dটি পর্ব রেকর্ড করা যাবে না৷ + "রেকর্ড করা প্রোগ্রাম খুঁজে পাওয়া যায়নি৷" "সম্পর্কিত রেকর্ডিং" "(প্রোগ্রামের কোনো বিবরণ নেই)" @@ -336,6 +335,7 @@ "সিরিজি রেকর্ড করা বন্ধ করতে চান?" "রেকর্ড করা পর্বগুলি DVR লাইব্রেরিতে উপলব্ধ থাকবে৷" "বন্ধ করুন" + "এখন কোনো পর্বের সম্প্রচার করা হচ্ছে না।" "কোনো পর্ব উপলব্ধ নেই।\nএকবার উপলব্ধ হলে সেগুলিকে রেকর্ড করা হবে।" (%1$d মিনিট) diff --git a/res/values-ca/strings.xml b/res/values-ca/strings.xml index 3ab61ffb..0c74c8ec 100644 --- a/res/values-ca/strings.xml +++ b/res/values-ca/strings.xml @@ -20,9 +20,8 @@ "mono" "estèreo" "Controls de reproducció" - "Canals recents" + "Canals" "Opcions de TV" - "Opcions de PIP" "Els controls de reproducció no estan disponibles en aquest canal" "Reprodueix o atura" "Avança ràpidament" @@ -35,33 +34,15 @@ "Subtítols" "Mode visualitz." "PIP" - "Activada" - "Desactivada" "Multiàudio" "Obtén més canals" "Configuració" - "Font" - "Canvia" - "Activada" - "Desactivada" - "So" - "Principal" - "Finestra PIP" - "Disseny" - "A baix a dreta" - "A dalt a dreta" - "A dalt a esq." - "A baix a esq." - "En paral·lel" - "Mida" - "Gran" - "Petita" - "Font d\'entrada" "Televisió (antena/cable)" "No hi ha informació del programa" "No hi ha informació" "Canal bloquejat" - "Idioma desconegut" + "Idioma desconegut" + "Subtítols en %1$d" "Subtítols ocults" "Desactivat" "Personalitza format" @@ -135,6 +116,10 @@ "El PIN era incorrecte. Torna-ho a provar." "Torna-ho a provar. El PIN no coincideix." + "Introdueix el codi postal." + "L\'aplicació TV en directe farà servir el codi postal per oferir una programació completa dels canals de televisió." + "Introdueix el codi postal" + "El codi postal no és vàlid" "Configuració" "Personalitza la llista de canals" "Tria canals per a la programació" @@ -143,6 +128,7 @@ "Controls parentals" "Llicències de programari lliure" "Llicències de programari lliure" + "Envia suggeriments" "Versió" "Per veure aquest canal, prem el botó dret i introdueix el PIN." "Per veure aquest programa, prem el botó dret i introdueix el PIN." @@ -181,8 +167,6 @@ "Prem SELECCIONA"" per accedir al menú de TV." "No s\'ha trobat cap entrada de televisió" "No es troba l\'entrada de televisió" - "No s\'admet la funció PIP" - "No hi ha cap entrada disponible que es pugui mostrar amb PIP" "El sintonitzador no és apte; inicia l\'aplicació Canals en directe per accedir a l\'entrada del sintonitzador." "No s\'ha pogut sintonitzar" "No s\'ha trobat cap aplicació per processar aquesta acció." @@ -257,8 +241,6 @@ "Desa" "Els enregistraments únics tenen la prioritat més alta" "Cancel·la" - "Cancel·la" - "No recordis" "Atura" "Mostra programa d\'enregistrament" "Només aquest programa" @@ -268,25 +250,29 @@ "Enregistra aquest programa" "Cancel·la aquest enregistrament" "Mira ara" + "Suprimeix els enregistraments…" "Enregistrable" "Enregistrament programat" "Conflicte d\'enregistrament" "S\'està enregistrant" "Error d\'enregistrament" "S\'estan llegint els programes per crear programacions d\'enregistrament" - "S\'estan llegint els programes" - - + "S\'estan llegint els programes" + "Mostra els enregistraments recents" + "L\'enregistrament del programa %1$s no s\'ha completat." + "Els enregistraments dels programes %1$s i %2$s no s\'han completat." + "Els enregistraments dels programes %1$s, %2$s i %3$s no s\'han completat." + "L\'enregistrament del programa %1$s no s\'ha completat perquè no hi ha prou espai d\'emmagatzematge." + "Els enregistraments dels programes %1$s i %2$s no s\'han completat perquè no hi ha prou espai d\'emmagatzematge." + "Els enregistraments dels programes %1$s, %2$s i %3$s no s\'han completat perquè no hi ha prou espai d\'emmagatzematge." "El DVR necessita més emmagatzematge" "Podràs· enregistrar· programes· amb· el· DVR.· No· obstant· això,· en· aquests· moments· no· tens· prou· emmagatzematge· al· dispositiu· perquè· el· DVR· pugui· funcionar.· Connecta· una· unitat· externa· que· tingui· com· a· mínim· %1$s GB· d\'espai· disponible· i· segueix· els· passos· per· formatar-la· com· a· unitat· d\'emmagatzematge· del· dispositiu." + "No hi ha prou espai d\'emmagatzematge" + "Aquest programa no s\'enregistrarà perquè no hi ha prou espai d\'emmagatzematge. Prova de suprimir algun dels enregistraments actuals." "Falta el dispositiu d\'emmagatzematge" - "Falta contingut emmagatzemat pel DVR. Connecta la unitat externa que utilitzaves abans per tornar a activar el DVR. Si el dispositiu d\'emmagatzematge ja no està disponible, pots optar perquè s\'oblidi." - "Vols que s\'oblidi el dispositiu d\'emmagatzematge?" - "Tot el contingut i totes les agendes que tinguis desades es perdran." "Vols aturar l\'enregistrament?" "El contingut enregistrat es desarà." - - + "L\'enregistrament de %1$s s\'aturarà perquè entra en conflicte amb aquest programa. El contingut enregistrat es desarà." "Hi ha un enregistrament programat, però té conflictes" "L\'enregistrament ha començat, però té conflictes" "%1$s s\'enregistrarà." @@ -304,14 +290,27 @@ "Ja s\'ha programat l\'enregistrament d\'aquest programa per a les %1$s." "Ja s\'ha enregistrat" "Aquest programa ja s\'ha enregistrat. El trobaràs a la biblioteca de DVR." - - - - - - - - + "S\'ha programat l\'enregistrament de la sèrie" + + S\'han programat %1$d enregistraments de la sèrie %2$s. + S\'ha programat %1$d enregistrament de la sèrie %2$s. + + + S\'han programat %1$d enregistraments de la sèrie %2$s. %3$d no s\'enregistraran a causa d\'un conflicte. + S\'ha programat %1$d enregistrament de la sèrie %2$s. No s\'enregistrarà a causa d\'un conflicte. + + + S\'han programat %1$d enregistraments de la sèrie %2$s. %3$d episodis d\'aquesta i d\'altres sèries no s\'enregistraran a causa d\'un conflicte. + S\'ha programat %1$d enregistrament de la sèrie %2$s. %3$d episodis d\'aquesta i d\'altres sèries no s\'enregistraran a causa d\'un conflicte. + + + S\'han programat %1$d enregistraments de la sèrie %2$s. No s\'enregistrarà un episodi d\'una altra sèrie a causa d\'un conflicte. + S\'ha programat %1$d enregistrament de la sèrie %2$s. No s\'enregistrarà un episodi d\'una altra sèrie a causa d\'un conflicte. + + + S\'han programat %1$d enregistraments de la sèrie %2$s. No s\'enregistraran %3$d episodis d\'altres sèries a causa d\'un conflicte. + S\'ha programat %1$d enregistrament de la sèrie %2$s. No s\'enregistraran %3$d episodis d\'altres sèries a causa d\'un conflicte. + "El programa enregistrat no s\'ha trobat." "Enregistraments relacionats" "(Cap descripció del programa)" @@ -334,6 +333,7 @@ "Vols aturar l\'enregistrament de la sèrie?" "Els episodis enregistrats continuaran estant disponibles a la biblioteca de DVR." "Atura" + "En aquest moment no s\'està emetent cap episodi en directe." "No hi ha episodis disponibles.\nS\'enregistraran quan estiguin disponibles." (%1$d minuts) diff --git a/res/values-cs/strings.xml b/res/values-cs/strings.xml index 6dac1751..c859c71b 100644 --- a/res/values-cs/strings.xml +++ b/res/values-cs/strings.xml @@ -20,9 +20,8 @@ "mono" "stereo" "Ovládání přehrávání" - "Poslední kanály" + "Kanály" "Možnosti TV" - "Možnosti PIP" "Ovládací prvky přehrávání pro tento kanál nejsou k dispozici" "Přehrát nebo pozastavit" "Přetočit vpřed" @@ -35,33 +34,15 @@ "Skryté titulky" "Režim zobrazení" "PIP" - "Zapnuto" - "Vypnuto" "Vícekanál. zvuk" "Další kanály" "Nastavení" - "Zdroj" - "Zaměnit" - "Zapnuto" - "Vypnuto" - "Zvuk" - "Hlavní" - "Okno PIP" - "Rozvržení" - "Vpravo dole" - "Vpravo nahoře" - "Vlevo nahoře" - "Vlevo dole" - "Vedle sebe" - "Rozměry" - "Velké" - "Malé" - "Zdroj vstupu" "TV (anténa/kabel)" "Žádné informace o programu" "Žádné informace" "Blokovaný kanál" - "Neznámý jazyk" + "Neznámý jazyk" + "Skryté titulky: %1$d" "Skryté titulky" "Vypnuto" "Nastavit formátování" @@ -139,6 +120,10 @@ "Kód PIN byl zadán chybně. Zkuste to znovu." "Kód PIN nesouhlasí. Zkuste to znovu." + "Zadejte PSČ." + "Aplikace Televize online vám na základě PSČ poskytne kompletního programového průvodce televizními kanály." + "Zadejte PSČ" + "Neplatné PSČ" "Nastavení" "Upravit seznam kanálů" "Vyberte kanály pro programového průvodce" @@ -147,6 +132,7 @@ "Rodičovská ochrana" "Licence open source" "Licence open source" + "Odeslat zpětnou vazbu" "Verze" "Chcete-li tento kanál sledovat, stiskněte šipku vpravo a zadejte kód PIN." "Chcete-li tento program sledovat, stiskněte šipku vpravo a zadejte kód PIN." @@ -189,8 +175,6 @@ "Chcete-li získat přístup k nabídce TV, ""stiskněte SELECT""." "Nebyl nalezen žádný televizní vstup." "Televizní vstup nebyl nalezen." - "Funkce PIP není podporována." - "Není k dispozici vstup, který lze zobrazovat pomocí PIP." "Typ tuneru není vhodný. Pro TV vstup typu tuneru spusťte aplikaci Televize online." "Ladění se nezdařilo." "Aplikace potřebná k provedení této akce nebyla nalezena." @@ -279,8 +263,6 @@ "Uložit" "Jednorázová nahrávání mají nejvyšší prioritu" "Zrušit" - "Zrušit" - "Zapomenout" "Zastavit" "Zobrazit plán nahrávání" "Pouze tento program" @@ -290,25 +272,29 @@ "Místo toho nahrát tento program" "Zrušit toto nahrávání" "Sledovat" + "Smazat nahraný obsah…" "Lze nahrát" "Nahrávání je naplánováno" "Konflikt nahrávání" "Nahrávání" "Nahrávání se nezdařilo" "Načítání programů za účelem vytvoření plánů nahrávání" - "Načítání programů" - - + "Načítání programů" + "Zobrazit poslední nahrávky" + "Program %1$s se nepodařilo nahrát celý." + "Programy %1$s%2$s se nepodařilo nahrát celé." + "Programy %1$s, %2$s%3$s se nepodařilo nahrát celé." + "Nahrávání programu %1$s nebylo dokončeno z důvodu nedostatku místa v úložišti." + "Nahrávání programů %1$s%2$s nebylo dokončeno z důvodu nedostatku místa v úložišti." + "Nahrávání programů %1$s, %2$s%3$s nebylo dokončeno z důvodu nedostatku místa v úložišti." "DVR potřebuje víc místa" "Programy bude možné nahrát pomocí DVR. Ve vašem zařízení však momentálně není dost místa, a DVR proto nebude fungovat. Zapojte externí úložiště o velikosti minimálně %1$s GB a podle pokynů jej naformátujte jako úložiště zařízení." + "Nedostatek místa" + "Tento program nebude nahrán z důvodu nedostatku místa. Zkuste smazat část nahraného obsahu." "Úložiště není dostupné" - "Část úložiště, které využívá DVR, není dostupná. Před opětovnou aktivací DVR připojte externí disk. Pokud úložiště již není k dispozici, můžete jej zapomenout." - "Zapomenout úložiště?" - "Veškerý nahraný obsah a plány nahrávání budou smazány." "Zastavit nahrávání?" "Nahraný obsah bude uložen." - - + "Nahrávání pořadu %1$s bude zastaveno, protože je v konfliktu s tímto programem. Nahraný obsah bude uložen." "Nahrávání je naplánováno, ale obsahuje konflikty" "Nahrávání bylo zahájeno, ale obsahuje konflikty" "Nahraje se program %1$s." @@ -328,14 +314,37 @@ "Nahrávání stejného programu již bylo naplánováno na %1$s." "Již nahráno" "Tento program již byl nahrán. Naleznete jej v knihovně DVR." - - - - - - - - + "Je naplánováno nahrávání pořadu" + + Byla naplánována %1$d nahrávání pořadu %2$s. + Bylo naplánováno %1$d nahrávání pořadu %2$s. + Bylo naplánováno %1$d nahrávání pořadu %2$s. + Bylo naplánováno %1$d nahrávání pořadu %2$s. + + + Byla naplánována %1$d nahrávání pořadu %2$s. Z důvodu konfliktů některá nahrávání nebudou provedena (celkem %3$d). + Bylo naplánováno %1$d nahrávání pořadu %2$s. Z důvodu konfliktů některá nahrávání nebudou provedena (celkem %3$d). + Bylo naplánováno %1$d nahrávání pořadu %2$s. Z důvodu konfliktů některá nahrávání nebudou provedena (celkem %3$d). + Bylo naplánováno %1$d nahrávání pořadu %2$s. Z důvodu konfliktů nahrávání nebude provedeno. + + + Byla naplánována %1$d nahrávání pořadu %2$s. Z důvodu konfliktů nebudou nahrány epizody tohoto pořadu a jiných pořadů (celkem %3$d). + Bylo naplánováno %1$d nahrávání pořadu %2$s. Z důvodu konfliktů nebudou nahrány epizody tohoto pořadu a jiných pořadů (celkem %3$d). + Bylo naplánováno %1$d nahrávání pořadu %2$s. Z důvodu konfliktů nebudou nahrány epizody tohoto pořadu a jiných pořadů (celkem %3$d). + Bylo naplánováno %1$d nahrávání pořadu %2$s. Z důvodu konfliktů nebudou nahrány epizody tohoto pořadu a jiných pořadů (celkem %3$d). + + + Byla naplánována %1$d nahrávání pořadu %2$s. Z důvodu konfliktů nebude nahrána 1 epizoda jiného pořadu. + Bylo naplánováno %1$d nahrávání pořadu %2$s. Z důvodu konfliktů nebude nahrána 1 epizoda jiného pořadu. + Bylo naplánováno %1$d nahrávání pořadu %2$s. Z důvodu konfliktů nebude nahrána 1 epizoda jiného pořadu. + Bylo naplánováno %1$d nahrávání pořadu %2$s. Z důvodu konfliktů nebude nahrána 1 epizoda jiného pořadu. + + + Byla naplánována %1$d nahrávání pořadu %2$s. Z důvodu konfliktů nebudou nahrány epizody jiných pořadů (celkem %3$d). + Bylo naplánováno %1$d nahrávání pořadu %2$s. Z důvodu konfliktů nebudou nahrány epizody jiných pořadů (celkem %3$d). + Bylo naplánováno %1$d nahrávání pořadu %2$s. Z důvodu konfliktů nebudou nahrány epizody jiných pořadů (celkem %3$d). + Bylo naplánováno %1$d nahrávání pořadu %2$s. Z důvodu konfliktů nebudou nahrány epizody jiných pořadů (celkem %3$d). + "Nahraný program nebyl nalezen." "Související nahrávky" "(Žádný popis programu)" @@ -362,6 +371,7 @@ "Zastavit nahrávání série?" "Nahrané epizody zůstanou dostupné v knihovně DVR." "Zastavit" + "Momentálně nejsou vysílány žádné epizody." "Nejsou k dispozici žádné epizody.\nEpizody budou nahrány, až budou k dispozici." (%1$d minuty) diff --git a/res/values-da/strings.xml b/res/values-da/strings.xml index b053e77d..9622b4c8 100644 --- a/res/values-da/strings.xml +++ b/res/values-da/strings.xml @@ -20,9 +20,8 @@ "mono" "stereo" "Afspilningsknapper" - "Seneste kanaler" + "Kanaler" "Tv-indstillinger" - "PIP-muligheder" "Afspilningsstyring er ikke tilgængeligt på denne kanal" "Afspil, eller sæt på pause" "Spol frem" @@ -35,33 +34,15 @@ "Undertekster" "Format" "PIP" - "Til" - "Fra" "Flere lydspor" "Få flere kanaler" "Indstillinger" - "Kilde" - "Skift" - "Til" - "Fra" - "Lyd" - "Primær" - "PIP-vindue" - "Layout" - "Nederst til højre" - "Øverst til højre" - "Øverst til venstre" - "Nederst til venstre" - "Side om side" - "Størrelse" - "Stor" - "Lille" - "Inputkilde" "Tv (antenne/kabel)" "Ingen programoplysninger" "Ingen oplysninger" "Blokeret kanal" - "Ukendt sprog" + "Ukendt sprog" + "Undertekster %1$d" "Undertekster" "Fra" "Tilpas formatering" @@ -135,6 +116,10 @@ "Pinkoden var forkert. Prøv igen." "Prøv igen. Pinkoden var forkert" + "Angiv dit postnummer." + "Appen Tv-kanaler bruger postnummeret til at levere en komplet tv-guide til tv-kanalerne." + "Angiv dit postnummer" + "Ugyldigt postnummer" "Indstillinger" "Tilpas kanallisten" "Vælg kanaler til din programoversigt" @@ -143,6 +128,7 @@ "Børnesikring" "Open source-licenser" "Open source-licenser" + "Send feedback" "Version" "Se denne kanal ved at trykke på højreknappen og angive din pinkode" "Se dette program ved at trykke på højreknappen og angive din pinkode" @@ -181,8 +167,6 @@ "Tryk på VÆLG"" for at få adgang til TV-menuen." "Der blev ikke fundet noget tv-input" "Tv-inputtet blev ikke fundet" - "PIP understøttes ikke" - "Der er intet tilgængeligt input, som kan vises med PIP" "Tunertypen er uegnet. Åbn appen Tv-kanaler for at få tv-input fra tunertypen." "Tuningen mislykkedes" "Der blev ikke fundet nogen app, der kan håndtere denne handling." @@ -259,8 +243,6 @@ "Gem" "Engangsoptagelser har højest prioritet" "Annuller" - "Annuller" - "Glem" "Stop" "Tidsplan for optagelse" "Dette ene program" @@ -270,25 +252,29 @@ "Optag dette program i stedet for" "Annuller denne optagelse" "Se nu" + "Slet optagelser…" "Kan optages" "Optagelse er planlagt" "Modstridende optagelser" "Optager" "Optagelsen mislykkedes" "Læser programmer for at oprette tidsplaner for optagelse" - "Læser programmer" - - + "Læser programmer" + "Se de seneste optagelser" + "Optagelsen af %1$s er ikke fuldført." + "Optagelserne af %1$s og %2$s er ikke fuldført." + "Optagelserne af %1$s, %2$s og %3$s er ikke fuldført." + "Optagelsen af %1$s blev ikke fuldført, da der ikke var nok lagerplads." + "Optagelserne af %1$s og %2$s blev ikke fuldført, da der ikke var nok lagerplads." + "Optagelserne af %1$s, %2$s og %3$s blev ikke fuldført, da der ikke var nok lagerplads." "DVR kræver mere lagerplads" "Du kan optage programmer med DVR. Der er dog ikke længere nok lagerplads på din enhed til at DVR kan fungere. Tilslut et eksternt drev på mindst %1$s GB, og følg vejledningen i, hvordan du formaterer det som internt lager." + "Der er ikke nok lagerplads" + "Dette program kan ikke optages, fordi der ikke er nok lagerplads. Prøv at slette nogle eksisterende optagelser." "Lager mangler" - "Noget af det lager, der bruges af DVR, mangler. Tilslut det eksterne drev, du brugte før, for at genaktivere DVR. Alternativt kan du vælge at glemme lageret, hvis det ikke længere er tilgængeligt." - "Vil du glemme lageret?" - "Du mister alt dit optagede indhold og dine tidsplaner." "Skal optagelsen stoppes?" "Det optagede indhold gemmes." - - + "Optagelsen af %1$s stoppes, fordi den falder sammen med dette program. Det indhold, der er optaget, vil blive gemt." "Optagelsen er planlagt, men der er konflikter" "Optagelsen er startet, men der er konflikter" "%1$s optages." @@ -306,14 +292,27 @@ "En optagelse af dette program er allerede planlagt kl. %1$s." "Det er allerede optaget" "Dette program er allerede optaget. Du kan finde det i DVR-samlingen." - - - - - - - - + "Optagelsen er planlagt" + + Der er planlagt %1$d optagelse af %2$s. + Der er planlagt %1$d optagelser af %2$s. + + + Der er planlagt %1$d optagelse af %2$s. %3$d optages ikke på grund af konflikter. + Der er planlagt %1$d optagelser af %2$s. %3$d af dem optages ikke på grund af konflikter. + + + Der er planlagt %1$d optagelse af %2$s. %3$d afsnit af denne og andre serier optages ikke på grund af konflikter. + Der er planlagt %1$d optagelser af %2$s. %3$d afsnit af denne og andre serier optages ikke på grund af konflikter. + + + Der er planlagt %1$d optagelse af %2$s. 1 afsnit af en anden serie optages ikke på grund af konflikter. + Der er planlagt %1$d optagelser af %2$s. 1 afsnit af en anden serie optages ikke på grund af konflikter. + + + Der er planlagt %1$d optagelse af %2$s. %3$d afsnit af andre serier optages ikke på grund af konflikter. + Der er planlagt %1$d optagelser af %2$s. %3$d afsnit af andre serier optages ikke på grund af konflikter. + "Det optagede program blev ikke fundet." "Relaterede optagelser" "(Ingen programbeskrivelse)" @@ -336,6 +335,7 @@ "Vil du stoppe optagelsen af serien?" "Optagede afsnit kan findes i DVR-samlingen." "Stop" + "Der optages ingen afsnit lige nu." "Der er ingen tilgængelige tv-serier.\nDe optages, så snart de er tilgængelige.." (%1$d minut) diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml index b4cc6c0f..7e72c63e 100644 --- a/res/values-de/strings.xml +++ b/res/values-de/strings.xml @@ -20,9 +20,8 @@ "Mono" "Stereo" "Wiedergabesteuerung" - "Letzte Kanäle" + "Kanäle" "TV-Optionen" - "PIP-Optionen" "Wiedergabesteuerung für diesen Kanal nicht verfügbar" "Abspielen oder pausieren" "Vorspulen" @@ -35,33 +34,15 @@ "Untertitel" "Anzeigemodus" "PIP" - "An" - "Aus" "Multi-Audio" "Mehr Kanäle erhalten" "Einstellungen" - "Quelle" - "Wechseln" - "An" - "Aus" - "Ton" - "Hauptoption" - "PIP-Fenster" - "Layout" - "Unten rechts" - "Oben rechts" - "Oben links" - "Unten links" - "Nebeneinander" - "Größe" - "Groß" - "Klein" - "Eingangsquelle" "TV (Antenne/Kabel)" "Keine Programminformationen" "Keine Informationen" "Blockierter Kanal" - "Unbekannte Sprache" + "Unbekannte Sprache" + "Untertitel für %1$d" "Untertitel" "Aus" "Untertitel anpassen" @@ -135,6 +116,10 @@ "Diese PIN war falsch. Versuchen Sie es erneut." "Die PIN stimmt nicht. Bitte versuchen Sie es erneut." + "Gib deine Postleitzahl ein." + "Die Live TV App verwendet die Postleitzahl, um dir die vollständige Programmübersicht der TV-Sender zur Verfügung zu stellen." + "Postleitzahl eingeben" + "Ungültige Postleitzahl" "Einstellungen" "Kanalliste anpassen" "Kanäle für Programmübersicht auswählen" @@ -143,6 +128,7 @@ "Jugendschutzeinstellungen" "Open-Source-Lizenzen" "Open-Source-Lizenzen" + "Feedback geben" "Version" "Um sich diesen Kanal anzusehen, drücken Sie rechts und geben Sie die PIN ein." "Um sich dieses Programm anzusehen, drücken Sie rechts und geben Sie die PIN ein." @@ -181,8 +167,6 @@ "Drücken Sie die Auswahltaste"", um auf das TV-Menü zuzugreifen." "Kein TV-Eingang gefunden" "TV-Eingang konnte nicht gefunden werden." - "PIP wird nicht unterstützt." - "Es ist kein Eingang für die PIP-Anzeige verfügbar." "Tunertyp ungeeignet, bitte Live TV App für TV-Eingang des Tunertyps starten" "Fehler beim Einstellen" "Für diese Aktion wurde keine App gefunden." @@ -259,8 +243,6 @@ "Speichern" "Einmalige Aufnahmen haben höchste Priorität" "Abbrechen" - "Abbrechen" - "Entfernen" "Beenden" "Aufnahmeplan ansehen" "Nur diese Folge" @@ -270,25 +252,29 @@ "Stattdessen diese Sendung aufnehmen" "Diesen Aufnahmeplan abbrechen" "Jetzt ansehen" + "Aufnahmen löschen…" "Kann aufgenommen werden" "Aufnahme geplant" "Konflikt bei der Aufnahme" "Aufnahme" "Aufnahme fehlgeschlagen" "Sendungen werden gelesen, um Aufnahmepläne zu erstellen" - "Sendungen werden gelesen" - - + "Sendungen werden gelesen" + "Letzte Aufnahmen ansehen" + "Die Aufnahme %1$s ist nicht abgeschlossen." + "Die Aufnahmen %1$s und %2$s sind nicht abgeschlossen." + "Die Aufnahmen %1$s, %2$s und %3$s sind nicht abgeschlossen." + "Die Aufnahme von %1$s konnte nicht abgeschlossen werden, weil nicht genügend Speicher vorhanden ist." + "Die Aufnahmen von %1$s und %2$s konnten nicht abgeschlossen werden, weil nicht genügend Speicher vorhanden ist." + "Die Aufnahmen von %1$s, %2$s und %3$s konnten nicht abgeschlossen werden, weil nicht genügend Speicher vorhanden ist." "DVR benötigt mehr Speicher" "Sie können mit DVR Sendungen aufnehmen, jedoch ist auf Ihrem Gerät momentan nicht ausreichend Speicherplatz vorhanden. Schließen Sie ein externes Laufwerk mit mindestens %1$s GB freiem Speicher an und folgen Sie der Anleitung zur Formatierung als Gerätespeicher." + "Nicht genug Speicherplatz" + "Diese Inhalte werden nicht aufgenommen, weil zu wenig Speicherplatz verfügbar ist. Du kannst Speicherplatz freigeben, indem du ein paar vorhandene Aufnahmen löschst." "Speicher nicht verfügbar" - "Ein Teil des DVR-Speichers ist nicht verfügbar. Um DVR neu zu aktivieren, stellen Sie eine Verbindung zu dem externen Gerät her, das Sie zuvor verwendet haben. Sie können den Speicher auch entfernen, wenn er nicht mehr verfügbar ist." - "Speicher entfernen?" - "Alle aufgenommenen Inhalte und Aufnahmepläne gehen verloren." "Aufnahme beenden?" "Die aufgenommenen Inhalte werden gespeichert." - - + "Die Aufzeichnung von %1$s wird aufgrund eines Konflikts mit diesem Programm beendet. Der aufgezeichnete Inhalt wird gespeichert." "Aufnahme geplant, aber es liegen Konflikte vor" "Aufnahme wurde gestartet, aber es liegen Konflikte vor" "%1$s wird aufgenommen." @@ -306,14 +292,27 @@ "Diese Sendung wurde dem Aufnahmeplan schon hinzugefügt und wird um %1$s aufgenommen." "Schon aufgenommen" "Diese Sendung wurde schon aufgenommen. Sie ist in der DVR-Bibliothek verfügbar." - - - - - - - - + "Serienaufnahme geplant" + + %1$d Aufnahmen wurden für %2$s geplant. + %1$d Aufnahme wurde für %2$s geplant. + + + %1$d Aufnahmen wurden für %2$s geplant. %3$d davon werden wegen Konflikten nicht aufgezeichnet. + %1$d Aufnahme wurde für %2$s geplant. Sie wird wegen Konflikten nicht aufgezeichnet. + + + %1$d Aufnahmen wurden für %2$s geplant. %3$d Folgen dieser Serie und anderer Serien werden wegen Konflikten nicht aufgezeichnet. + %1$d Aufnahme wurde für %2$s geplant. %3$d Folge dieser oder einer anderen Serie wird wegen Konflikten nicht aufgezeichnet. + + + %1$d Aufnahmen wurden für %2$s geplant. 1 Folge einer anderen Serie wird wegen Konflikten nicht aufgezeichnet. + %1$d Aufnahme wurde für %2$s geplant. 1 Folge einer anderen Serie wird wegen Konflikten nicht aufgezeichnet. + + + %1$d Aufnahmen wurden für %2$s geplant. %3$d Folgen anderer Serien werden wegen Konflikten nicht aufgezeichnet. + %1$d Aufnahme wurde für %2$s geplant. %3$d Folge einer anderen Serie wird wegen Konflikten nicht aufgezeichnet. + "Aufgenommenes Programm wurde nicht gefunden." "Ähnliche Aufnahmen" "(Keine Programmbeschreibung)" @@ -336,6 +335,7 @@ "Sie möchten die Serienaufnahme beenden?" "Die aufgenommenen Folgen werden in der DVR-Bibliothek gespeichert." "Beenden" + "Momentan werden keine Folgen ausgestrahlt." "Keine Folgen verfügbar.\nSie werden aufgenommen, sobald sie verfügbar sind." (%1$d Minuten) diff --git a/res/values-el/strings.xml b/res/values-el/strings.xml index 9e8e042c..817d116a 100644 --- a/res/values-el/strings.xml +++ b/res/values-el/strings.xml @@ -20,9 +20,8 @@ "μονοφων." "στερεοφ." "Στοιχ. ελέγ. αναπαραγωής" - "Πρόσφατα κανάλια" + "Κανάλια" "Επιλογές TV" - "Επιλογές PIP" "Τα στοιχεία ελέγχου αναπαραγωγής δεν είναι διαθέσιμα γι\' αυτό το κανάλι" "Αναπαραγωγή ή παύση" "Γρήγορη προώθηση" @@ -35,33 +34,15 @@ "Υπότιτλοι" "Τρόπος εμφάν." "PIP" - "Ενεργό" - "Ανενεργό" "Πολλαπλός ήχος" "Περισσότ. κανάλια" "Ρυθμίσεις" - "Πηγή" - "Ανταλλαγή" - "Ενεργό" - "Ανενεργό" - "Ήχος" - "Κύριο" - "Παράθυρο PIP" - "Διάταξη" - "Κάτω δεξιά" - "Επάνω δεξιά" - "Επάνω αριστερά" - "Κάτω αριστερά" - "Δίπλα" - "Μέγεθος" - "Μεγάλο" - "Μικρό" - "Πηγή εισόδου" "Τηλεόραση (κεραία/καλωδιακή)" "Δεν υπάρχουν πληροφορίες προγράμματος" "Δεν υπάρχουν πληροφορίες" "Αποκλεισμένο κανάλι" - "Άγνωστη γλώσσα" + "Άγνωστη γλώσσα" + "Υπότιτλοι %1$d" "Υπότιτλοι" "Ανενεργό" "Προσ. μορφοποίησης" @@ -135,6 +116,10 @@ "Λάθος PIN. Δοκιμάστε ξανά." "Δοκιμάστε ξανά, δεν υπάρχει αντιστοιχία PIN" + "Εισαγωγή ταχυδρομικού κώδικα" + "Η εφαρμογή \"Ζωντανά κανάλια\" θα χρησιμοποιεί τον ταχυδρομικό κώδικα για να παρέχει έναν πλήρη οδηγό προγράμματος για τα τηλεοπτικά κανάλια." + "Εισαγωγή ταχυδρομικού κώδικα" + "Μη έγκυρος ταχυδρομ. κώδικας" "Ρυθμίσεις" "Προσαρμογή λίστας καναλιών" "Επιλέξτε κανάλια για τον οδηγό προγράμματος" @@ -143,6 +128,7 @@ "Γονικοί έλεγχοι" "Άδειες λογισμικού ανοικτού κώδικα" "Άδειες λογισμικού ανοικτού κώδικα" + "Αποστολή σχολίων" "Έκδοση" "Για να παρακολουθήσετε αυτό το κανάλι, πατήστε το δεξιά και εισαγάγετε το PIN σας" "Για να παρακολουθήσετε αυτό το πρόγραμμα, πατήστε δεξιά και εισαγάγετε το PIN σας" @@ -181,8 +167,6 @@ "Πατήστε το πλήκτρο SELECT"" για να μεταβείτε στο μενού της τηλεόρασης." "Δεν βρέθηκε είσοδος τηλεόρασης" "Δεν είναι δυνατή η εύρεση εισόδου τηλεόρασης" - "Το PIP δεν υποστηρίζεται" - "Δεν υπάρχει διαθέσιμη είσοδος για εμφάνιση με PIP" "Ακατάλληλος τύπος δέκτη. Εκκινήστε την εφαρμογή Κανάλια ζωντανά για είσοδο τύπου δέκτη τηλεόρασης." "Αποτυχία συντονισμού" "Δεν βρέθηκε εφαρμογή για τη διαχείριση αυτής της ενέργειας." @@ -259,8 +243,6 @@ "Αποθήκευση" "Οι εγγραφές μίας φοράς έχουν την υψηλότερη προτεραιότητα" "Ακύρωση" - "Ακύρωση" - "Διαγραφή" "Διακοπή" "Προβολή προγραμματισ. εγγραφών" "Αυτό το συγκεκριμένο επεισόδιο" @@ -270,25 +252,29 @@ "Αντί αυτού, εγγρ. αυτού του πρ." "Ακύρωση αυτής της εγγραφής" "Προβολή τώρα" + "Διαγραφή εγγραφών…" "Με δυνατότητα εγγραφής" "Η εγγραφή προγραμματίστηκε" "Διένεξη εγγραφής" "Εγγραφή" "Αποτυχία εγγραφής" "Γίνεται ανάγνωση προγραμμάτων για δημιουργία προγραμματισμών εγγραφής" - "Ανάγνωση προγραμμάτων" - - + "Ανάγνωση προγραμμάτων" + "Προβολή πρόσφατων εγγραφών" + "Η εγγραφή του προγράμματος %1$s δεν είναι ολοκληρωμένη." + "Οι εγγραφές των προγραμμάτων %1$s και %2$s δεν είναι ολοκληρωμένες." + "Οι εγγραφές των προγραμμάτων %1$s, %2$s και %3$s δεν είναι ολοκληρωμένες." + "Η εγγραφή του προγράμματος %1$s δεν ολοκληρώθηκε, λόγω ανεπαρκούς αποθηκευτικού χώρου." + "Οι εγγραφές των προγραμμάτων %1$s και %2$s δεν ολοκληρώθηκαν, λόγω ανεπαρκούς αποθηκευτικού χώρου." + "Οι εγγραφές των προγραμμάτων %1$s, %2$s και %3$s δεν ολοκληρώθηκαν, λόγω ανεπαρκούς αποθηκευτικού χώρου." "Το DVR χρειάζεται περισσότερο αποθηκευτικό χώρο" "Θα μπορείτε να εγγράψετε προγράμματα με το DVR. Ωστόσο, αυτήν τη στιγμή δεν υπάρχει αρκετός αποθηκευτικός χώρος στη συσκευή σας έτσι ώστε να λειτουργήσει το DVR. Συνδέστε έναν εξωτερικό δίσκο με χωρητικότητα %1$s GB ή μεγαλύτερη και ακολουθήστε τα βήματα για να τον μορφοποιήσετε και να τον ορίσετε ως αποθηκευτικό χώρο της συσκευής." + "Δεν υπάρχει αρκετός αποθηκευτικός χώρος" + "Αυτό το πρόγραμμα δεν θα εγγραφεί, επειδή δεν υπάρχει αρκετός αποθηκευτικός χώρος. Δοκιμάστε να διαγράψετε ορισμένες υπάρχουσες εγγραφές." "Ο αποθηκευτικός χώρος λείπει" - "Κάποιο τμήμα του αποθηκευτικού χώρου που χρησιμοποιείται από το DVR λείπει. Συνδέστε τον εξωτερικό δίσκο που χρησιμοποιήσατε στο παρελθόν για να ενεργοποιήσετε εκ νέου το DVR. Εναλλακτικά, μπορείτε να επιλέξετε να διαγράψετε τον αποθηκευτικό χώρο αν δεν είναι πλέον διαθέσιμος." - "Να διαγραφεί ο αποθηκευτικός χώρος;" - "Όλο το εγγεγραμμένο περιεχόμενο και τα χρονοδιαγράμματα θα χαθούν." "Να διακοπεί η εγγραφή;" "Θα γίνει αποθήκευση του περιεχομένου που έχει εγγραφεί." - - + "Η εγγραφή του προγράμματος %1$s θα τερματιστεί καθώς βρίσκεται σε διένεξη με αυτό το πρόγραμμα. Το περιεχόμενο που έχει εγγραφεί θα αποθηκευτεί." "Η εγγραφή προγραμματίστηκε, αλλά έχει διενέξεις" "Η εγγραφή ξεκίνησε αλλά υπάρχουν διενέξεις" "Θα γίνει εγγραφή του προγράμματος %1$s." @@ -306,14 +292,27 @@ "Το ίδιο πρόγραμμα έχει προγραμματιστεί για εγγραφή στις %1$s." "Έχει ήδη εγγραφεί" "Αυτό το πρόγραμμα έχει ήδη εγγραφεί. Είναι διαθέσιμο στη βιβλιοθήκη DVR." - - - - - - - - + "Η εγγραφή της σειράς προγραμματίστηκε" + + %1$d εγγραφές έχουν προγραμματιστεί για τη σειρά %2$s. + %1$d εγγραφή έχει προγραμματιστεί για τη σειρά %2$s. + + + %1$d εγγραφές έχουν προγραμματιστεί για τη σειρά %2$s. %3$d από αυτές δεν θα εγγραφούν εξαιτίας διενέξεων. + %1$d εγγραφή έχει προγραμματιστεί για τη σειρά %2$s. Δεν θα γίνει η εγγραφή εξαιτίας διενέξεων. + + + %1$d εγγραφές έχουν προγραμματιστεί για τη σειρά %2$s. %3$d επεισόδια αυτής της σειράς και άλλης σειράς δεν θα εγγραφούν εξαιτίας διενέξεων. + %1$d εγγραφή έχει προγραμματιστεί για τη σειρά %2$s. %3$d επεισόδια αυτής της σειράς και άλλης σειράς δεν θα εγγραφούν εξαιτίας διενέξεων. + + + %1$d εγγραφές έχουν προγραμματιστεί για τη σειρά %2$s. 1 επεισόδιο άλλης σειράς δεν θα εγγραφεί εξαιτίας διενέξεων. + %1$d εγγραφή έχει προγραμματιστεί για τη σειρά %2$s. 1 επεισόδιο άλλης σειράς δεν θα εγγραφεί εξαιτίας διενέξεων. + + + %1$d εγγραφές έχουν προγραμματιστεί για τη σειρά %2$s. %3$d επεισόδια άλλης σειράς δεν θα εγγραφούν εξαιτίας διενέξεων. + %1$d εγγραφή έχει προγραμματιστεί για τη σειρά %2$s. %3$d επεισόδια άλλης σειράς δεν θα εγγραφούν εξαιτίας διενέξεων. + "Το εγγεγραμμένο πρόγραμμα δεν βρέθηκε." "Σχετικές εγγραφές" "(Καμία περιγραφή προγράμματος)" @@ -336,6 +335,7 @@ "Διακοπή εγγραφής σειράς;" "Τα επεισόδια που έχουν εγγραφεί θα εξακολουθήσουν να είναι διαθέσιμα στη βιβλιοθήκη DVR." "Διακοπή" + "Δεν προβάλλονται επεισόδια ζωντανά αυτήν τη στιγμή." "Δεν υπάρχουν διαθέσιμα επεισόδια.\nΗ εγγραφή τους θα πραγματοποιηθεί όταν θα είναι διαθέσιμα." (%1$d λεπτά) diff --git a/res/values-en-rAU/strings.xml b/res/values-en-rAU/strings.xml index bda4e1ea..1b15a022 100644 --- a/res/values-en-rAU/strings.xml +++ b/res/values-en-rAU/strings.xml @@ -20,9 +20,8 @@ "mono" "stereo" "Play controls" - "Recent channels" + "Channels" "TV options" - "PIP options" "Play controls unavailable for this channel" "Play or pause" "Fast-forward" @@ -35,33 +34,15 @@ "Closed captions" "Display mode" "PIP" - "On" - "Off" "Multi-audio" "Get more channels" "Settings" - "Source" - "Swap" - "On" - "Off" - "Sound" - "Main" - "PIP window" - "Layout" - "Bottom right" - "Top right" - "Top left" - "Bottom left" - "Side by side" - "Size" - "Big" - "Small" - "Input source" "TV (aerial/cable)" "No programme information" "No information" "Blocked channel" - "Unknown language" + "Unknown language" + "Closed captions %1$d" "Closed captions" "Off" "Customise formatting" @@ -135,6 +116,10 @@ "That PIN was wrong. Try again." "Try again, PIN doesn\'t match" + "Enter your postcode." + "Live TV app will use the postcode to provide a complete programme guide for the TV channels." + "Enter your postcode" + "Invalid postcode" "Settings" "Customise channel list" "Choose channels for your programme guide" @@ -143,6 +128,7 @@ "Parental controls" "Open-source licences" "Open-source licences" + "Send feedback" "Version" "To watch this channel, press Right and enter your PIN" "To watch this program, press Right and enter your PIN" @@ -181,8 +167,6 @@ "Press SELECT"" to access the TV menu." "No TV input found" "Cannot find the TV input" - "PIP is not supported" - "There is no available input which can be shown with PIP" "Tuner type not suitable. Please launch Live TV app for tuner type TV input." "Tune failed" "No app was found to handle this action." @@ -259,8 +243,6 @@ "Save" "One-time recordings have the highest priority" "Cancel" - "Cancel" - "Forget" "Stop" "View recording schedule" "This single programme" @@ -270,25 +252,29 @@ "Record this one instead" "Cancel this recording" "Watch now" + "Delete recordings…" "Recordable" "Recording scheduled" "Recording conflict" "Recording" "Recording failed" "Reading programs to create recording schedules" - "Reading programmes" - - + "Reading programmes" + "View recent recordings" + "The recording of %1$s is incomplete." + "The recordings of %1$s and %2$s are incomplete." + "The recordings of %1$s, %2$s and %3$s are incomplete." + "The recording of %1$s didn\'t complete due to insufficient storage." + "The recordings of %1$s and %2$s didn\'t complete due to insufficient storage." + "The recordings of %1$s, %2$s and %3$s didn\'t complete due to insufficient storage." "DVR needs more storage" "You will be able to record programmes with DVR. At the moment there is not enough storage on your device for DVR to work. Please connect an external drive that is %1$sGB or larger and follow the steps to format it as device storage." + "Not enough storage" + "This programme will not be recorded because there is not enough storage. Try deleting some existing recordings." "Missing storage" - "Some of the storage used by DVR is missing. Please connect the external drive that you used before to re-enable DVR. Alternatively, you can choose to forget the storage if it\'s no longer available." - "Forget storage?" - "All your recorded content and schedules will be lost." "Stop recording?" "The recorded content will be saved." - - + "The recording of %1$s will be stopped because it conflicts with this programme. The recorded content will be saved." "Recording scheduled but has conflicts" "Recording has started but has conflicts" "%1$s will be recorded." @@ -306,14 +292,27 @@ "The same programme has already been scheduled to be recorded at %1$s." "Already recorded" "This programme has already been recorded. It’s available in the DVR library." - - - - - - - - + "Series recording scheduled" + + %1$d recordings have been scheduled for %2$s. + %1$d recording has been scheduled for %2$s. + + + %1$d recordings have been scheduled for %2$s. %3$d of them will not be recorded due to conflicts. + %1$d recording has been scheduled for %2$s. It will not be recorded due to conflicts. + + + %1$d recordings have been scheduled for %2$s. %3$d episodes of this series and other series will not be recorded due to conflicts. + %1$d recording has been scheduled for %2$s. %3$d episodes of this series and other series will not be recorded due to conflicts. + + + %1$d recordings have been scheduled for %2$s. 1 episode of other series will not be recorded due to conflicts. + %1$d recording has been scheduled for %2$s. 1 episode of other series will not be recorded due to conflicts. + + + %1$d recordings have been scheduled for %2$s. %3$d episodes of other series will not be recorded due to conflicts. + %1$d recording has been scheduled for %2$s. %3$d episodes of other series will not be recorded due to conflicts. + "Recorded programme not found." "Related recordings" "(No programme description)" @@ -336,6 +335,7 @@ "Stop series recording?" "Recorded episodes will remain available in the DVR library." "Stop" + "No episodes are on air now." "No episodes are available.\nThey will be recorded once they are available." (%1$d minutes) diff --git a/res/values-en-rGB/strings.xml b/res/values-en-rGB/strings.xml index bda4e1ea..1b15a022 100644 --- a/res/values-en-rGB/strings.xml +++ b/res/values-en-rGB/strings.xml @@ -20,9 +20,8 @@ "mono" "stereo" "Play controls" - "Recent channels" + "Channels" "TV options" - "PIP options" "Play controls unavailable for this channel" "Play or pause" "Fast-forward" @@ -35,33 +34,15 @@ "Closed captions" "Display mode" "PIP" - "On" - "Off" "Multi-audio" "Get more channels" "Settings" - "Source" - "Swap" - "On" - "Off" - "Sound" - "Main" - "PIP window" - "Layout" - "Bottom right" - "Top right" - "Top left" - "Bottom left" - "Side by side" - "Size" - "Big" - "Small" - "Input source" "TV (aerial/cable)" "No programme information" "No information" "Blocked channel" - "Unknown language" + "Unknown language" + "Closed captions %1$d" "Closed captions" "Off" "Customise formatting" @@ -135,6 +116,10 @@ "That PIN was wrong. Try again." "Try again, PIN doesn\'t match" + "Enter your postcode." + "Live TV app will use the postcode to provide a complete programme guide for the TV channels." + "Enter your postcode" + "Invalid postcode" "Settings" "Customise channel list" "Choose channels for your programme guide" @@ -143,6 +128,7 @@ "Parental controls" "Open-source licences" "Open-source licences" + "Send feedback" "Version" "To watch this channel, press Right and enter your PIN" "To watch this program, press Right and enter your PIN" @@ -181,8 +167,6 @@ "Press SELECT"" to access the TV menu." "No TV input found" "Cannot find the TV input" - "PIP is not supported" - "There is no available input which can be shown with PIP" "Tuner type not suitable. Please launch Live TV app for tuner type TV input." "Tune failed" "No app was found to handle this action." @@ -259,8 +243,6 @@ "Save" "One-time recordings have the highest priority" "Cancel" - "Cancel" - "Forget" "Stop" "View recording schedule" "This single programme" @@ -270,25 +252,29 @@ "Record this one instead" "Cancel this recording" "Watch now" + "Delete recordings…" "Recordable" "Recording scheduled" "Recording conflict" "Recording" "Recording failed" "Reading programs to create recording schedules" - "Reading programmes" - - + "Reading programmes" + "View recent recordings" + "The recording of %1$s is incomplete." + "The recordings of %1$s and %2$s are incomplete." + "The recordings of %1$s, %2$s and %3$s are incomplete." + "The recording of %1$s didn\'t complete due to insufficient storage." + "The recordings of %1$s and %2$s didn\'t complete due to insufficient storage." + "The recordings of %1$s, %2$s and %3$s didn\'t complete due to insufficient storage." "DVR needs more storage" "You will be able to record programmes with DVR. At the moment there is not enough storage on your device for DVR to work. Please connect an external drive that is %1$sGB or larger and follow the steps to format it as device storage." + "Not enough storage" + "This programme will not be recorded because there is not enough storage. Try deleting some existing recordings." "Missing storage" - "Some of the storage used by DVR is missing. Please connect the external drive that you used before to re-enable DVR. Alternatively, you can choose to forget the storage if it\'s no longer available." - "Forget storage?" - "All your recorded content and schedules will be lost." "Stop recording?" "The recorded content will be saved." - - + "The recording of %1$s will be stopped because it conflicts with this programme. The recorded content will be saved." "Recording scheduled but has conflicts" "Recording has started but has conflicts" "%1$s will be recorded." @@ -306,14 +292,27 @@ "The same programme has already been scheduled to be recorded at %1$s." "Already recorded" "This programme has already been recorded. It’s available in the DVR library." - - - - - - - - + "Series recording scheduled" + + %1$d recordings have been scheduled for %2$s. + %1$d recording has been scheduled for %2$s. + + + %1$d recordings have been scheduled for %2$s. %3$d of them will not be recorded due to conflicts. + %1$d recording has been scheduled for %2$s. It will not be recorded due to conflicts. + + + %1$d recordings have been scheduled for %2$s. %3$d episodes of this series and other series will not be recorded due to conflicts. + %1$d recording has been scheduled for %2$s. %3$d episodes of this series and other series will not be recorded due to conflicts. + + + %1$d recordings have been scheduled for %2$s. 1 episode of other series will not be recorded due to conflicts. + %1$d recording has been scheduled for %2$s. 1 episode of other series will not be recorded due to conflicts. + + + %1$d recordings have been scheduled for %2$s. %3$d episodes of other series will not be recorded due to conflicts. + %1$d recording has been scheduled for %2$s. %3$d episodes of other series will not be recorded due to conflicts. + "Recorded programme not found." "Related recordings" "(No programme description)" @@ -336,6 +335,7 @@ "Stop series recording?" "Recorded episodes will remain available in the DVR library." "Stop" + "No episodes are on air now." "No episodes are available.\nThey will be recorded once they are available." (%1$d minutes) diff --git a/res/values-en-rIN/strings.xml b/res/values-en-rIN/strings.xml index bda4e1ea..1b15a022 100644 --- a/res/values-en-rIN/strings.xml +++ b/res/values-en-rIN/strings.xml @@ -20,9 +20,8 @@ "mono" "stereo" "Play controls" - "Recent channels" + "Channels" "TV options" - "PIP options" "Play controls unavailable for this channel" "Play or pause" "Fast-forward" @@ -35,33 +34,15 @@ "Closed captions" "Display mode" "PIP" - "On" - "Off" "Multi-audio" "Get more channels" "Settings" - "Source" - "Swap" - "On" - "Off" - "Sound" - "Main" - "PIP window" - "Layout" - "Bottom right" - "Top right" - "Top left" - "Bottom left" - "Side by side" - "Size" - "Big" - "Small" - "Input source" "TV (aerial/cable)" "No programme information" "No information" "Blocked channel" - "Unknown language" + "Unknown language" + "Closed captions %1$d" "Closed captions" "Off" "Customise formatting" @@ -135,6 +116,10 @@ "That PIN was wrong. Try again." "Try again, PIN doesn\'t match" + "Enter your postcode." + "Live TV app will use the postcode to provide a complete programme guide for the TV channels." + "Enter your postcode" + "Invalid postcode" "Settings" "Customise channel list" "Choose channels for your programme guide" @@ -143,6 +128,7 @@ "Parental controls" "Open-source licences" "Open-source licences" + "Send feedback" "Version" "To watch this channel, press Right and enter your PIN" "To watch this program, press Right and enter your PIN" @@ -181,8 +167,6 @@ "Press SELECT"" to access the TV menu." "No TV input found" "Cannot find the TV input" - "PIP is not supported" - "There is no available input which can be shown with PIP" "Tuner type not suitable. Please launch Live TV app for tuner type TV input." "Tune failed" "No app was found to handle this action." @@ -259,8 +243,6 @@ "Save" "One-time recordings have the highest priority" "Cancel" - "Cancel" - "Forget" "Stop" "View recording schedule" "This single programme" @@ -270,25 +252,29 @@ "Record this one instead" "Cancel this recording" "Watch now" + "Delete recordings…" "Recordable" "Recording scheduled" "Recording conflict" "Recording" "Recording failed" "Reading programs to create recording schedules" - "Reading programmes" - - + "Reading programmes" + "View recent recordings" + "The recording of %1$s is incomplete." + "The recordings of %1$s and %2$s are incomplete." + "The recordings of %1$s, %2$s and %3$s are incomplete." + "The recording of %1$s didn\'t complete due to insufficient storage." + "The recordings of %1$s and %2$s didn\'t complete due to insufficient storage." + "The recordings of %1$s, %2$s and %3$s didn\'t complete due to insufficient storage." "DVR needs more storage" "You will be able to record programmes with DVR. At the moment there is not enough storage on your device for DVR to work. Please connect an external drive that is %1$sGB or larger and follow the steps to format it as device storage." + "Not enough storage" + "This programme will not be recorded because there is not enough storage. Try deleting some existing recordings." "Missing storage" - "Some of the storage used by DVR is missing. Please connect the external drive that you used before to re-enable DVR. Alternatively, you can choose to forget the storage if it\'s no longer available." - "Forget storage?" - "All your recorded content and schedules will be lost." "Stop recording?" "The recorded content will be saved." - - + "The recording of %1$s will be stopped because it conflicts with this programme. The recorded content will be saved." "Recording scheduled but has conflicts" "Recording has started but has conflicts" "%1$s will be recorded." @@ -306,14 +292,27 @@ "The same programme has already been scheduled to be recorded at %1$s." "Already recorded" "This programme has already been recorded. It’s available in the DVR library." - - - - - - - - + "Series recording scheduled" + + %1$d recordings have been scheduled for %2$s. + %1$d recording has been scheduled for %2$s. + + + %1$d recordings have been scheduled for %2$s. %3$d of them will not be recorded due to conflicts. + %1$d recording has been scheduled for %2$s. It will not be recorded due to conflicts. + + + %1$d recordings have been scheduled for %2$s. %3$d episodes of this series and other series will not be recorded due to conflicts. + %1$d recording has been scheduled for %2$s. %3$d episodes of this series and other series will not be recorded due to conflicts. + + + %1$d recordings have been scheduled for %2$s. 1 episode of other series will not be recorded due to conflicts. + %1$d recording has been scheduled for %2$s. 1 episode of other series will not be recorded due to conflicts. + + + %1$d recordings have been scheduled for %2$s. %3$d episodes of other series will not be recorded due to conflicts. + %1$d recording has been scheduled for %2$s. %3$d episodes of other series will not be recorded due to conflicts. + "Recorded programme not found." "Related recordings" "(No programme description)" @@ -336,6 +335,7 @@ "Stop series recording?" "Recorded episodes will remain available in the DVR library." "Stop" + "No episodes are on air now." "No episodes are available.\nThey will be recorded once they are available." (%1$d minutes) diff --git a/res/values-es-rUS/strings.xml b/res/values-es-rUS/strings.xml index 14e9f506..314058c9 100644 --- a/res/values-es-rUS/strings.xml +++ b/res/values-es-rUS/strings.xml @@ -20,9 +20,8 @@ "mono" "estéreo" "Controles de reproducción" - "Recientes" + "Canales" "Opciones de TV" - "Opciones de PIP" "Los controles de reproducción no están disponibles en este canal." "Reproducir o pausar" "Avanzar" @@ -35,33 +34,15 @@ "Subtítulos" "Modo pantalla" "PIP" - "Activada" - "Desactivada" "Varios audios" "Obtener canales" "Configuración" - "Fuente" - "Cambiar" - "Activada" - "Desactivada" - "Sonido" - "Principal" - "Ventana PIP" - "Diseño" - "Abajo derecha" - "Arriba derecha" - "Arriba izqda." - "Abajo izquierda" - "Lado a lado" - "Tamaño" - "Grande" - "Pequeña" - "Fuente de entrada" "Televisión (antena/cable)" "No hay información del programa" "Sin información" "Canal bloqueado" - "Idioma desconocido" + "Idioma desconocido" + "Subtítulos opcionales en %1$d" "Subtítulos opcionales" "Desactivado" "Personalizar formato" @@ -135,6 +116,10 @@ "El PIN es incorrecto. Vuelve a intentarlo." "Inténtalo de nuevo, el PIN no coincide." + "Ingresa tu código postal" + "La app de Live Channels usará el código postal para brindar la guía completa de programas de los canales de TV." + "Ingresa tu código postal" + "Código postal no válido" "Configuración" "Personalizar lista de canales" "Seleccionar canales de la guía de programas" @@ -143,6 +128,7 @@ "Controles parentales" "Licencias de código abierto" "Licencias de código abierto" + "Envía comentarios" "Versión" "Para mirar este canal, presiona la tecla hacia la derecha e ingresa el PIN." "Para mirar este programa, presiona la tecla hacia la derecha e ingresa el PIN." @@ -181,8 +167,6 @@ "Presiona SELECCIONAR"" para acceder al menú de la televisión." "No se encontró ninguna entrada de TV." "No se puede encontrar la entrada de TV." - "No se admite PIP." - "No hay entradas disponibles que se puedan mostrar con PIP." "Tipo de sintonizador no admitido. Abre la aplicación Canales en vivo para el tipo de sintonizador de entrada de TV." "Error al sintonizar" "No se encontró ninguna aplicación que pueda realizar esta acción." @@ -259,8 +243,6 @@ "Guardar" "Las grabaciones únicas tienen mayor prioridad" "Cancelar" - "Cancelar" - "Borrar" "Detener" "Ver cronograma de grabación" "Solo este programa" @@ -270,25 +252,29 @@ "Grabar este programa en su lugar" "Cancelar esta grabación" "Mirar ahora" + "Borrar grabaciones…" "Se puede grabar" "Grabación programada" "Error de grabación" "Grabando" "Se produjo un error al grabar" "Leyendo programas para crear programaciones de grabación" - "Leyendo programas" - - + "Leyendo programas" + "Ver grabaciones recientes" + "No se completó la grabación de %1$s" + "No se completó la grabación de %1$s y %2$s" + "No se completó la grabación de %1$s, %2$s y %3$s" + "No se completó la grabación de %1$s por falta de espacio de almacenamiento." + "No se completó la grabación de %1$s y %2$s por falta de espacio de almacenamiento." + "No se completó la grabación de %1$s, %2$s y %3$s por falta de espacio de almacenamiento." "El DVR necesita más espacio de almacenamiento" "Si bien puedes grabar programas con el DVR, no hay espacio de almacenamiento suficiente en tu dispositivo para usar esta opción. Conecta una unidad externa de %1$s GB como mínimo y sigue los pasos para formatearla como almacenamiento del dispositivo." + "No hay suficiente espacio de almacenamiento" + "No se grabará este programa porque no hay suficiente espacio de almacenamiento. Intenta borrar algunas grabaciones existentes." "Falta almacenamiento" - "Falta parte del almacenamiento que se usa para DVR. Conecta la unidad externa que usaste anteriormente para volver a habilitar esta función. También puedes borrar el almacenamiento si ya no está disponible." - "¿Borrar almacenamiento?" - "Se perderán todas las programaciones y los contenidos grabados." "¿Deseas detener la grabación?" "Se guardará el contenido grabado." - - + "Se detendrá la grabación de %1$s porque entra en conflicto con este programa. Sin embargo, se guardará el contenido grabado." "Grabación programada con conflictos" "Comenzó la grabación, pero tiene problemas" "Se grabará %1$s." @@ -306,14 +292,27 @@ "El mismo programa se grabará a las %1$s." "Programa grabado" "Este programa ya está grabado. Está disponible en la biblioteca de DVR." - - - - - - - - + "Se programó la grabación de series" + + Se programaron %1$d grabaciones de %2$s. + Se programó %1$d grabación de %2$s. + + + Se programaron %1$d grabaciones de %2$s. No se grabarán %3$d de ellas debido a algunos conflictos. + Se programó %1$d grabación de %2$s. No podrá completarse debido a algunos conflictos. + + + Se programaron %1$d grabaciones de %2$s. No se grabarán %3$d episodios de esta serie y de otras debido a algunos conflictos. + Se programó %1$d grabación de %2$s. No se grabarán %3$d episodios de esta serie y de otras debido a algunos conflictos. + + + Se programaron %1$d grabaciones de %2$s. No se grabará 1 episodio de otra serie debido a algunos conflictos. + Se programó %1$d grabación de %2$s. No se grabará 1 episodio de otra serie debido a algunos conflictos. + + + Se programaron %1$d grabaciones de %2$s. No se grabarán %3$d episodios de otra serie debido a algunos conflictos. + Se programó %1$d grabación de %2$s. No se grabarán %3$d episodios de otra serie debido a algunos conflictos. + "No se encontró el programa grabado." "Grabaciones relacionadas" "(Sin descripción del programa)" @@ -336,6 +335,7 @@ "¿Detener grabación de la serie?" "Los episodios grabados estarán disponibles en la biblioteca de DVR." "Detener" + "No hay episodios en vivo disponibles en este momento." "No hay episodios disponibles.\nSe grabarán cuando lo estén." (%1$d minutos) diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml index badab6a4..f7941bc7 100644 --- a/res/values-es/strings.xml +++ b/res/values-es/strings.xml @@ -20,9 +20,8 @@ "mono" "estéreo" "Controles de reproducción" - "Canales recientes" + "Canales" "Opciones de TV" - "Opciones de PIP" "Controles de reproducción no disponibles en este canal" "Reproducir o pausar" "Avance rápido" @@ -35,33 +34,15 @@ "Subtítulos" "Modo de pantalla" "PIP" - "Activado" - "Desactivado" "Varios audios" "Más canales" "Ajustes" - "Fuente" - "Cambiar" - "Activado" - "Desactivado" - "Sonido" - "Principal" - "Ventana PIP" - "Diseño" - "Abajo derecha" - "Arriba derecha" - "Arriba izquierda" - "Abajo izquierda" - "En paralelo" - "Dimensiones" - "Grande" - "Pequeño" - "Fuente de entrada" "TV (antena/cable)" "No hay información del programa" "Sin información" "Canal bloqueado" - "Idioma desconocido" + "Idioma desconocido" + "Subtítulos en %1$d" "Subtítulos" "Desactivados" "Personalizar formato" @@ -135,14 +116,19 @@ "Ese PIN era incorrecto. Vuelve a intentarlo." "Vuelve a intentarlo, el PIN no coincide" + "Introduce tu código postal" + "La aplicación TV en directo se basa en el código postal para ofrecer una programación completa de los canales de televisión disponibles." + "Introduce tu código postal" + "El código postal no es válido" "Ajustes" "Personalizar lista de canales" "Seleccionar canales de la programación" "Fuentes de canales" "Nuevos canales disponibles" "Control parental" - "Licencias de software libre" - "Licencias software libre" + "Licencias de código abierto" + "Licencias de código abierto" + "Enviar sugerencias" "Versión" "Para ver este canal, pulsa la tecla hacia la derecha e introduce el número PIN" "Para ver este programa, pulsa la tecla hacia la derecha e introduce el número PIN" @@ -181,8 +167,6 @@ "Pulsa SELECCIONAR"" para acceder al menú de la TV." "No se han encontrado entradas de TV" "No se puede encontrar la entrada de TV" - "PIP no admitido" - "No hay entradas disponibles que se puedan mostrar con PIP" "Tipo de sintonizador inadecuado. Abre Canales en directo para seleccionar la entrada de TV del tipo de sintonizador." "Error al sintonizar" "No se ha encontrado ninguna aplicación que pueda realizar esta acción." @@ -259,8 +243,6 @@ "Guardar" "Las grabaciones únicas son las que tienen mayor prioridad" "Cancelar" - "Cancelar" - "Olvidar" "Detener" "Ver programación de grabación" "Este programa" @@ -270,25 +252,29 @@ "Grabar esta en su lugar" "Cancelar esta grabación" "Ver ahora" + "Eliminar grabaciones…" "Se puede grabar" "Grabación programada" "Problema de grabación" "Grabación" "No se ha podido grabar" "Leyendo programas para crear programaciones de grabación" - "Leyendo programas" - - + "Leyendo programas" + "Ver grabaciones recientes" + "La grabación de %1$s está incompleta." + "Las grabaciones de %1$s y %2$s están incompletas." + "Las grabaciones de %1$s, %2$s y %3$s están incompletas." + "No se ha completado la grabación de %1$s porque no hay espacio de almacenamiento suficiente." + "No se han completado las grabaciones de %1$s y %2$s porque no hay espacio de almacenamiento suficiente." + "No se han completado las grabaciones de %1$s, %2$s y %3$s porque no hay espacio de almacenamiento suficiente." "Se necesita más almacenamiento para el DVR" "Puedes grabar programas con el DVR, pero no tienes suficiente espacio de almacenamiento en el dispositivo para que el DVR funcione. Conecta una unidad externa que tenga %1$s GB como mínimo y sigue los pasos para formatearlo como almacenamiento del dispositivo." + "No hay suficiente espacio de almacenamiento" + "Este programa no se grabará porque no hay espacio de almacenamiento suficiente. Borra algunas grabaciones." "No se puede acceder al almacenamiento" - "No se puede acceder a parte del almacenamiento utilizado por el DVR. Para volver a habilitarlo, conecta la unidad externa que has utilizado anteriormente. También puedes indicar que se olvide el almacenamiento si ya no está disponible." - "¿Olvidar almacenamiento?" - "Se perderán todo el contenido grabado y las programaciones." "¿Detener grabación?" "El contenido grabado se guardará." - - + "La grabación de %1$s se detendrá porque entra en conflicto con este programa. El contenido grabado se guardará." "Grabación programada con conflictos" "La grabación se ha iniciado, pero tiene conflictos" "Se grabará %1$s." @@ -306,14 +292,27 @@ "Ya se ha programado la grabación del mismo programa para esta hora: %1$s." "Ya se ha grabado" "Este programa ya se ha grabado y está disponible en la colección del DVR." - - - - - - - - + "Grabación de series programada" + + Se han programado %1$d grabaciones para %2$s. + Se ha programado %1$d grabación para %2$s. + + + Se han programado %1$d grabaciones para %2$s. Episodios que no se grabarán debido a conflictos: %3$d. + Se ha programado %1$d grabación para %2$s. Debido a conflictos, no se grabará. + + + Se han programado %1$d grabaciones para %2$s. Episodios de esta y otras series que no se grabarán debido a conflictos: %3$d. + Se ha programado %1$d grabación para %2$s. Episodios de esta y otras series que no se grabarán debido a conflictos: %3$d. + + + Se han programado %1$d grabaciones para %2$s. Debido a conflictos, no se grabará 1 episodio de otra serie. + Se ha programado %1$d grabación para %2$s. Debido a conflictos, no se grabará 1 episodio de otra serie. + + + Se han programado %1$d grabaciones para %2$s. Episodios de otras series que no se grabarán debido a conflictos: %3$d. + Se ha programado %1$d grabación para %2$s. Episodios de otras series que no se grabarán debido a conflictos: %3$d. + "No se ha encontrado el programa grabado." "Grabaciones relacionadas" "(No hay ninguna descripción)" @@ -336,6 +335,7 @@ "¿Detener la grabación de series?" "Los episodios grabados seguirán estando disponibles en la colección del DVR." "Detener" + "No se está emitiendo ningún episodio." "No hay episodios disponibles.\nSe grabarán cuando estén disponibles." (%1$d minutos) diff --git a/res/values-et-rEE/strings.xml b/res/values-et-rEE/strings.xml index 83022b8e..4d56af1e 100644 --- a/res/values-et-rEE/strings.xml +++ b/res/values-et-rEE/strings.xml @@ -20,9 +20,8 @@ "mono" "stereo" "Esituse juhtnupud" - "Viimas. kanalid" + "Kanalid" "TV-valikud" - "PIP-valikud" "Esituse juhtelemendid ei ole selle kanali puhul saadaval" "Esitamine või peatamine" "Edasikerimine" @@ -35,33 +34,15 @@ "Subtiitrid" "Kuvarežiim" "PIP" - "Sees" - "Väljas" "Multiaudio" "Hangi kanaleid" "Seaded" - "Allikas" - "Vaheta" - "Sees" - "Väljas" - "Heli" - "Peamine" - "PIP-aken" - "Paigutus" - "All paremal" - "Ülal paremal" - "Ülal vasakul" - "All vasakul" - "Kõrvuti" - "Suurus" - "Suur" - "Väike" - "Sisendallikas" "TV (antenn/kaabel)" "Programmiteavet pole" "Teave puudub" "Blokeeritud kanal" - "Tundmatu keel" + "Tundmatu keel" + "Subtiitrid %1$d" "Subtiitrid" "Väljas" "Vormingu kohandamine" @@ -135,6 +116,10 @@ "See PIN-kood oli vale. Proovige uuesti." "Proovige uuesti, PIN-kood pole õige" + "Sisestage sihtnumber." + "Rakendus Reaalajakanalid kasutab telekanalitele täieliku saatekava pakkumiseks sihtnumbrit." + "Sisestage sihtnumber" + "Sobimatu sihtnumber" "Seaded" "Kohanda kanaliloendit" "Kanalite valimine saatekava jaoks" @@ -143,6 +128,7 @@ "Vanemlik järelevalve" "Avatud lähtekoodi litsentsid" "Avatud lähtekoodi litsentsid" + "Tagasiside saatmine" "Versioon" "Kanali vaatamiseks vajutage paremale ja sisestage PIN-kood" "Saate vaatamiseks vajutage paremale ja sisestage PIN-kood" @@ -181,8 +167,6 @@ "Teleri menüüle juurdepääsemiseks ""vajutage nuppu SELECT""." "TV-sisendit ei leitud" "TV-sisendit ei õnnestu leida" - "PIP-d ei toetata" - "Pole ühtegi sisendit, mida saab näidata koos PIP-ga" "Sobimatu tuuneri tüüp. Käivitage tuuneri tüübi TV-sisendi jaoks rakendus Otseülekande kanalid." "Häälestamine ebaõnnestus" "Selle toimingu käsitlemiseks ei leitud ühtegi rakendust." @@ -259,8 +243,6 @@ "Salvesta" "Ühekordsete salvestiste prioriteet on kõige kõrgem" "Tühista" - "Tühista" - "Unusta" "Peata" "Kuva salvestamise ajakava" "Ainult see saade" @@ -270,25 +252,29 @@ "Salvesta hoopis see" "Tühista see salvestus" "Kuva nüüd" + "Kustuta salvestised …" "Salvestatav" "Salvestamine on ajastatud" "Konflikt salvestamisel" "Salvestamine" "Salvestamine ebaõnnestus" "Programmide lugemine salvestusajakavade loomiseks" - "Programmide lugemine" - - + "Programmide lugemine" + "Kuva hiljutised salvestised" + "Saate %1$s salvestamine on pooleli." + "Saadete %1$s ja %2$s salvestamine on pooleli." + "Saadete %1$s, %2$s ja %3$s salvestamine on pooleli." + "Ebapiisava salvestusruumi tõttu ei viidud saate %1$s salvestamist lõpule." + "Ebapiisava salvestusruumi tõttu ei viidud saadete %1$s ja %2$s salvestamist lõpule." + "Ebapiisava salvestusruumi tõttu ei viidud saadete %1$s, %2$s ja %3$s salvestamist lõpule." "DVR vajab rohkem salvestusruumi" "Saateid saate salvestada DVR-iga. Praegu pole teie seadmes DVR-i töötamiseks siiski piisavalt salvestusruumi. Ühendage väline ketas, mille maht on vähemalt %1$s GB, ja järgige juhiseid selle vormindamiseks salvestusseadmena." + "Pole piisavalt mälu" + "Programmi ei salvestata, kuna salvestusruumi on liiga vähe. Kustutage mõned olemasolevad salvestised." "Puuduv salvestusruum" - "Osa DVR-i kasutatavast salvestusruumist on puudu. DVR-i uuesti lubamiseks ühendage väline ketas, mida varem kasutasite. Teise võimalusena saate salvestusruumi unustada, kui see enam saadaval pole." - "Kas unustada salvestusruum?" - "Kogu teie salvestatud sisu ja ajakavad lähevad kaotsi." "Kas peatada salvestamine?" "Salvestatud sisu talletatakse." - - + "Saate %1$s salvestamine peatatakse, kuna see on selle saatega konfliktis. Salvestatud sisu salvestatakse." "Salvestamine on ajastatud, ent ilmnesid vastuolud" "Salvestamine on alanud, kuid ilmnesid vastuolud" "Saadet %1$s salvestatakse." @@ -306,14 +292,27 @@ "Sama saate salvestus on juba lisatud ajakavva algusega %1$s." "Juba salvestatud" "See saade on juba salvestatud. See on saadaval DVR-i kogus." - - - - - - - - + "Sarja salvestamine lisati ajakavasse" + + Seeria %2$s puhul lisati ajakavasse %1$d salvestist. + Seeria %2$s puhul lisati ajakavasse %1$d salvestis. + + + Seeria %2$s puhul lisati ajakavasse %1$d salvestist. Konfliktide tõttu ei salvestata neist %3$d. + Seeria %2$s puhul lisati ajakavasse %1$d salvestis. Konfliktide tõttu seda ei salvestata. + + + Seeria %2$s puhul lisati ajakavasse %1$d salvestist. Konfliktide tõttu ei salvestata selle seeria ja muude seeriate %3$d jagu. + Seeria %2$s puhul lisati ajakavasse %1$d salvestis. Konfliktide tõttu ei salvestata selle seeria ja muude seeriate %3$d jagu. + + + Seeria %2$s puhul lisati ajakavasse %1$d salvestist. Konfliktide tõttu ei salvestata muu seeria 1 jagu. + Seeria %2$s puhul lisati ajakavasse %1$d salvestis. Konfliktide tõttu ei salvestata muu seeria 1 jagu. + + + Seeria %2$s puhul lisati ajakavasse %1$d salvestist. Konfliktide tõttu ei salvestata muu seeria %3$d jagu. + Seeria %2$s puhul lisati ajakavasse %1$d salvestis. Konfliktide tõttu ei salvestata muu seeria %3$d jagu. + "Salvestatud programmi ei leitud." "Seotud salvestised" "(Programmi kirjeldust pole)" @@ -336,6 +335,7 @@ "Kas peatada seeria salvestamine?" "Salvestatud jaod jäävad saadavale DVR-i kogusse." "Peata" + "Ühtegi jagu pole praegu eetris." "Ükski osa pole saadaval.\nNeed salvestatakse siis, kui need kättesaadavaks muutuvad." (%1$d minutit) diff --git a/res/values-eu-rES/strings.xml b/res/values-eu-rES/strings.xml index 19a00433..e9761874 100644 --- a/res/values-eu-rES/strings.xml +++ b/res/values-eu-rES/strings.xml @@ -20,9 +20,8 @@ "mono" "estereo" "Erreprodukzioa kontrolatzeko aukerak" - "Azken kanalak" + "Kanalak" "Telebistaren aukerak" - "Pantaila txikia" "Erreprodukzioa kontrolatzeko aukerak ez daude erabilgarri kanal honetan" "Erreproduzitu edo pausatu" "Aurreratu" @@ -35,33 +34,15 @@ "Azpitituluak" "Bistaratzeko modua" "Pantaila txikia" - "Aktibatuta" - "Desaktibatuta" "Audio anitza" "Kanal gehiago" "Ezarpenak" - "Iturburua" - "Aldatu" - "Aktibatuta" - "Desaktibatuta" - "Soinua" - "Nagusia" - "Pantaila txikia" - "Diseinua" - "Behean eskuinean" - "Goian eskuinean" - "Goian ezkerrean" - "Behean ezkerrean" - "Alboz albo" - "Tamaina" - "Handia" - "Txikia" - "Sarrera-iturburua" "Telebista (antena/digitala)" "Ez dago telesaioei buruzko informaziorik" "Ez dago informaziorik" "Blokeatutako kanala" - "Hizkuntza ezezaguna" + "Hizkuntza ezezaguna" + "Azpitituluak - %1$d" "Azpitituluak" "Desaktibatuta" "Pertsonalizatu formatua" @@ -135,6 +116,10 @@ "PIN kodea ez da zuzena. Saiatu berriro." "PIN kodeak ez datoz bat. Saiatu berriro." + "Idatzi posta-kodea." + "\"Telebista zuzenean\" aplikazioak posta-kodea erabiliko du telebistako kanalen programen gida osoa eskaintzeko." + "Idatzi posta-kodea" + "Posta-kodeak ez du balio" "Ezarpenak" "Pertsonalizatu zerrenda" "Aukeratu telesaioen gidako kanalak" @@ -143,6 +128,7 @@ "Gurasoen ezarpenak" "Kode irekiko lizentziak" "Kode irekiko lizentziak" + "Bidali iritzia" "Bertsioa" "Kanal hau ikusteko, sakatu Eskuinera tekla eta idatzi PIN kodea." "Telesaio hau ikusteko, sakatu Eskuinera tekla eta idatzi PIN kodea." @@ -181,15 +167,13 @@ "Telebistaren menua atzitzeko, sakatu HAUTATU" "Ez da telebista-sarrerarik aurkitu" "Ezin da aurkitu telebista-sarrera" - "Ez da pantaila txikia erabiltzea onartzen" - "Ez dago pantaila txikian erakuts daitekeen sarrerarik" - "Sintonizagailua ez da egokia. Abiarazi zuzeneko kanalen aplikazioa sintonizagailua telebistaren sarrera gisa erabiltzeko." + "Sintonizagailua ez da egokia. Abiarazi Telebista zuzenean aplikazioa sintonizagailua telebistaren sarrera gisa erabiltzeko." "Ezin izan da sintonizatu" "Ez da aurkitu ekintza gauza dezakeen aplikaziorik." "Iturburuko kanal guztiak ezkutuan daude.\nHautatu gutxienez kanal bat ikusteko." "Bideoa ez dago erabilgarri" "Atzera tekla konektatutako gailuari dagokio. Irteteko, sakatu Hasiera botoia." - "Zuzeneko kanalak aplikazioak baimena behar du telebistako programazioa irakurtzeko." + "Telebista zuzenean aplikazioak baimena behar du telebistako programazioa irakurtzeko." "Konfiguratu iturburuak" "Telebista zuzenean zerbitzuarekin, aplikazioek zuzenean erreproduzitzen dituzten kanalak ohiko telebistaren moduan ikus ditzakezu. \n\nLehen urratsak emateko, konfiguratu instalatutako kanal-iturburuak. Bestela, arakatu Google Play Store denda zuzeneko kanalak eskaintzen dituzten aplikazio gehiago aurkitzeko." "Grabaketak eta grabaketen programazioa" @@ -259,8 +243,6 @@ "Gorde" "Grabaketa solteek dute lehentasun handiena" "Utzi" - "Utzi" - "Ahaztu" "Gelditu" "Ikusi grabaketen agenda" "Programa hau bakarrik" @@ -270,25 +252,29 @@ "Grabatu beste hau haren ordez" "Utzi grabaketa hau bertan behera" "Ikusi" + "Ezabatu grabaketak…" "Graba daiteke" "Grabatzeko antolatuta" "Grabatzeko gatazka" "Grabatzen" "Ezin izan da grabatu" "Programak irakurtzen ari gara grabaketa-ordutegiak sortzeko" - "Programazioa irakurtzen" - - + "Programazioa irakurtzen" + "Ikusi azken grabaketak" + "Ezin izan da osorik grabatu %1$s." + "Ezin izan dira osorik grabatu %1$s eta %2$s." + "Ezin izan dira osorik grabatu %1$s, %2$s eta %3$s." + "Ezin izan da osorik grabatu %1$s, memoria agortu delako." + "Ezin izan dira osorik grabatu %1$s eta %2$s, memoria agortu delako." + "Ezin izan dira osorik grabatu %1$s, %2$s eta %3$s, memoria agortu delako." "Bideo-grabagailu digitalak ez dauka behar adina memoria erabilgarri" "Bideo-grabagailu digitalarekin programak grabatu ahal izango dituzu. Dena dela, une honetan ez daukazu bideo-grabagailua erabili ahal izateko behar adina memoria erabilgarri. Konektatu %1$s GB edo gehiago dituen unitate aldagarri bat eta formatea ezazu gailuaren memoria gisa erabiltzeko." + "Ez dago behar adina toki" + "Ezingo dugu grabatu telesaio hau, ez delako behar adina toki geratzen. Ezabatu grabaketa batzuk tokia egiteko." "Memoria-unitatea falta da" - "Bideo-grabagailu digitalak erabili duen memoria-unitateren bat falta da. Bideo-grabagailu digitala gaitu ahal izateko, konektatu aurrez erabilitako unitate aldagarria. Memoria-unitate hura eskura ez baduzu, berriz, aukera ezazu unitatea ahazteko aukera." - "Memoria-unitate hau ahaztea nahi duzu?" - "Grabatuta edo programatuta duzun eduki guztia galduko da." "Grabaketa gelditu nahi duzu?" "Grabatutako edukia gordeta geratuko da." - - + "%1$s grabatzeari utziko zaio programa honekin gatazkan dagoelako. Gorde egingo da grabatutako edukia." "Grabatzeko programatu da baina gatazkan dago beste grabaketa batzuekin" "Grabatzen hasi da, baina gatazkak ditu" "%1$s grabatuko da." @@ -306,14 +292,27 @@ "Programa hau bera grabatzeko programatu duzu dagoeneko (%1$s)." "Grabatuta dago dagoeneko" "Programa hau grabatuta daukazu dagoeneko. DVR liburutegian duzu ikusgai." - - - - - - - - + "Telesaila grabatzeko antolatuta dago" + + %1$d grabaketa antolatu dira (%2$s). + %1$d grabaketa antolatu da (%2$s). + + + %1$d grabaketa antolatu dira (%2$s). Haietako %3$d ezin izango dira grabatu gatazkak daudelako. + %1$d grabaketa antolatu da (%2$s). Ezin izango da grabatu gatazkak daudelako. + + + %1$d grabaketa antolatu dira (%2$s). Telesail horren eta beste batzuen %3$d atal ezin izango dira grabatu gatazkak daudelako. + %1$d grabaketa antolatu da (%2$s). Telesail horren eta beste batzuen %3$d atal ezin izango dira grabatu gatazkak daudelako. + + + %1$d grabaketa antolatu dira (%2$s). Beste telesail baten atal bat ezin izango da grabatu gatazkak daudelako. + %1$d grabaketa antolatu da (%2$s). Beste telesail baten atal bat ezin izango da grabatu gatazkak daudelako. + + + %1$d grabaketa antolatu dira (%2$s). Beste telesail batzuen %3$d atal ezin izango dira grabatu gatazkak daudelako. + %1$d grabaketa antolatu da (%2$s). Beste telesail batzuen %3$d atal ezin izango dira grabatu gatazkak daudelako. + "Ez da aurkitu grabatutako programa." "Erlazionatutako grabaketak" "(Ez dago programaren azalpenik)" @@ -336,6 +335,7 @@ "Seriea grabatzeari utzi nahi diozu?" "DVR liburutegian gordeta geratuko dira grabatutako atalak." "Gelditu" + "Une honetan ez dira ari atalik igortzen." "Ez dago atalik ikusgai.\nIkusgai ezartzen dituztenean grabatuko ditugu." (%1$d minutu) diff --git a/res/values-fa/strings.xml b/res/values-fa/strings.xml index 7dd59c28..396a5e66 100644 --- a/res/values-fa/strings.xml +++ b/res/values-fa/strings.xml @@ -20,9 +20,8 @@ "مونو" "استریو" "کنترل‌های پخش" - "کانال‌های اخیر" + "کانال‌ها" "گزینه‌‌ تلویزیون" - "‏گزینه‌های PIP" "دسترسی به کنترل‌های پخش برای این کانال امکان‌پذیر نیست" "پخش یا مکث" "جلو بردن سریع" @@ -35,33 +34,15 @@ "زیرنویس‌ها" "حالت نمایش" "تصویر در تصویر" - "روشن" - "خاموش" "چند صدایی" "دریافت کانا‌ل‌های بیشتر" "تنظیمات" - "منبع" - "تعویض" - "روشن" - "خاموش" - "صدا" - "اصلی" - "پنجره تصویردرتصویر" - "طرح‌بندی" - "پایین سمت راست" - "بالا سمت راست" - "بالا سمت چپ" - "پایین سمت چپ" - "پهلو به پهلو" - "اندازه" - "بزرگ" - "کوچک" - "منبع ورودی" "تلویزیون (آنتنی/کابلی)" "هیچ اطلاعات برنامه‌ای وجود ندارد" "هیچ اطلاعاتی موجود نیست" "کانال مسدود شده" - "زبان نامشخص" + "زبان نامشخص" + "‏زیرنویس %1$d" "زیرنویس" "خاموش" "سفارشی کردن قالب‌بندی" @@ -135,6 +116,10 @@ "این پین اشتباه بود. دوباره امتحان کنید." "دوباره امتحان کنید، پین منطبق نیست" + "زیپ‌کدتان را وارد کنید." + "برنامه «کانال‌های زنده» از زیپ‌کد جهت ارائه یک راهنمای برنامه کامل برای کانال‌های تلویزیونی استفاده می‌کند." + "زیپ‌کدتان را وارد کنید" + "زیپ‌کد نامعتبر است" "تنظیمات" "سفارشی کردن فهرست کانال‌ها" "کانال‌ها را برای راهنمای برنامه‌تان انتخاب کنید" @@ -143,6 +128,7 @@ "کنترل‌های والدین" "مجوزهای منبع آزاد" "مجوزهای منبع آزاد" + "ارسال بازخورد" "نسخه" "برای مشاهده این کانال، راست را فشار دهید و پین خودتان را وارد کنید" "برای مشاهده این برنامه، راست را فشار دهید و پین خودتان را وارد کنید" @@ -181,8 +167,6 @@ "‏برای دسترسی به منوی تلویزیون، ""SELECT (انتخاب) را فشار دهید""." "هیچ ورودی تلویزیون یافت نشد" "نمی‌توان ورودی تلویزیون پیدا کرد" - "تصویر در تصویر پشتیبانی نمی‌شود." - "ورودی قابل نمایش با تصویر در تصویر، در دسترس نیست." "نوع تیونر مناسب نیست. لطفاً برنامه‌ «کانال‌های مستقیم» را برای ورودی تلویزیون نوع تیونر، راه‌اندازی کنید." "تنظیم نشد" "برنامه‌ای برای انجام این اقدام پیدا نشد." @@ -259,8 +243,6 @@ "ذخیره" "ضبط‌های تکی بالاترین اولویت را دارند" "لغو" - "لغو" - "فراموش شود" "توقف" "مشاهده زمان‌بندی ضبط" "فقط همین برنامه" @@ -270,25 +252,29 @@ "درعوض این مورد ضبط شود" "لغو این ضبط" "اکنون تماشا کنید" + "درحال حذف موارد ضبط‌شده…" "قابل ضبط" "ضبط برنامه‌ریزی‌شده" "ضبط متناقض با زمان‌بندی" "درحال ضبط" "ضبط ناموفق بود" "درحال خواندن برنامه‌ها برای ایجاد زمان‌بندی ضبط" - "درحال خواندن برنامه‌ها" - - + "درحال خواندن برنامه‌ها" + "مشاهده موارد ضبط‌شده اخیر" + "ضبط %1$s کامل نشده است." + "ضبط %1$s و %2$s کامل نشده است." + "ضبط %1$s، %2$s و %3$s کامل نشده است." + "به دلیل حافظه ناکافی، ضبط %1$s کامل نشد." + "به دلیل حافظه ناکافی، ضبط %1$s و%2$s کامل نشد." + "به دلیل حافظه ناکافی، ضبط %1$s، %2$s و %3$s کامل نشد." "‏DVR به فضای بیشتری نیاز دارد" "‏با DVR می‌توانید برنامه‌ها را ضبط کنید. اما اکنون فضای ذخیره‌سازی کافی در دستگاهتان وجود ندارد و DVR کار نمی‌کند. لطفاً درایو خارجی‌‌ای با حجم %1$s گیگابایت یا بیشتر متصل کنید و برای قالب‌بندی آن‌ به‌عنوان حافظه دستگاه این مراحل را دنبال کنید." + "حافظه ذخیره‌سازی کافی نیست" + "این برنامه ضبط نخوادهد شد، زیرا حافظه ذخیره‌سازی وجود ندارد. برخی از موارد ضبط‌‌شده قبلی را حذف کنید." "حافظه دردسترس نیست" - "‏مقداری از حافظه‌ای که توسط DVR استفاده می‌شود از بین می‌رود. لطفاً برای فعال‌سازی مجدد DVR، درایو خارجی را که قبلاً‌ استفاده کردید متصل کنید. یا اگر این حافظه دیگر دردسترس نیست می‌توانید انتخاب کنید فراموش شود." - "حافظه فراموش شود؟" - "همه زمان‌بندی‌ها و محتوای ضبط‌شده‌ شما از دست می‌رود." "ضبط متوقف شود؟" "محتوای ضبط‌شده ذخیره خواهد شد." - - + "ضبط %1$s به دلیل تناقض با این برنامه متوقف خواهد شد. محتوای ضبط‌شده ذخیره می‌شود." "ضبط، زمان‌بندی شده است اما متناقض است" "ضبط شروع شده است اما متناقض است" "%1$s ضبط خواهد شد." @@ -306,14 +292,27 @@ "این برنامه قبلاً‌ برای ضبط در %1$s زمان‌بندی شده است." "قبلاً‌ ضبط شده است" "‏این برنامه قبلاً‌ ضبط شده است و در کتابخانه DVR موجود است." - - - - - - - - + "ضبط مجموعه زمان‌بندی شد" + + %1$d ضبط برای %2$s زمان‌بندی شده است. + %1$d ضبط برای %2$s زمان‌بندی شده است. + + + %1$d ضبط برای %2$s زمان‌بندی شده است. %3$d از آن‌ها به دلیل تناقض ضبط نخواهد شد. + %1$d ضبط برای %2$s زمان‌بندی شده است. %3$d از آن‌ها به دلیل تناقض ضبط نخواهد شد. + + + %1$d ضبط برای %2$s زمان‌بندی شده است. %3$d قسمت این مجموعه و مجموعه‌های دیگر به دلیل تناقض ضبط نخواهند شد. + %1$d ضبط برای %2$s زمان‌بندی شده است. %3$d قسمت این مجموعه و مجموعه‌های دیگر به دلیل تناقض ضبط نخواهند شد. + + + %1$d ضبط برای %2$s زمان‌بندی شده است. ۱ قسمت مجموعه‌های دیگر به دلیل تناقض ضبط نخواهد شد. + %1$d ضبط برای %2$s زمان‌بندی شده است. ۱ قسمت مجموعه‌های دیگر به دلیل تناقض ضبط نخواهد شد. + + + %1$d ضبط برای %2$s زمان‌بندی شده است. %3$d قسمت مجموعه‌های دیگر به دلیل تناقض ضبط نخواهند شد. + %1$d ضبط برای %2$s زمان‌بندی شده است. %3$d قسمت مجموعه‌های دیگر به دلیل تناقض ضبط نخواهند شد. + "برنامه ضبط‌شده پیدا نشد." "ضبط‌های مرتبط" "(بدون شرح برنامه)" @@ -336,6 +335,7 @@ "ضبط مجموعه متوقف شود؟" "‏قسمت‌های ضبط‌شده در کتابخانه DVR دردسترس باقی می‌ماند." "توقف" + "درحال‌حاضر قسمتی پخش مستقیم نمی‌شود." "هیچ قسمتی موجود نیست.\nهر قسمتی در دسترس قرار بگیرد ضبط خواهد شد." ‏(%1$d دقیقه) diff --git a/res/values-fi/strings.xml b/res/values-fi/strings.xml index b6f5fb7e..548ef41f 100644 --- a/res/values-fi/strings.xml +++ b/res/values-fi/strings.xml @@ -20,9 +20,8 @@ "mono" "stereo" "Toistosäätimet" - "Viim. kanavat" + "Kanavat" "TV-asetukset" - "PIP-asetukset" "Toistosäätimet eivät ole käytettävissä tällä kanavalla." "Toisto tai keskeytys" "Kelaa eteenpäin" @@ -35,33 +34,15 @@ "Tekstitykset" "Näyttötila" "PIP" - "Käytössä" - "Ei käytössä" "Moniääni" "Lisää kanavia" "Asetukset" - "Lähde" - "Vaihda" - "Käytössä" - "Ei käytössä" - "Ääni" - "Ensisijainen" - "PIP-ikkuna" - "Asettelu" - "Alaoikealla" - "Yläoikealla" - "Ylävasemmalla" - "Alavasemmalla" - "Vierekkäin" - "Mitat" - "Suuri" - "Pieni" - "Syötteen lähde" "TV (antenni/kaapeli)" "Ei ohjelmatietoja" "Ei tietoja" "Estetty kanava" - "Tuntematon kieli" + "Tuntematon kieli" + "Tekstitykset %1$d" "Tekstitykset" "Ei käytössä" "Muokkaa muotoilua" @@ -135,6 +116,10 @@ "Väärä PIN-koodi. Yritä uudelleen." "PIN-koodit ovat erilaiset, yritä uudelleen" + "Anna postinumero" + "Live-kanavat-sovellus tarjoaa postinumeron avulla kaikkien alueellasi näkyvien TV-kanavien ohjelmaoppaan." + "Anna postinumero" + "Virheellinen postinumero" "Asetukset" "Muokkaa kanavaluetteloa" "Valitse kanavat ohjelmaoppaaseen" @@ -143,6 +128,7 @@ "Lapsilukko" "Avoimen lähdekoodin käyttöluvat" "Avoimen lähdekoodin käyttöluvat" + "Lähetä palautetta" "Versio" "Katsele tätä kanavaa painamalla näppäimellä oikealle ja antamalla PIN-koodi" "Katsele tämä ohjelma painamalla näppäimellä oikealle ja antamalla PIN-koodi" @@ -181,8 +167,6 @@ "Avaa TV-valikko ""painamalla VALITSE""." "TV-tuloa ei löytynyt." "TV-tuloa ei löydy." - "PIP:tä ei tueta." - "Käytettävissä ei ole syötettä, joka voitaisiin näyttää PIP:ssä." "Viritintä ei tueta. Voit käyttää viritintä käynnistämällä Live-kanavat-sovelluksen." "Viritys epäonnistui." "Tätä toimintoa käsittelevää sovellusta ei löydy." @@ -259,8 +243,6 @@ "Tallenna" "Kertaluotoiset tallennukset ovat tärkeimpiä." "Peruuta" - "Peruuta" - "Unohda" "Lopeta" "Näytä tallennusaikataulu" "Vain tämä jakso" @@ -270,25 +252,29 @@ "Nauhoita tämä sen sijaan" "Peruuta tämä nauhoitus" "Katso nyt" + "Poista tallenteita…" "Tallennettavissa" "Tallennus ajastettu" "Tallennusristiriita" "Tallennetaan" "Nauhoitus epäonnistui" "Luetaan ohjelmatietoja tallennusaikataulujen luomista varten." - "Luetaan ohjelmatietoja" - - + "Luetaan ohjelmatietoja" + "Näytä uusimmat tallenteet" + "Kohdetta %1$s ei nauhoitettu loppuun." + "Kohteita %1$s ja %2$s ei nauhoitettu loppuun." + "Kohteita %1$s, %2$s ja %3$s ei nauhoitettu loppuun." + "Kohdetta %1$s ei nauhoitettu loppuun, koska tallennustila ei riitä." + "Kohteita %1$s ja %2$s ei nauhoitettu loppuun, koska tallennustila ei riitä." + "Kohteita %1$s, %2$s ja %3$s ei nauhoitettu loppuun, koska tallennustila ei riitä." "DVR tarvitsee lisää tilaa" "Voit tallentaa ohjelmia DVR:llä. Laitteellasi ei kuitenkaan ole tarpeeksi tilaa DVR:n käyttöön. Yhdistä vähintään %1$s Gt:n kokoinen ulkoinen tallennuslaite ja alusta se laitteen tallennustilaksi ohjeiden mukaisesti." + "Tallennustila ei riitä" + "Tätä ohjelmaa ei tallenneta, koska tallennustila ei riitä. Kokeile poistaa joitakin nykyisiä tallenteita." "Tallennustila puuttuu" - "Osa DVR:n käytössä olleesta tallennustilasta puuttuu. Palauta DVR käyttöön liittämällä aiemmin käyttämäsi ulkoinen asema. Voit myös unohtaa tallennustilan, jos asema ei ole enää käytettävissä." - "Unohdetaanko tallennustila?" - "Kaikki tallennettu sisältö ja aikataulut menetetään." "Lopetetaanko tallennus?" "Tallennettu sisältö lisätään kirjastoon." - - + "Ohjelman %1$s nauhoitus pysäytetään, koska se on ristiriidassa tämän ohjelman kanssa. Nauhoitettu sisältö tallennetaan." "Tallennus ajastettu – ristiriitoja havaittu" "Tallennus käynnissä – ristiriitoja havaittu" "%1$s tallennetaan" @@ -306,14 +292,27 @@ "Sama ohjelma on jo ajastettu nauhoitettavaksi klo %1$s." "Jo nauhoitettu" "Tämä ohjelma on jo nauhoitettu. Se on käytettävissä DVR-kirjastossa." - - - - - - - - + "Sarjan nauhoitus ajastettu" + + %1$d sarjan %2$s nauhoitusta lisätty. + %1$d sarjan %2$s nauhoitus lisätty. + + + %1$d sarjan %2$s nauhoitusta lisätty. %3$d niistä jätetään nauhoittamatta ristiriidan takia. + %1$d sarjan %2$s nauhoitus lisätty. Se jätetään nauhoittamatta ristiriidan takia. + + + %1$d sarjan %2$s nauhoitusta lisätty. Tämän sarjan ja muiden sarjojen %3$d jaksoa jätetään nauhoittamatta ristiriidan takia. + %1$d sarjan %2$s nauhoitus lisätty. Tämän sarjan ja muiden sarjojen %3$d jaksoa jätetään nauhoittamatta ristiriidan takia. + + + %1$d sarjan %2$s nauhoitusta lisätty. Yksi toisen sarjan jakso jätetään nauhoittamatta ristiriidan takia. + %1$d sarjan %2$s nauhoitus lisätty. Yksi toisen sarjan jakso jätetään nauhoittamatta ristiriidan takia. + + + %1$d sarjan %2$s nauhoitusta lisätty. Muiden sarjojen %3$d jaksoa jätetään nauhoittamatta ristiriidan takia. + %1$d sarjan %2$s nauhoitus lisätty. Muiden sarjojen %3$d jaksoa jätetään nauhoittamatta ristiriidan takia. + "Tallennettua ohjelmaa ei löytynyt." "Aiheeseen liittyvät tallenteet" "(Ohjelmalla ei ole kuvausta.)" @@ -336,6 +335,7 @@ "Lopetetaanko sarjan tallennus?" "Tallennettuja osia voi edelleen katsella DVR:n kirjastossa." "Lopeta" + "Jaksoja ei ole tällä hetkellä saatavilla." "Yhtään jaksoa ei ole saatavilla.\nNe nauhoitetaan, kun ne ovat saatavilla." (%1$d minuuttia) diff --git a/res/values-fr-rCA/strings.xml b/res/values-fr-rCA/strings.xml index 1fc794e3..4b97eb8f 100644 --- a/res/values-fr-rCA/strings.xml +++ b/res/values-fr-rCA/strings.xml @@ -20,9 +20,8 @@ "mono" "stéréo" "Commandes de lecture" - "Chaînes récentes" + "Chaînes" "Options télé" - "Options IDI" "Les commandes de lecture ne sont pas disponibles pour cette chaîne" "Lire ou mettre en pause" "Avance rapide" @@ -35,33 +34,15 @@ "Sous-titres" "Mode d\'affichage" "IDI" - "Activé" - "Désactivé" "Multi-audio" "Plus de chaînes" "Paramètres" - "Source" - "Basculer" - "Activé" - "Désactivé" - "Son" - "Principale" - "Fenêtre IDI" - "Disposition" - "En bas à droite" - "En haut à droite" - "En haut à gauche" - "En bas à gauche" - "Côte à côte" - "Taille" - "Grande" - "Petite" - "Source de l\'entrée" "Télévision (antenne/câble)" "Aucune information sur le programme" "Pas d\'information" "Chaîne bloquée" - "Langue : indéterminée" + "Langue indéterminée" + "Sous-titres en %1$d" "Sous-titres" "Désactivé" "Personnaliser format" @@ -135,6 +116,10 @@ "Ce NIP est incorrect. Veuillez réessayer." "Le NIP est incorrect. Veuillez réessayer." + "Entrez votre code postal." + "L\'application Télé en direct utilisera le code postal pour vous présenter un guide complet des stations de télévision dans votre région." + "Entrez votre code postal" + "Code postal non valide" "Paramètres" "Personnaliser la liste chaînes" "Choisir des chaînes pour le guide des programmes" @@ -143,6 +128,7 @@ "Contrôles parentaux" "Licences de logiciels libres" "Licences de logiciels libres" + "Envoyer un commentaire" "Version" "Pour regarder cette chaîne, touchez la droite, puis entrez votre NIP." "Pour regarder ce programme, touchez la droite, puis entrez votre NIP." @@ -181,8 +167,6 @@ "Appuyez sur la touche ""Sélectionner"" pour accéder au menu Télévision." "Aucune entrée trouvée" "Entrée introuvable" - "Le mode Image dans image n\'est pas pris en charge" - "Aucune entrée ne pouvant être affichée en mode IDI" "Le type d\'entrée ne convient pas. Veuillez lancer l\'application Chaînes en direct pour une entrée de type syntoniseur." "Échec des réglages" "Aucune application pouvant gérer cette action n\'a été trouvée." @@ -259,8 +243,6 @@ "Enregistrer" "Les enregistrements ponctuels ont la plus haute priorité" "Annuler" - "Annuler" - "Supprimer" "Arrêter" "Voir le programme d\'enregistrement" "Uniquement ce programme" @@ -270,25 +252,29 @@ "Enregistrez celui-ci à la place" "Annuler cet enregistrement" "Regarder" + "Supprimer des enregistrements" "Enregistrable" "Enregistrement programmé" "Conflit d\'enregistrement" "Enregistrement en cours" "Échec de l\'enregistrement" "Lecture des programmes pour créer des horaires d\'enregistrement…" - "Lecture des programmes en cours…" - - + "Lecture des programmes en cours…" + "Afficher les enregistrements récents" + "L\'enregistrement de %1$s est incomplet." + "Les enregistrements de %1$s et %2$s sont incomplets." + "Les enregistrements de %1$s, %2$s et %3$s sont incomplets." + "L\'enregistrement de %1$s n\'a pas été terminé à cause d\'un espace de stockage insuffisant." + "L\'enregistrement de %1$s et %2$s n\'a pas été terminé à cause d\'un espace de stockage insuffisant." + "L\'enregistrement de %1$s, %2$s et %3$s n\'a pas été terminé à cause d\'un espace de stockage insuffisant." "Le magnétoscope numérique a besoin de plus d\'espace" "Vous pourrez enregistrer des programmes avec le magnétoscope numérique. Toutefois, l\'espace de stockage est insuffisant sur votre appareil pour que le magnétoscope numérique puisse fonctionner actuellement. Veuillez brancher un disque externe d\'au moins %1$s Go ou suivez les étapes pour le formater en tant qu\'espace de stockage de l\'appareil." + "Espace de stockage insuffisant" + "Ce programme ne sera pas enregistré, car l\'espace de stockage disponible est insuffisant. Essayez de supprimer certains enregistrements." "Espace de stockage manquant" - "Une partie de l\'espace de stockage utilisé par le magnétoscope numérique est manquante. Veuillez connecter de nouveau le disque externe que vous avez utilisé auparavant afin de réactiver le magnétoscope numérique. Vous pouvez également supprimer cet espace de stockage s\'il n\'est plus disponible." - "Supprimer l\'espace de stockage?" - "Tous vos contenus enregistrés et vos enregistrements planifiés seront perdus." "Arrêter l\'enregistrement?" "Le contenu enregistré sera gardé." - - + "L\'enregistrement de « %1$s » sera interrompu, car il crée un conflit avec ce programme. Le contenu enregistré sera conservé." "Enregistrement planifié, mais d\'autres enregistrements sont prévus en même temps." "L\'enregistrement a commencé, mais d\'autres enregistrements sont prévus en même temps" "Le programme « %1$s » sera enregistré." @@ -306,14 +292,27 @@ "Vous avez déjà planifié l\'enregistrement de ce programme à %1$s." "Déjà enregistré" "Ce programme a déjà été enregistré. Il est accessible dans la bibliothèque du magnétoscope numérique." - - - - - - - - + "L\'enregistrement de la série est programmé" + + %1$d enregistrement a été programmé pour %2$s. + %1$d enregistrements ont été programmés pour %2$s. + + + %1$d enregistrement a été programmé pour %2$s. L\'enregistrement (%3$d) ne sera pas effectué en raison de conflits. + %1$d enregistrements ont été programmés pour %2$s. %3$d d\'entre eux ne seront pas effectués en raison de conflits. + + + %1$d enregistrement a été programmé pour %2$s. %3$d épisodes de cette série et d\'une autre série ne seront pas enregistrés en raison de conflits. + %1$d enregistrements ont été programmés pour %2$s. %3$d épisodes de cette série et d\'une autre série ne seront pas enregistrés en raison de conflits. + + + %1$d enregistrement a été programmé pour %2$s. Un épisode d\'une autre série ne sera pas enregistré en raison de conflits. + %1$d enregistrements ont été programmés pour %2$s. Un épisode d\'une autre série ne sera pas enregistré en raison de conflits. + + + %1$d enregistrement a été programmé pour %2$s. %3$d épisodes d\'une autre série ne seront pas enregistrés en raison de conflits. + %1$d enregistrements ont été programmés pour %2$s. %3$d épisodes d\'une autre série ne seront pas enregistrés en raison de conflits. + "Programme enregistré non trouvé." "Enregistrements connexes" "(Programme sans description)" @@ -336,6 +335,7 @@ "Arrêter l\'enregistrement de la série?" "Les épisodes enregistrés resteront accessibles dans la bibliothèque du magnétoscope numérique." "Arrêter" + "Aucun épisode n\'est diffusé en ce moment." "Aucun épisode.\nLes épisodes seront enregistrés lorsqu\'ils seront diffusés." (%1$d minute) diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml index c140aa62..9192a821 100644 --- a/res/values-fr/strings.xml +++ b/res/values-fr/strings.xml @@ -20,9 +20,8 @@ "mono" "stéréo" "Commandes de lecture" - "Chaînes récentes" + "Chaînes" "Options TV" - "Options PIP" "Commandes de lecture indisponibles pour cette chaîne." "Lire ou mettre en pause" "Avance rapide" @@ -35,33 +34,15 @@ "Sous-titres" "Mode d\'affichage" "Mode PIP" - "Activé" - "Désactivé" "Multi-audio" "Plus de chaînes" "Paramètres" - "Source" - "Permuter" - "Activé" - "Désactivé" - "Son" - "Principale" - "Fenêtre mode PIP" - "Mise en page" - "En bas à droite" - "En haut à droite" - "En haut à gauche" - "En bas à gauche" - "Côte à côte" - "Taille" - "Grande" - "Petite" - "Source d\'entrée" "Téléviseur (antenne/câble)" "Aucune information sur le programme." "Aucune information" "Chaîne bloquée" - "Langue inconnue" + "Langue inconnue" + "Sous-titres %1$d" "Sous-titres" "Désactivé" "Personnaliser le formatage" @@ -135,6 +116,10 @@ "Ce code d\'accès est incorrect. Veuillez réessayer." "Le code est incorrect. Veuillez réessayer." + "Saisissez votre code postal." + "L\'application TV en direct utilise le code postal pour fournir un guide des programmes complet pour les chaînes de télévision." + "Saisissez votre code postal" + "Code postal incorrect" "Paramètres" "Personnaliser liste chaînes" "Choisir des chaînes pour le guide des programmes" @@ -143,6 +128,7 @@ "Contrôle parental" "Licences Open Source" "Licences Open Source" + "Envoyer des commentaires" "Version" "Pour regarder cette chaîne, appuyez sur le bouton droit, puis saisissez votre code d\'accès." "Pour regarder ce programme, appuyez sur la droite, puis saisissez votre code d\'accès." @@ -181,8 +167,6 @@ "Appuyez sur SÉLECTIONNER"" pour accéder au menu de la télévision." "Aucune entrée TV trouvée." "Entrée TV introuvable." - "Le mode PIP n\'est pas compatible." - "Aucune entrée disponible ne peut être affichée en mode PIP." "Type de tuner non adapté. Veuillez lancer l\'application Chaînes en direct pour l\'entrée TV de type tuner." "Échec des réglages." "Aucune application trouvée pour gérer cette action." @@ -259,8 +243,6 @@ "Enregistrer" "Les enregistrements ponctuels ont la plus haute priorité." "Annuler" - "Annuler" - "Supprimer" "Arrêter" "Voir planning d\'enregistrement" "Ce programme uniquement" @@ -270,25 +252,29 @@ "Enregistrer ce programme" "Annuler cet enregistrement" "Regarder" + "Supprimer des enregistrements" "Enregistrable" "Enregistrement programmé" "Conflit d\'enregistrement" "Enregistrement…" "Échec de l\'enregistrement" "Lecture des programmes pour créer des plannings d\'enregistrement…" - "Lecture des programmes…" - - + "Lecture des programmes…" + "Afficher les enregistrements récents" + "L\'enregistrement de \"%1$s\" est incomplet" + "Les enregistrements de \"%1$s\" et de \"%2$s\" sont incomplets" + "Les enregistrements de \"%1$s\", de \"%2$s\" et de \"%3$s\" sont incomplets" + "L\'enregistrement de \"%1$s\" n\'a pas été effectué en raison d\'un espace de stockage insuffisant." + "Les enregistrements de \"%1$s\" et de \"%2$s\" n\'ont pas été effectués en raison d\'un espace de stockage insuffisant." + "Les enregistrements de \"%1$s\", de \"%2$s\" et de \"%3$s\" n\'ont pas été effectués en raison d\'un espace de stockage insuffisant." "Le magnétoscope numérique a besoin de plus d\'espace" "Vous pourrez enregistrer des programmes avec le magnétoscope numérique. Toutefois, l\'espace de stockage est insuffisant sur votre appareil pour que le magnétoscope numérique puisse fonctionner actuellement. Veuillez brancher un disque externe d\'au moins %1$s Go ou suivre les étapes pour le formater en tant qu\'espace de stockage de l\'appareil." + "Espace de stockage insuffisant" + "Ce programme ne sera pas enregistré, car l\'espace de stockage disponible est insuffisant. Essayez de supprimer certains enregistrements." "Espace de stockage manquant" - "Une partie de l\'espace de stockage utilisé par le magnétoscope numérique est manquante. Veuillez connecter de nouveau le disque externe que vous avez utilisé auparavant afin de réactiver le magnétoscope numérique. Vous pouvez également supprimer cet espace de stockage s\'il n\'est plus disponible." - "Supprimer l\'espace de stockage ?" - "Tous vos contenus enregistrés et vos enregistrements planifiés seront perdus." "Arrêter l\'enregistrement ?" "Le contenu enregistré sera sauvegardé." - - + "L\'enregistrement de \"%1$s\" va être interrompu, car il crée un conflit avec ce programme. Le contenu enregistré sera conservé." "Enregistrement planifié, mais d\'autres enregistrements sont prévus en même temps." "L\'enregistrement a commencé, mais présente des conflits" "Le programme \"%1$s\" sera enregistré." @@ -306,14 +292,27 @@ "Vous avez déjà planifié l\'enregistrement de ce programme à %1$s." "Déjà enregistré" "Ce programme a déjà été enregistré. Il est disponible dans la bibliothèque DVR." - - - - - - - - + "Enregistrement de la série programmé" + + %1$d enregistrement a été programmé pour %2$s. + %1$d enregistrements ont été programmés pour %2$s. + + + %1$d enregistrement a été programmé pour %2$s. L\'enregistrement (%3$d) ne sera pas effectué en raison de conflits. + %1$d enregistrements ont été programmés pour %2$s. %3$d enregistrements ne seront pas effectués en raison de conflits. + + + %1$d enregistrement a été programmé pour %2$s. %3$d épisodes de cette série et d\'une autre série ne seront pas enregistrés en raison de conflits. + %1$d enregistrements ont été programmés pour %2$s. %3$d épisodes de cette série et d\'une autre série ne seront pas enregistrés en raison de conflits. + + + %1$d enregistrement a été programmé pour %2$s. 1 épisode d\'une autre série ne sera pas enregistré en raison de conflits. + %1$d enregistrements ont été programmés pour %2$s. 1 épisode d\'une autre série ne sera pas enregistré en raison de conflits. + + + %1$d enregistrement a été programmé pour %2$s. %3$d épisodes d\'une autre série ne seront pas enregistrés en raison de conflits. + %1$d enregistrements ont été programmés pour %2$s. %3$d épisodes d\'une autre série ne seront pas enregistrés en raison de conflits. + "Programme enregistré introuvable." "Enregistrements associés" "(Programme sans description)" @@ -336,6 +335,7 @@ "Arrêter l\'enregistrement de la série ?" "Les épisodes enregistrés resteront disponibles dans la bibliothèque DVR." "Arrêter" + "Aucun épisode n\'est diffusé actuellement." "Aucun épisode disponible.\nLes épisodes seront enregistrés lorsqu\'ils seront disponibles." (%1$d minute) diff --git a/res/values-gl-rES/strings.xml b/res/values-gl-rES/strings.xml index 7ce0ac2d..31c42390 100644 --- a/res/values-gl-rES/strings.xml +++ b/res/values-gl-rES/strings.xml @@ -20,9 +20,8 @@ "mono" "estéreo" "Controis de reprodución" - "Canles recentes" + "Canles" "Opcións de TV" - "Opcións de PIP" "Os controis de reprodución non están dispoñibles nesta canle" "Reprodución ou pausa" "Avance rápido" @@ -35,33 +34,15 @@ "Subtítulos" "Modo visualiz." "PIP" - "Activado" - "Desactivado" "Multiaudio" "Obter máis canles" "Configuración" - "Fonte" - "Cambiar" - "Activado" - "Desactivado" - "Son" - "Principal" - "Ventá de PIP" - "Deseño" - "Abaixo dereita" - "Arriba dereita" - "Arriba esquerda" - "Abaixo esquerda" - "En paralelo" - "Tamaño" - "Grande" - "Pequeno" - "Fonte de entrada" "TV (antena/cable)" "Non hai información do programa" "Non hai información" "Canle bloqueada" - "Idioma descoñecido" + "Idioma descoñecido" + "Subtítulos en %1$d" "Subtítulos" "Desactivado" "Personalizar formato" @@ -135,6 +116,10 @@ "О PIN era incorrecto. Téntao de novo." "Téntao de novo. O PIN non coincide." + "Introduce o teu código postal." + "A aplicación TV en directo usará o teu código postal para ofrecer unha guía de programas completa para as canles da televisión." + "Introduce o teu código postal" + "O código postal non é válido" "Configuración" "Personalizar lista de canles" "Selecciona canles para a túa guía de programas" @@ -143,6 +128,7 @@ "Controis parentais" "Licenzas de código aberto" "Licenzas de código aberto" + "Enviar comentarios" "Versión" "Para ver esta canle, preme na tecla cara á dereita e introduce o PIN" "Para ver esta programa, preme na tecla cara á dereita e introduce o PIN" @@ -181,8 +167,6 @@ "Preme SELECCIONAR"" para acceder ao menú da televisión." "Non se atopou ningunha entrada de televisión" "Non se pode atopar a entrada de televisión" - "PIP non é compatible" - "Non hai ningunha entrada que se poida mostrar con PIP" "Tipo de sintonizador non adecuado. Inicia a aplicación Canles en directo para a entrada de TV do tipo de sintonizador." "Erro de sintonización" "Non se encontrou ningunha aplicación para procesar esta acción." @@ -259,8 +243,6 @@ "Gardar" "As gravacións realizadas unha soa vez teñen máis prioridade" "Cancelar" - "Cancelar" - "Borrar" "Deter" "Ver programación de gravación" "Só este programa" @@ -270,25 +252,29 @@ "Gravar isto no seu lugar" "Cancelar esta gravación" "Visualizar agora" + "Eliminar gravacións..." "Gravable" "Gravación programada" "Conflito de gravación" "Gravando" "Erro na gravación" "Lendo programas para crear programacións de gravacións" - "Lendo programas" - - + "Lendo programas" + "Ver gravacións recentes" + "A gravación de %1$s non está completa." + "As gravacións de %1$s e %2$s non están completas." + "As gravacións de %1$s, %2$s e %3$s non están completas." + "A gravación de %1$s non se completou por falta de almacenamento suficiente." + "As gravacións de %1$s e %2$s non se completaron por falta de almacenamento suficiente." + "As gravacións de %1$s, %2$s e %3$s non se completaron por falta de almacenamento suficiente." "O DVR precisa máis almacenamento" "Poderás gravar programas con DVR. Non obstante, o teu dispositivo non ten almacenamento suficiente para que funcione DVR. Conecta unha unidade externa de %1$s GB ou máis e sigue os pasos para formatala como almacenamento do dispositivo." + "Non hai almacenamento suficiente" + "Este programa non se gravará porque non hai almacenamento suficiente. Proba a eliminar algunhas gravacións." "Falta o almacenamento" - "Falta parte do almacenamento que utiliza DVR. Conecta a unidade externa que utilizaches antes para volver activar DVR. Tamén podes decidir borrar o almacenamento se xa non está dispoñible." - "Queres borrar o almacenamento?" - "Perderase toda a programación e o contido gravados." "Queres deter a gravación?" "Gardarase o contido gravado." - - + "Deterase a gravación de %1$s porque supón un conflito con este programa. Gardarase o contido gravado." "Programouse a gravación, pero presenta conflitos" "Iniciouse a gravación pero presenta conflitos" "Gravarase %1$s." @@ -306,14 +292,27 @@ "Xa se programou a gravación do mesmo programa para a seguinte hora: %1$s." "Xa está gravado" "Este programa xa está gravado e está dispoñible na mediateca de DVR." - - - - - - - - + "Programouse a gravación da serie" + + Programáronse %1$d gravacións para %2$s. + Programouse %1$d gravación para %2$s. + + + Programáronse %1$d gravacións para %2$s. Non se gravarán %3$d delas por causa dun conflito. + Programouse %1$d gravación para %2$s. Non se gravará por causa dun conflito. + + + Programáronse %1$d gravacións para %2$s. Non se gravarán %3$d episodios destas series nin doutras por causa dun conflito. + Programouse %1$d gravación para %2$s. Non se gravarán %3$d episodios destas series nin doutras por causa dun conflito. + + + Programáronse %1$d gravacións para %2$s. Non se gravará 1 episodio doutras series por causa dun conflito. + Programouse %1$d gravación para %2$s. Non se gravará 1 episodio doutras series por causa dun conflito. + + + Programáronse %1$d gravacións para %2$s. Non se gravarán %3$d episodios doutras series por causa dun conflito. + Programouse %1$d gravación para %2$s. Non se gravarán %3$d episodios doutras series por causa dun conflito. + "Non se atopou o programa gravado." "Gravacións relacionadas" "(Non hai descrición do programa)" @@ -336,6 +335,7 @@ "Queres deter a gravación da serie?" "Os episodios gravados seguirán dispoñibles na mediateca de DVR." "Deter" + "Agora non hai episodios en directo." "Non hai episodios dispoñibles.\nGravaranse unha vez que estean dispoñibles." (%1$d minutos) diff --git a/res/values-hi/strings.xml b/res/values-hi/strings.xml index d6db8677..16178462 100644 --- a/res/values-hi/strings.xml +++ b/res/values-hi/strings.xml @@ -20,9 +20,8 @@ "मोनो" "स्टीरियो" "चलाने के नियंत्रण" - "हाल ही के चैनल" + "चैनल" "टीवी विकल्प" - "PIP विकल्‍प" "इस चैनल के लिए चलाने के नियंत्रण अनुपलब्‍ध हैं" "चलाएं या रोकें" "फ़ास्ट फ़ॉरवर्ड करें" @@ -35,33 +34,15 @@ "उपशीर्षक" "प्रदर्शन मोड" "PIP" - "चालू" - "बंद" "एकाधिक-ऑडियो" "अधिक चैनल पाएं" "सेटिंग" - "स्रोत" - "स्वैप करें" - "चालू" - "बंद" - "ध्वनि" - "मुख्‍य" - "PIP विंडो" - "लेआउट" - "नीचे दाएं" - "ऊपर दाएं" - "ऊपर बाएं" - "नीचे बाएं" - "साथ-साथ" - "आकार" - "बड़ा" - "छोटा" - "इनपुट स्रोत" "टीवी (एंटिना/केबल)" "कोई कार्यक्रम जानकारी नहीं" "कोई सूचना नहीं" "अवरोधित चैनल" - "अज्ञात भाषा" + "अज्ञात भाषा" + "बंद कैप्शन %1$d" "उपशीर्षक" "बंद" "प्रारूपण कस्टमाइज़ करें" @@ -135,6 +116,10 @@ "वह पिन गलत था. पुन: प्रयास करें." "फिर से प्रयास करें, पिन का मिलान नहीं हुआ" + "अपना ज़िप कोड डालें." + "लाइव चैनल ऐप्लिकेशन ज़िप कोड का उपयोग करके टीवी चैनल के लिए पूरी कार्यक्रम मार्गदर्शिका देगा." + "अपना ज़िप कोड डालें" + "अमान्य ज़िप कोड" "सेटिंग" "चैनल सूची कस्टमाइज़ करें" "कार्यक्रम मार्गदर्शिका के लिए चैनल चुनें" @@ -143,6 +128,7 @@ "अभिभावकीय नियंत्रण" "ओपन सोर्स लाइसेंस" "ओपन सोर्स लाइसेंस" + "फ़ीडबैक भेजें" "वर्शन" "इस चैनल को देखने के लिए, दाईं ओर दबाएं और अपना पिन डालें" "इस कार्यक्रम को देखने के लिए, दाईं ओर दबाएं और अपना पिन डालें" @@ -181,8 +167,6 @@ "टीवी मेनू ऐक्‍सेस करने के लिए ""चुनें"" दबाएं." "कोई टीवी इनपुट नहीं मिला" "टीवी इनपुट नहीं मिल पा रहा है" - "PIP समर्थित नहीं है" - "कोई उपलब्ध इनपुट नहीं है जिसे PIP के साथ दिखाया जा सके" "ट्यूनर प्रकार उपयुक्‍त नहीं है. कृपया ट्यूनर प्रकार टीवी इनपुट के लिए Live TV ऐप लॉन्‍च करें." "ट्यून विफल रहा" "यह कार्रवाई प्रबंधित करने के लिए कोई ऐप नहीं मिला." @@ -259,8 +243,6 @@ "सहेजें" "एक बार की रिकॉर्डिंग को उच्च प्राथमिकता दी जाती है" "रद्द करें" - "रद्द करें" - "भूल जाएं" "रोकें" "रिकॉर्डिंग शेड्यूल देखें" "यह एक ही कार्यक्रम" @@ -270,25 +252,29 @@ "उसके बजाय इसे रिकॉर्ड करें" "यह रिकॉर्डिंग रद्द करें" "अभी देखें" + "रिकॉर्डिंग हटाएं…" "रिकॉर्ड करने योग्य" "रिकॉर्डिंग शेड्यूल की गई" "रिकॉर्डिंग संबंधी विरोध" "रिकॉर्ड हो रहा है" "रिकॉर्डिंग विफल रही" "रिकॉर्डिंग शेड्यूल बनाने के लिए कार्यक्रम पढ़े जा रहे हैं" - "प्रोग्राम पढ़े जा रहे हैं" - - + "कार्यक्रम पढ़े जा रहे हैं" + "हाल ही की रिकॉर्डिंग देखें" + "%1$s की रिकॉर्डिंग अधूरी है." + "%1$s और %2$s की रिकॉर्डिंग अधूरी है." + "%1$s, %2$s और %3$s की रिकॉर्डिंग अधूरी है." + "पर्याप्त जगह नहीं होने के कारण %1$s की रिकॉर्डिंग पूरी नहीं हुई." + "पर्याप्त जगह नहीं होने के कारण %1$s और %2$s की रिकॉर्डिंग पूरी नहीं हुई." + "पर्याप्त जगह नहीं होने के कारण %1$s, %2$s और %3$s की रिकॉर्डिंग पूरी नहीं हुई." "DVR को अधिक जगह की आवश्यकता है" "आप DVR से प्रोग्राम रिकॉर्ड कर पाएंगे. हालांकि इस समय आपके डिवाइस पर DVR के काम करने के लिए पर्याप्त जगह नहीं है. कृपया एक बाहरी डिवाइस कनेक्ट करें जिसमें %1$s GB या उससे अधिक जगह हो और उसे डिवाइस जगह के रूप में फ़ॉर्मेट करने के चरणों का अनुसरण करें." + "पर्याप्‍त जगह नहीं है" + "इस कार्यक्रम को रिकॉर्ड नहीं किया जाएगा क्योंकि पर्याप्त जगह उपलब्ध नहीं है. कुछ मौजूदा रिकॉर्डिंग हटाकर देखें." "जगह मिल नहीं रही है" - "DVR द्वारा उपयोग की गई कुछ जगह मिल नहीं रही है. कृपया DVR को दोबारा सक्षम करने से पहले अपनी उपयोग की हुई बाहरी डिस्क कनेक्ट करें. वैकल्पिक रूप से, यदि जगह अब उपलब्ध नहीं है तो आप उसे भूल जाना चुन सकते हैं." - "जगह को भूल जाएं?" - "आपकी रिकॉर्ड की हुई सभी सामग्री और शेड्यूल खो जाएंगे." "रिकॉर्डिंग बंद करें?" "रिकॉर्ड की गई सामग्री सहेज ली जाएगी." - - + "%1$s की रिकॉर्डिंग रोक दी जाएगी क्योंकि यह इस प्रोग्राम का विरोध करता है. रिकॉर्ड की गई सामग्री सहेज ली जाएगी." "रिकॉर्डिंग शेड्यूल की गई लेकिन विरोध मौजूद हैं" "रिकॉर्डिंग शुरू हो गई है लेकिन उसमें विरोध मौजूद हैं" "%1$s रिकॉर्ड किया जाएगा." @@ -306,14 +292,27 @@ "इसी कार्यक्रम को %1$s बजे रिकॉर्ड करने के लिए पहले ही शेड्यूल किया जा चुका है." "पहले ही रिकॉर्ड हो चुका है" "यह कार्यक्रम पहले ही रिकॉर्ड हो चुका है. वह DVR लाइब्रेरी में उपलब्ध है." - - - - - - - - + "सीरीज़ रिकॉर्डिंग शेड्यूल की गई" + + %2$s के लिए %1$d रिकॉर्डिंग शेड्यूल कर दी गई हैं. + %2$s के लिए %1$d रिकॉर्डिंग शेड्यूल कर दी गई हैं. + + + %2$s के लिए %1$d रिकॉर्डिंग शेड्यूल कर दी गई हैं. अन्य शेड्यूल से विरोधों के कारण उनमें से %3$d को रिकॉर्ड नहीं किया जाएगा. + %2$s के लिए %1$d रिकॉर्डिंग शेड्यूल कर दी गई हैं. अन्य शेड्यूल से विरोधों के कारण उनमें से %3$d को रिकॉर्ड नहीं किया जाएगा. + + + %2$s के लिए %1$d रिकॉर्डिंग शेड्यूल कर दी गई हैं. अन्य शेड्यूल और दूसरे सीरीज़ के एपिसोड से विरोधों के कारण इस सीरीज़ और अन्य सीरीज़ के %3$d एपिसोड रिकॉर्ड नहीं किए जाएंगे. + %2$s के लिए %1$d रिकॉर्डिंग शेड्यूल कर दी गई हैं. अन्य शेड्यूल और दूसरे सीरीज़ के एपिसोड से विरोधों के कारण इस सीरीज़ और अन्य सीरीज़ के %3$d एपिसोड रिकॉर्ड नहीं किए जाएंगे. + + + %2$s के लिए %1$d रिकॉर्डिंग शेड्यूल कर दी गई हैं. इस सीरीज़ से विरोधों के कारण अन्य सीरीज़ का 1 एपिसोड रिकॉर्ड नहीं किया जाएगा. + %2$s के लिए %1$d रिकॉर्डिंग शेड्यूल कर दी गई हैं. इस सीरीज़ से विरोधों के कारण अन्य सीरीज़ का 1 एपिसोड रिकॉर्ड नहीं किया जाएगा. + + + %2$s के लिए %1$d रिकॉर्डिंग शेड्यूल कर दी गई हैं. इस सीरीज़ से विरोधों के कारण अन्य सीरीज़ के %3$d एपिसोड रिकॉर्ड नहीं किए जाएंगे. + %2$s के लिए %1$d रिकॉर्डिंग शेड्यूल कर दी गई हैं. इस सीरीज़ से विरोधों के कारण अन्य सीरीज़ के %3$d एपिसोड रिकॉर्ड नहीं किए जाएंगे. + "रिकॉर्ड किया गया प्रोग्राम नहीं मिला." "संबंधित रिकॉर्डिंग" "(कोई कार्यक्रम वर्णन नहीं)" @@ -336,6 +335,7 @@ "श्रृंखला की रिकॉर्डिंग रोकें?" "रिकॉर्ड किए गए एपिसोड DVR लाइब्रेरी में उपलब्ध रहेंगे." "रोकें" + "इस समय कोई भी एपिसोड प्रसारित नहीं हो रहा है." "कोई भी एपिसोड उपलब्ध नहीं है.\nएपिसोड उपलब्ध होने पर उन्हें रिकॉर्ड कर लिया जाएगा." (%1$d मिनट) diff --git a/res/values-hr/strings.xml b/res/values-hr/strings.xml index 596a3f96..2bd8328e 100644 --- a/res/values-hr/strings.xml +++ b/res/values-hr/strings.xml @@ -20,9 +20,8 @@ "mono" "stereo" "Kontrole reprodukcije" - "Nedavni kanali" + "Kanali" "TV opcije" - "Opcije PIP-a" "Kontrole reprodukcije nisu dostupne za ovaj kanal" "Reprodukcija ili pauziranje" "Brzo unaprijed" @@ -35,33 +34,15 @@ "Titlovi" "Način prikaza" "PIP" - "Uključeno" - "Isključeno" "Multiaudio" "Više kanala" "Postavke" - "Izvor" - "Zamijeni" - "Uključeno" - "Isključeno" - "Zvuk" - "Glavni" - "Prozor PIP-a" - "Raspored" - "Pri dnu desno" - "Pri vrhu desno" - "Pri vrhu lijevo" - "Pri dnu lijevo" - "Jedno uz drugo" - "Veličina" - "Velik" - "Malen" - "Izvor ulaza" "TV (antena/kabel)" "Nema podataka o programu" "Nema informacija" "Blokirani kanal" - "Nepoznati jezik" + "Nepoznati jezik" + "Titlovi %1$d" "Titlovi" "Isključeno" "Prilagodi format" @@ -137,6 +118,10 @@ "PIN je pogrešan. Pokušajte ponovno." "Pokušaj ponovo, PIN se ne podudara" + "Unesite svoj poštanski broj." + "Na temelju poštanskog broja aplikacija TV kanali uživo pružit će vam potpun programski vodič za TV kanale." + "Unesite svoj poštanski broj" + "Poštanski broj nije važeći" "Postavke" "Prilagodite popis kanala" "Odaberite kanale za programski vodič" @@ -145,6 +130,7 @@ "Roditeljski nadzor" "Licence otvorenog izvornog koda" "Licence otvorenog izvornog koda" + "Pošaljite povratne informacije" "Verzija" "Da biste gledali ovaj kanal, pritisnite desno pa unesite PIN" "Da biste gledali ovaj program, pritisnite desno pa unesite PIN" @@ -185,8 +171,6 @@ "Pritisnite ODABERITE"" da biste pristupili izborniku TV-a." "TV ulaz nije pronađen" "TV ulaz nije pronađen" - "PIP nije podržan" - "Nema dostupnog ulaza koji se može prikazati putem PIP-a" "Vrsta prijemnika nije prikladna. Pokrenite aplikaciju Kanali uživo ako želite koristiti prijemnik kao TV ulaz." "Traženje kanala nije uspjelo" "Nije pronađena nijedna aplikacija koja može provesti tu radnju." @@ -269,8 +253,6 @@ "Spremi" "Jednokratna snimanja imaju najviši prioritet" "Odustani" - "Odustani" - "Zaboravi" "Zaustavi" "Prikaz rasporeda snimanja" "Samo jedna epizoda" @@ -280,25 +262,29 @@ "Snimi ovo" "Otkaži snimanje" "Pogledajte odmah" + "Brisanje snimki…" "Snimanje moguće" "Snimanje programirano" "Sukob rasporeda snimanja" "Snimanje" "Snimanje nije uspjelo" "Čitanje emisija za izradu rasporeda snimanja" - "Čitanje emisija" - - + "Čitanje emisija" + "Prikaži nedavne snimke" + "Snimka sadržaja %1$s nije dovršena." + "Snimke sadržaja %1$s i %2$s nisu dovršene." + "Snimke sadržaja %1$s, %2$s i %3$s nisu dovršene." + "Snimanje sadržaja %1$s nije dovršeno jer nema dovoljno mjesta u pohrani." + "Snimanje sadržaja %1$s i %2$s nije dovršeno jer nema dovoljno mjesta u pohrani." + "Snimanje sadržaja %1$s, %2$s i %3$s nije dovršeno jer nema dovoljno mjesta u pohrani." "DVR treba više prostora za pohranu" "Moći ćete snimati programe DVR-om. No na vašem uređaju trenutačno nema dovoljno prostora za pohranu da bi DVR funkcionirao. Priključite vanjski disk od najmanje %1$s GB i formatirajte ga kao pohranu uređaja prema uputama." + "Nema dovoljno prostora za pohranu" + "Emisija se neće snimiti jer nema dovoljno prostora za pohranu. Izbrišite neke snimljene emisije." "Pohrana nedostaje" - "Nedostaje dio pohrane kojom se koristi DVR. Da biste ponovo omogućili DVR, povežite ga s vanjskim diskom koji ste upotrebljavali prethodno. Ako ta pohrana više nije dostupna, možete odabrati da je uređaj zaboravi." - "Zaboraviti pohranu?" - "Izgubit ćete sve snimljene sadržaje i programirana snimanja." "Želite li zaustaviti snimanje?" "Snimljeni će se sadržaj spremiti." - - + "Snimanje sadržaja %1$s zaustavit će se zbog sukoba s ovim programom. Snimljeni sadržaj ostat će spremljen." "Snimanje je programirano, ali ima sukoba" "Snimanje je započelo, ali ima sukoba" "Snimit će se %1$s." @@ -317,14 +303,32 @@ "Snimanje tog programa već je programirano za %1$s." "Već snimljeno" "Taj je program već snimljen. Dostupan je u zbirci DVR-a." - - - - - - - - + "Snimanje serije je zakazano" + + %1$d snimanje zakazano je za seriju %2$s. + %1$d snimanja zakazana su za seriju %2$s. + %1$d snimanja zakazano je za seriju %2$s. + + + %1$d snimanje zakazano je za seriju %2$s. %3$d od njih neće se snimiti zbog sukoba. + %1$d snimanja zakazana su za seriju %2$s. %3$d od njih neće se snimiti zbog sukoba. + %1$d snimanja zakazano je za seriju %2$s. %3$d od njih neće se snimiti zbog sukoba. + + + %1$d snimanje zakazano je za seriju %2$s. %3$d epizoda/e te serije i druge serije neće se snimiti zbog sukoba. + %1$d snimanja zakazana su za seriju %2$s. %3$d epizoda/e te serije i druge serije neće se snimiti zbog sukoba. + %1$d snimanja zakazano je za seriju %2$s. %3$d epizoda/e te serije i druge serije neće se snimiti zbog sukoba. + + + %1$d snimanje zakazano je za seriju %2$s. 1 epizoda neke druge serije neće se snimiti zbog sukoba. + %1$d snimanja zakazana su za seriju %2$s. 1 epizoda neke druge serije neće se snimiti zbog sukoba. + %1$d snimanja zakazano je za seriju %2$s. 1 epizoda neke druge serije neće se snimiti zbog sukoba. + + + %1$d snimanje zakazano je za seriju %2$s. %3$d epizoda/e neke druge serije neće se snimiti zbog sukoba. + %1$d snimanja zakazana su za seriju %2$s. %3$d epizoda/e neke druge serije neće se snimiti zbog sukoba. + %1$d snimanja zakazano je za seriju %2$s. %3$d epizoda/e neke druge serije neće se snimiti zbog sukoba. + "Snimljeni program nije pronađen." "Povezane snimke" "(Nema opisa programa)" @@ -349,6 +353,7 @@ "Želite li zaustaviti snimanje serije?" "Snimljene epizode ostat će dostupne u zbirci DVR-a." "Zaustavi" + "Trenutačno se ne emitira nijedna epizoda." "Nije dostupna nijedna epizoda.\nEpizode će se snimiti kada budu dostupne." (%1$d minuta) diff --git a/res/values-hu/strings.xml b/res/values-hu/strings.xml index d007a135..7f8b700d 100644 --- a/res/values-hu/strings.xml +++ b/res/values-hu/strings.xml @@ -20,9 +20,8 @@ "monó" "sztereó" "Lejátszásvezérlők" - "Legutóbbiak" + "Csatornák" "Tv beállításai" - "PIP-beállítások" "A lejátszási vezérlők nem érhetők el ennél a csatornánál" "Lejátszás vagy szüneteltetés" "Előretekerés" @@ -35,33 +34,15 @@ "Feliratok" "Megjelenítés" "PIP" - "Bekapcsolva" - "Kikapcsolva" "Több hangsáv" "További csatornák" "Beállítások" - "Forrás" - "Csere" - "Bekapcsolva" - "Kikapcsolva" - "Hang" - "Elsődleges" - "PIP-ablak" - "Elrendezés" - "Lent jobbra" - "Fent jobbra" - "Fent balra" - "Lent balra" - "Egymás mellett" - "Méret" - "Nagy" - "Kicsi" - "Bemeneti forrás" "TV (antenna/kábel)" "Nincsenek műsorinformációk" "Nincs információ." "Letiltott csatorna" - "Ismeretlen nyelv" + "Ismeretlen nyelv" + "Feliratok (%1$d)" "Feliratok" "Kikapcsolva" "Személyre szabás" @@ -135,6 +116,10 @@ "A PIN-kód helytelen, próbálja újra." "A PIN-kód nem egyezik, próbálja újra" + "Irányítószám megadása" + "Az Élő Csatornák alkalmazás az irányítószám használatával megjeleníti a csatornák teljes tévéműsorát." + "Adja meg irányítószámát" + "Érvénytelen irányítószám" "Beállítások" "Csatornalista testreszabása" "Válasszon csatornákat a műsorfüzetbe" @@ -143,6 +128,7 @@ "Szülői felügyelet" "Nyílt forráskódú licencek" "Nyílt forráskódú licencek" + "Visszajelzés küldése" "Verzió" "A csatorna megtekintéséhez nyomja meg a jobbra gombot, majd adja meg a PIN kódot" "A műsor megtekintéséhez nyomja meg a jobbra gombot, majd adja meg a PIN kódot" @@ -181,8 +167,6 @@ "Nyomja meg a KIVÁLASZTÁS"" gombot a tévé menüjének eléréséhez." "Nem található tévébemenet." "A tévébemenet nem található." - "A kép a képben funkció nem támogatott." - "Nincs elérhető bemeneti adás, amely megjeleníthető lenne a kép a képben (picture in picture, PIP) funkcióval." "A tuner típusa nem megfelelő. Indítsa el a tévé bemeneti tunertípusának megfelelő Élő csatornák alkalmazást." "Tunerhiba" "Nincs megfelelő alkalmazás a művelet végrehajtásához." @@ -259,8 +243,6 @@ "Mentés" "Az egyszeri rögzítések rendelkeznek a legmagasabb prioritással" "Mégse" - "Mégse" - "Elfelejt" "Leállítás" "Rögzítési ütemterv megtekintése" "Ezt az egy műsort" @@ -270,25 +252,29 @@ "Inkább ez legyen rögzítve" "A rögzítés törlése" "Nézze meg most" + "Felvételek törlése…" "Felvehető" "A felvétel beállítva" "Ütközés más felvétellel" "Rögzítés" "A felvétel nem sikerült" "Műsorok beolvasása a rögzítési ütemterv kialakításához" - "Műsorok beolvasása" - - + "Műsorok beolvasása" + "A legutóbbi felvételek megtekintése" + "A(z) %1$s felvétele megszakadt." + "A következők felvétele megszakadt: %1$s és %2$s." + "A következők felvétele megszakadt: %1$s, %2$s és %3$s." + "A(z) %1$s felvétele nem sikerült, mert nincs elég tárhely." + "A következők felvétele a kevés tárhely miatt nem sikerült: %1$s és %2$s." + "A következők felvétele a kevés tárhely miatt nem sikerült: %1$s, %2$s és %3$s." "A DVR számára több tárhely szükséges" "A DVR segítségével műsorokat vehet fel. Azonban eszközén nincs elég szabad tárhely a DVR működéséhez. Csatlakoztasson egy legalább %1$s GB tárhellyel rendelkező külső meghajtót, majd kövesse az utasításokat, hogy az eszköz tárhelyévé formázhassa." + "Nincs elegendő tárhely" + "Ez a műsor nem lesz felvéve, mivel nincs elegendő tárhely. Próbáljon meg törölni néhány meglévő felvételt." "Hiányzó tárhely" - "A DVR által használt tárhely bizonyos része hiányzik. Kérjük, csatlakoztassa a korábban használt külső meghajtót a DVR ismételt engedélyezéséhez. Másik megoldásként megadhatja a rendszernek, hogy ne emlékezzen többé erre a tárhelyre, ha már nem áll a rendelkezésére." - "A rendszer ne emlékezzen többé erre a tárhelyre?" - "Az összes rögzített tartalom és ütemterv elvész." "Leállítja a rögzítést?" "A rögzített tartalmat elmenti a rendszer." - - + "A(z) %1$s felvétele le fog állni, mert ütközik ezzel a programmal. A felvett tartalmat az alkalmazás menteni fogja." "A rögzítést beállította, de az ütközik más műsorokkal" "A rögzítés elindult, de az ütközik más műsorokkal" "A(z) %1$s rögzítve lesz." @@ -306,14 +292,27 @@ "Ugyanennek a műsornak a rögzítése már be van ütemezve a következő időpontban: %1$s." "Már készült róla felvétel" "Erről a műsorról már készült felvétel, amely a DVR könyvtárban található." - - - - - - - - + "Sorozatfelvétel ütemezve" + + %1$d felvétel van ütemezve a következőhöz: %2$s. + %1$d felvétel van ütemezve a következőhöz: %2$s. + + + %1$d felvétel van ütemezve a következőhöz: %2$s. Ütközés miatt a rendszer nem rögzít %3$d felvételt. + %1$d felvétel van ütemezve a következőhöz: %2$s. Ütközés miatt a rendszer nem rögzíti a felvételt. + + + %1$d felvétel van ütemezve a következőhöz: %2$s. Ütközés miatt a rendszer %3$d epizódot nem rögzít ebből, valamint más sorozatokból. + %1$d felvétel van ütemezve a következőhöz: %2$s. Ütközés miatt a rendszer %3$d epizódot nem rögzít ebből, valamint más sorozatokból. + + + %1$d felvétel van ütemezve a következőhöz: %2$s. Ütközés miatt a rendszer 1 epizódot nem rögzít más sorozatokból. + %1$d felvétel van ütemezve a következőhöz: %2$s. Ütközés miatt a rendszer 1 epizódot nem rögzít más sorozatokból. + + + %1$d felvétel van ütemezve a következőhöz: %2$s. Ütközés miatt a rendszer %3$d epizódot nem rögzít más sorozatokból. + %1$d felvétel van ütemezve a következőhöz: %2$s. Ütközés miatt a rendszer %3$d epizódot nem rögzít más sorozatokból. + "A rögzített program nem található." "Kapcsolódó felvételek" "(Nincs programleírás)" @@ -336,6 +335,7 @@ "Leállítja a sorozat rögzítését?" "A rögzített részek továbbra is hozzáférhetők lesznek a DVR könyvtárban." "Leállítás" + "Egyetlen epizódot sem közvetítenek jelenleg." "Nincs rendelkezésre álló epizód.\nAz epizódok a megjelenésüket követően lesznek rögzítve." (%1$d perc) diff --git a/res/values-hy-rAM/strings.xml b/res/values-hy-rAM/strings.xml index d10652e1..8ed0fb5e 100644 --- a/res/values-hy-rAM/strings.xml +++ b/res/values-hy-rAM/strings.xml @@ -20,9 +20,8 @@ "մոնո" "ստերեո" "Նվագարկման կառավար" - "Վերջինները" + "Հեռուստաալիքներ" "Հեռ. ընտրանքներ" - "PIP ընտրանքներ" "Նվագարկման կառավարներն այս ալիքում անհասանելի են" "Նվագարկել կամ դադարեցնել" "Արագ առաջանցում" @@ -35,33 +34,15 @@ "Փակ խորագրեր" "Ցուցադրման ռեժիմ" "PIP" - "Միացված է" - "Անջատված է" "Բազմաուդիո" "Ավելացնել ալիքներ" "Կարգավորումներ" - "Աղբյուր" - "Փոխել" - "Միացված է" - "Անջատված է" - "Ձայն" - "Հիմնական" - "PIP պատուհան" - "Դասավորություն" - "Ստորին աջ" - "Վերին աջ" - "Վերին ձախ" - "Ստորին ձախ" - "Կողք կողքի" - "Չափը" - "Մեծ" - "Փոքր" - "Աղբյուր" "Հեռուստացույց (ալեհավաք/մալուխ)" "Ծրագրի մասին տեղեկություններ չկան" "Տեղեկություն չկա" "Ալիքն արգելափակված է:" - "Անհայտ լեզու" + "Անհայտ լեզու" + "%1$d ենթագրեր" "Փակ ենթագրեր" "Անջատված է" "Հարմարացնել ձևաչափը" @@ -135,6 +116,10 @@ "PIN-ը սխալ էր: Կրկին փորձեք:" "Փորձեք կրկին, PIN-ը չի համապատասխանում" + "Մուտքագրեք փոստային դասիչը:" + "Ուղիղ եթեր հավելվածը փոստային դասիչը կօգտագործի հեռուստաալիքների ամբողջական ցանկը տրամադրելու համար։" + "Մուտքագրեք փոստային դասիչը" + "Փոստային դասիչը սխալ է" "Կարգավորումներ" "Հարմարեցնել ալիքների ցանկը" "Ընտրեք ալիքներ հեռուստահաղորդումների ծրագրի համար" @@ -143,6 +128,7 @@ "Ծնողական վերահսկողություն" "Բաց կոդով ծրագրակազմի արտոնագրեր" "Բաց կոդով ծրագրաշարի լիցենզիաներ" + "Կարծիք հայտնել" "Տարբերակ" "Այս կապուղին դիտելու համար սեղմեք Աջ և մուտքագրեք ձեր PIN-ը" "Այս ծրագիրը դիտելու համար սեղմեք Աջ և մուտքագրեք ձեր PIN-ը" @@ -181,8 +167,6 @@ "Հեռուստացույցի ընտրացանկից օգտվելու համար ""սեղմեք ԸՆՏՐԵԼ"":" "Հեռուստացույցի մուտք չի գտնվել" "Հեռուստացույցի մուտքը տեղադրված չէ" - "PIP-ը չի աջակցվում" - "PIP-ը ցուցադրելու համար աղբյուր չկա" "Կարգավորիչի մուտքը համապատասխան չէ: Գործարկեք «Ուղիղ եթեր» հավելվածը, եթե օգտագործում եք հեռուստացույցի մուտք:" "Կարգավորումը չհաջողվեց" "Այս գործողությունը կատարելու համար ոչ մի հավելված չի գտնվել" @@ -259,8 +243,6 @@ "Պահել" "Միանգամյա տեսագրումն ունի ամենաբարձր առաջնահերթությունը" "Չեղարկել" - "Չեղարկել" - "Մոռանալ" "Դադար" "Դիտել տեսագրման ժամանակացույցը" "Միայն այս ծրագիրը" @@ -270,25 +252,29 @@ "Փոխարենը տեսագրել այս ծրագիրը" "Չեղարկել այս տեսագրումը" "Դիտել հիմա" + "Ջնջել տեսագրությունները…" "Հնարավոր է տեսագրել" "Տեսագրումը ծրագրավորված է" "Տեսագրման հակասություն" "Տեսագրում" "Չհաջողվեց տեսագրել" "Ծրագրերի ընթերցում՝ տեսագրության ժամանակացույցեր ստեղծելու համար" - "Ծրագրերի ընթերցում" - - + "Ծրագրերի ընթերցում" + "Դիտել վերջին տեսագրությունները" + "%1$s ծրագրի տեսագրումը չի ավարտվել:" + "%1$s և %2$s ծրագրերի տեսագրումը չի ավարտվել:" + "%1$s, %2$s և %3$s ծրագրերի տեսագրումը չի ավարտվել:" + "%1$s ծրագրի տեսագրումը չի ավարտվել, քանի որ հիշողությունը բավարար չէ:" + "%1$s և %2$s ծրագրերի տեսագրումը չի ավարտվել, քանի որ հիշողությունը բավարար չէ:" + "%1$s, %2$s և %3$s ծրագրերի տեսագրումը չի ավարտվել, քանի որ հիշողությունը բավարար չէ:" "DVR-ին ավելի շատ հիշողություն է անհրաժեշտ" "Դուք կկարողանաք տեսագրել ծրագրեր DVR-ի օգնությամբ: Սակայն այս պահին ձեր սարքում DVR-ի աշխատանքի համար անհրաժեշտ բավականաչափ հիշողություն չկա: Միացեք առնվազն %1$sԳԲ հիշողություն ունեցող արտաքին սարք և հետևեք ցուցումներին՝ այն որպես սարքի հիշողություն ձևաչափելու համար:" + "Բավականաչափ հիշողություն չկա" + "Այս ծրագիրը չի տեսագրվի բավականաչափ հիշողություն բացակայության պատճառով: Փորձեք ջնջել առկա տեսագրություններից մի քանիսը:" "Հիշողությունն անհասանելի է" - "DVR-ի կողմից օգտագործվող հիշողության մի մասն անհասանելի է: DVR-ը կրկին ակտիվացնելու համար միացրեք նախկինում օգտագործված արտաքին սարքը: Կարող եք նաև մոռանալ հիշողությունը, եթե այն այևս հասանելի չէ:" - "Մոռանա՞լ հիշողությունը:" - "Ձեր տեսագրած ամբողջ բովանդակությունը և ժամանակացույցները չեն պահվի:" "Դադարեցնե՞լ տեսագրումը:" "Տեսագրված բովանդակությունը կպահվի:" - - + "%1$s ծրագրի տեսագրումը կդադարեցվի, քանի որ այն ունի հակասություններ այս ծրագրի հետ: Արդեն իսկ տեսագրված բովանդակությունը կպահվի:" "Տեսագրումը ծրագրավորվեց, սակայն այն հակասություններ ունի" "Տեսագրումը սկսվել է, սակայն որոշ հակասություններ ունի" "%1$s ծրագիրը կտեսագրվի:" @@ -306,14 +292,27 @@ "Միևնույն ծրագրի տեսագրումն արդեն ծրագրավորվել է %1$s-ին:" "Արդեն տեսագրվել է" "Այս ծրագիրն արդեն տեսագրվել է: Այն հասանելի է DVR դարանում:" - - - - - - - - + "Սերիալի տեսագրումը ծրագրավորված է" + + %1$d recordings have been scheduled for %2$s. + %2$s-ի համար ծրագրավորված է %1$d տեսագրում: + + + %1$d recordings have been scheduled for %2$s. %3$d of them will not be recorded due to conflicts. + %2$s-ի համար ծրագրավորված է %1$d տեսագրում: Այդ տեսագրումներից %3$d-ը հակասությունների պատճառով չեն կատարվի: + + + %1$d recordings have been scheduled for %2$s. %3$d episodes of this series and other series will not be recorded due to conflicts. + %2$s-ի համար ծրագրավորված է %1$d տեսագրում: Այս սերիալի %3$d դրվագներ և մյուս սերիալը հակասությունների պատճառով չեն տեսագրվի: + + + %1$d recordings have been scheduled for %2$s. 1 episode of other series will not be recorded due to conflicts. + %2$s-ի համար ծրագրավորված է %1$d տեսագրում: Մյուս սերիալի 1 դրվագ հակասությունների պատճառով չի տեսագրվի: + + + %1$d recordings have been scheduled for %2$s. %3$d episodes of other series will not be recorded due to conflicts. + %2$s-ի համար ծրագրավորված է %1$d տեսագրում: Մյուս սերիալի %3$d դրվագներ հակասությունների պատճառով չեն տեսագրվի: + "Տեսագրված ծրագիրը չի գտնվել:" "Առնչվող տեսագրություններ" "(Ծրագիրը նկարագրություն չունի)" @@ -336,6 +335,7 @@ "Դադարեցնե՞լ սերիալի տեսագրումը:" "Տեսագրված դրվագները հասանելի կմնան միայն DVR դարանում:" "Դադար" + "Այս պահին եթեր հեռարձակվող դրվագներ չկան:" "Հասանելի դրվագներ չկան:\nԴրանք կտեսագրվեն հասանելի դառնալուց հետո:" (%1$d minutes) diff --git a/res/values-in/strings.xml b/res/values-in/strings.xml index 662cfc89..1822bd1f 100644 --- a/res/values-in/strings.xml +++ b/res/values-in/strings.xml @@ -20,9 +20,8 @@ "mono" "stereo" "Kontrol pemutar" - "Saluran terkini" + "Saluran" "Opsi TV" - "Opsi PIP" "Kontrol Play tidak tersedia untuk saluran ini" "Putar atau jeda" "Maju cepat" @@ -35,33 +34,15 @@ "Teks" "Mode tayang" "PIP" - "Aktif" - "Nonaktif" "Multi-audio" "Dapatkan saluran lain" "Setelan" - "Sumber" - "Tukar" - "Aktif" - "Nonaktif" - "Suara" - "Utama" - "Jendela PIP" - "Tata letak" - "Kanan bawah" - "Kanan atas" - "Kiri atas" - "Kiri bawah" - "Berdampingan" - "Ukuran" - "Besar" - "Kecil" - "Sumber masukan" "TV (antena/kabel)" "Tidak ada informasi program" "Tidak ada informasi" "Saluran yang diblokir" - "Bahasa tidak dikenal" + "Bahasa tidak dikenal" + "Subtitel, CC %1$d" "Teks" "Nonaktif" "Sesuaikan format" @@ -135,6 +116,10 @@ "PIN salah. Coba lagi." "Pin tidak cocok, coba lagi" + "Memasukkan Kode Pos" + "Aplikasi Live TV akan menggunakan Kode Pos untuk memberikan panduan program lengkap bagi saluran TV." + "Masukkan Kode Pos" + "Kode ZIP Tidak Valid" "Setelan" "Sesuaikan daftar saluran" "Pilih saluran untuk panduan program" @@ -143,6 +128,7 @@ "Kontrol induk" "Lisensi sumber terbuka" "Lisensi sumber terbuka" + "Kirim masukan" "Versi" "Untuk menonton saluran ini, tekan Kanan dan masukkan PIN" "Untuk menonton program ini, tekan Kanan dan masukkan PIN" @@ -181,8 +167,6 @@ "Tekan PILIH"" untuk mengakses menu TV." "Tidak ditemukan masukan TV" "Tidak dapat menemukan masukan TV" - "PIP tidak didukung" - "Tidak tersedia masukan yang dapat ditampilkan dengan PIP" "Jenis tuner tidak cocok. Luncurkan aplikasi Saluran Siaran Langsung untuk masukan TV yang sesuai dengan jenis tuner" "Penalaan gagal" "Tidak ditemukan aplikasi untuk menangani tindakan ini." @@ -259,8 +243,6 @@ "Simpan" "Perekaman satu kali memiliki prioritas tertinggi" "Batal" - "Batal" - "Lupakan" "Berhenti" "Lihat jadwal rekaman" "Program tunggal ini" @@ -270,25 +252,29 @@ "Rekam yang ini saja" "Batalkan rekaman ini" "Tonton sekarang" + "Hapus rekaman…" "Dapat direkam" "Perekaman dijadwalkan" "Perekaman bentrok" "Merekam" "Rekaman gagal" "Membaca program untuk membuat jadwal rekaman" - "Membaca program" - - + "Membaca program" + "Lihat rekaman baru-baru ini" + "Rekaman %1$s tidak selesai." + "Rekaman %1$s dan %2$s tidak selesai." + "Rekaman %1$s, %2$s, dan %3$s tidak selesai." + "Rekaman %1$s tidak selesai karena penyimpanan tidak mencukupi." + "Rekaman %1$s dan %2$s tidak selesai karena penyimpanan tidak mencukupi." + "Rekaman %1$s, %2$s, dan %3$s tidak selesai karena penyimpanan tidak mencukupi." "DVR memerlukan penyimpanan ekstra" "Anda akan dapat merekam program dengan DVR. Namun, saat ini penyimpanan pada perangkat tidak cukup untuk menjalankan DVR. Hubungkan drive eksternal berukuran %1$sGB atau lebih besar dan ikuti langkah untuk memformat drive tersebut sebagai perangkat penyimpanan." + "Penyimpanan tidak cukup" + "Program ini tidak akan direkam karena penyimpanan tidak cukup. Coba menghapus beberapa rekaman yang ada." "Penyimpanan tidak ada" - "Beberapa penyimpanan yang digunakan oleh DVR tidak ada. Hubungkan drive eksternal yang sebelumnya digunakan untuk kembali mengaktifkan DVR. Atau, Anda dapat memilih untuk melupakan penyimpanan jika sudah tidak tersedia." - "Lupakan penyimpanan?" - "Semua konten dan jadwal yang direkam akan hilang." "Berhenti merekam?" "Konten yang direkam akan disimpan." - - + "Perekaman %1$s akan dihentikan karena bentrok dengan program ini. Konten yang direkam akan disimpan." "Rekaman telah dijadwalkan, tapi jadwalnya bentrok" "Rekaman telah dimulai, namun jadwalnya bentrok" "%1$s akan direkam." @@ -306,14 +292,27 @@ "Program yang sama telah dijadwalkan untuk direkam pukul %1$s." "Sudah direkam" "Program ini telah direkam. Rekaman ada di pustaka DVR." - - - - - - - - + "Perekaman serial dijadwalkan" + + %1$d rekaman telah dijadwalkan untuk %2$s. + %1$d rekaman telah dijadwalkan untuk %2$s. + + + %1$d rekaman telah dijadwalkan untuk %2$s. %3$d rekaman tersebut tidak akan direkam karena jadwalnya bentrok. + %1$d rekaman telah dijadwalkan untuk %2$s. Rekaman tersebut tidak akan direkam karena jadwalnya bentrok. + + + %1$d rekaman telah dijadwalkan untuk %2$s. %3$d episode seri ini dan seri lainnya tidak akan direkam karena jadwalnya bentrok. + %1$d rekaman telah dijadwalkan untuk %2$s. %3$d episode seri ini dan seri lainnya tidak akan direkam karena jadwalnya bentrok. + + + %1$d rekaman telah dijadwalkan untuk %2$s. 1 episode seri lainnya tidak akan direkam karena jadwalnya bentrok. + %1$d rekaman telah dijadwalkan untuk %2$s. 1 episode seri lainnya tidak akan direkam karena jadwalnya bentrok. + + + %1$d rekaman telah dijadwalkan untuk %2$s. %3$d episode seri lainnya tidak akan direkam karena jadwalnya bentrok. + %1$d rekaman telah dijadwalkan untuk %2$s. %3$d episode seri lainnya tidak akan direkam karena jadwalnya bentrok. + "Tidak ditemukan program yang direkam." "Rekaman terkait" "(Tidak ada deskripsi program)" @@ -336,6 +335,7 @@ "Hentikan rekaman seri?" "Episode yang direkam akan tetap tersedia di pustaka DVR." "Hentikan" + "Tidak ada episode tayang sekarang." "Tidak ada episode yang tersedia.\nEpisode akan direkam setelah tersedia." (%1$d menit) diff --git a/res/values-is-rIS/strings.xml b/res/values-is-rIS/strings.xml index 6111b578..4cb91d48 100644 --- a/res/values-is-rIS/strings.xml +++ b/res/values-is-rIS/strings.xml @@ -20,9 +20,8 @@ "einóma" "víðóma" "Spilunarstýringar" - "Nýlegar rásir" + "Rásir" "Sjónvarpskostir" - "Innfelld mynd" "Spilunarstýringar eru ekki í boði fyrir þessa rás" "Spila eða gera hlé" "Spóla áfram" @@ -35,33 +34,15 @@ "Skjátextar" "Birtingarstill." "Innfelld mynd" - "Kveikt" - "Slökkt" "Multi-Audio" "Fá fleiri rásir" "Stillingar" - "Inntak" - "Víxla" - "Kveikt" - "Slökkt" - "Hljóð" - "Aðalgluggi" - "Innfelling" - "Útlit" - "Neðst til hægri" - "Efst til hægri" - "Efst til vinstri" - "Neðst til vinstri" - "Hlið við hlið" - "Stærð" - "Stór" - "Lítil" - "Inntak" "Sjónvarp (loftnet/kapall)" "Engar dagskrárupplýsingar" "Engar upplýsingar" "Læst rás" - "Óþekkt tungumál" + "Óþekkt tungumál" + "Skjátexti %1$d" "Skjátextar" "Slökkt" "Sérstilla snið" @@ -135,6 +116,10 @@ "Rangt PIN-númer. Reyndu aftur." "Reyndu aftur; PIN-númerin stemma ekki" + "Sláðu inn póstnúmerið þitt." + "Forritið Beinar útsendingar notar póstnúmerið þitt til að veita þér nákvæma sjónvarpsdagskrá." + "Sláðu inn póstnúmerið þitt" + "Ógilt póstnúmer" "Stillingar" "Sérsníða rásalista" "Veldu rásir fyrir dagskrárvísinn þinn" @@ -143,6 +128,7 @@ "Foreldraeftirlit" "Leyfi opins kóða" "Leyfi opins kóða" + "Senda ábendingu" "Útgáfa" "Til að horfa á þessa rás skaltu ýta til hægri og slá inn PIN-númerið þitt" "Til að horfa á þennan þátt skaltu ýta til hægri og slá inn PIN-númerið þitt" @@ -181,8 +167,6 @@ "Ýttu á VELJA"" til að fá aðgang að sjónvarpsvalmyndinni." "Ekkert sjónvarpsinntak fannst" "Sjónvarpsinntak finnst ekki" - "Ekki er stuðningur við innfellda mynd" - "Ekkert inntak er fyrir hendi til að nota í innfelldri mynd" "Gerð móttakara passar ekki. Ræstu forritið Rásir í beinni til að fá sjónvarpsinntak með móttakaragerð." "Stilling mistókst" "Ekkert forrit fannst sem getur framkvæmt þessa aðgerð." @@ -259,8 +243,6 @@ "Vista" "Stakar upptökur hafa forgang" "Hætta við" - "Hætta við" - "Gleyma" "Stöðva" "Skoða upptökuáætlun" "Bara þennan þátt" @@ -270,25 +252,29 @@ "Taka þetta upp í staðinn" "Hætta við þessa upptöku" "Horfa núna" + "Eyða upptökum…" "Hægt að taka upp" "Upptaka sett á áætlun" "Skarast á við aðra upptöku" "Upptaka í gangi" "Upptaka mistókst" "Les dagskrár til að búa til upptökuáætlanir" - "Les dagskrár" - - + "Skoðar þætti" + "Skoða nýlegar upptökur" + "Upptöku á %1$s er ekki lokið." + "Upptöku á %1$s og %2$s er ekki lokið." + "Upptöku á %1$s, %2$s og %3$s er ekki lokið." + "Skortur á geymslurými kom í veg fyrir að upptöku á %1$s væri lokið." + "Skortur á geymslurými kom í veg fyrir að upptöku á %1$s og %2$s væri lokið." + "Skortur á geymslurými kom í veg fyrir að upptöku á %1$s, %2$s og %3$s væri lokið." "DVR þarf meira geymslupláss" "Þú getur tekið upp þætti með stafræna upptökubúnaðinum (DVR). Hins vegar er ekki nóg geymslupláss til staðar á tækinu þínu sem stendur til að DVR virki. Tengdu utanáliggjandi disk við tækið sem er %1$s GB eða stærri og fylgdu skrefunum til að setja það upp sem geymslupláss tækisins." + "Of lítið geymslurými" + "Þátturinn verður ekki tekinn upp þar sem of lítið geymslurými er til staðar. Prófaðu að eyða einhverjum af þeim upptökum sem eru þegar til staðar." "Geymslu vantar" - "Hluta af geymslurýminu sem stafræni upptökubúnaðurinn notar vantar. Tengdu utanáliggjandi drif sem þú notaðir áður til að gera stafræna upptökubúnaðinn virkan á ný. Einnig geturðu valið að gleyma geymslunni ef hún er ekki lengur tiltæk." - "Viltu gleyma geymslunni?" - "Allt efni sem þú hefur tekið upp og allar dagskrár glatast." "Stöðva upptöku?" "Upptökur á efni verða vistaðar." - - + "Upptöku á %1$s verður hætt þar sem það skarast á við þennan þátt. Upptekið efni verður vistað." "Upptaka á áætlun en skarast við aðra" "Upptaka er hafin en hún skarast við aðra" "%1$s verður tekið upp." @@ -306,14 +292,27 @@ "Sami þáttur er þegar á upptökuáætlun kl. %1$s." "Þegar tekið upp" "Þessi þáttur hefur þegar verið tekinn upp. Hann er tiltækur í DVR-safninu." - - - - - - - - + "Upptaka þáttaseríu sett á áætlun" + + %1$d upptaka var sett á áætlun fyrir %2$s. + %1$d upptökur voru settar á áætlun fyrir %2$s. + + + %1$d upptökur voru settar á áætlun fyrir %2$s. %3$d verður ekki tekið upp því að það skarast á við annað. + %1$d upptökur voru settar á áætlun fyrir %2$s. %3$d verður ekki tekið upp því að það skarast á við annað. + + + %1$d upptaka var sett á áætlun fyrir %2$s. %3$d þættir úr þessari þáttaröð og í öðrum þáttaröðum verða ekki teknir upp því það skarast á við annað. + %1$d upptökur voru settar á áætlun fyrir %2$s. %3$d þættir úr þessari þáttaröð og í öðrum þáttaröðum verða ekki teknir upp því það skarast á við annað. + + + %1$d upptaka var sett á áætlun fyrir %2$s. Einn þáttur í annarri þáttaröð verður ekki tekinn upp því að hann skarast á við annað. + %1$d upptökur voru settar á áætlun fyrir %2$s. Einn þáttur í annarri þáttaröð verður ekki tekinn upp því að hann skarast á við annað. + + + %1$d upptaka var sett á áætlun fyrir %2$s. %3$d þættir úr annarri þáttaröð verða ekki teknir upp því þeir skarast á við annað. + %1$d upptökur voru settar á áætlun fyrir %2$s. %3$d þættir úr annarri þáttaröð verða ekki teknir upp því þeir skarast á við annað. + "Upptekinn þáttur fannst ekki." "Tengdar upptökur" "(Engin þáttalýsing)" @@ -336,6 +335,7 @@ "Hætta upptöku á þáttaröð?" "Upptökur af þáttum verða áfram tiltækar í DVR-safninu." "Stöðva" + "Ekki er verið að sýna þætti að svo stöddu." "Engir þættir eru tiltækir.\nÞeir verða teknir upp þegar þeir eru tiltækir." (%1$d mínúta) diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml index da0b48a6..5264f43a 100644 --- a/res/values-it/strings.xml +++ b/res/values-it/strings.xml @@ -20,9 +20,8 @@ "mono" "stereo" "Controlli riproduzione" - "Canali recenti" + "Canali" "Opzioni TV" - "Opzioni PIP" "Controlli di riproduzione non disponibili per questo canale" "Riproduci o metti in pausa" "Avanza velocemente" @@ -35,33 +34,15 @@ "Sottotitoli" "Visualizzazione" "PIP" - "Attiva" - "Non attiva" "Multi-audio" "Trova altri canali" "Impostazioni" - "Fonte" - "Scambia" - "Attiva" - "Non attiva" - "Suono" - "Principale" - "Finestra PIP" - "Layout" - "In basso a des." - "In alto a des." - "In alto a sin." - "In basso a sin." - "Affiancata" - "Dimensioni" - "Grande" - "Piccola" - "Origine ingresso" "TV (antenna/cavo)" "Nessuna informazione sul programma" "Nessuna informazione" "Canale bloccato" - "Lingua sconosciuta" + "Lingua sconosciuta" + "Sottotitoli in %1$d" "Sottotitoli" "Off" "Personalizza" @@ -137,6 +118,10 @@ "Il PIN è errato. Riprova." "Riprova, il PIN non corrisponde" + "Inserisci il codice postale." + "L\'app Dirette TV userà il codice postale per fornire una guida ai programmi completa per i canali TV." + "Inserisci il codice postale" + "Codice postale non valido" "Impostazioni" "Personalizza canali" "Scegli canali per guida ai programmi" @@ -145,6 +130,7 @@ "Controllo genitori" "Licenze open source" "Licenze open source" + "Invia feedback" "Versione" "Per guardare questo canale, premi il pulsante destro e inserisci il PIN" "Per guardare questo programma, premi il pulsante destro e inserisci il PIN" @@ -183,8 +169,6 @@ "Premi SELEZIONA"" per accedere al menu della TV." "Nessun ingresso TV trovato" "Impossibile trovare l\'ingresso TV" - "Funzione PIP non supportata" - "Nessun ingresso disponibile per visualizzazione PIP" "Tipo di sintonizzatore non adatto. Avvia l\'app Live TV per l\'ingresso TV di tipo sintonizzatore." "Sintonizzazione non riuscita" "Nessuna app trovata per gestire questa azione." @@ -261,8 +245,6 @@ "Salva" "Le registrazioni uniche hanno la massima priorità" "Annulla" - "Annulla" - "Elimina" "Interrompi" "Pianificazione registrazione" "Solo questo programma" @@ -272,25 +254,29 @@ "Registra questo" "Annulla la registrazione" "Guarda adesso" + "Elimina registrazioni…" "Registrabile" "Registrazione programmata" "Conflitto di registrazione" "Registrazione in corso" "Registrazione non riuscita" "Lettura dei programmi per la creazione delle pianificazioni di registrazione" - "Lettura dei programmi…" - - + "Lettura dei programmi…" + "Visualizza registrazioni recenti" + "La registrazione di %1$s è incompleta." + "Le registrazioni di %1$s e %2$s sono incomplete." + "Le registrazioni di %1$s, %2$s e %3$s sono incomplete." + "La registrazione di %1$s non è stata completata poiché lo spazio di archiviazione non era sufficiente." + "Le registrazioni di %1$s e %2$s non sono state completate poiché lo spazio di archiviazione non era sufficiente." + "Le registrazioni di %1$s, %2$s e %3$s non sono state completate poiché lo spazio di archiviazione non era sufficiente." "Il dispositivo DVR ha bisogno di più spazio di archiviazione" "Potrai registrare programmi con un dispositivo DVR. Tuttavia, al momento lo spazio di archiviazione non è sufficiente per consentire il funzionamento del DVR. Collega un\'unità esterna di almeno %1$s GB e segui la procedura per formattarla come memoria dispositivo." + "Spazio di archiviazione insufficiente" + "Il programma non sarà registrato perché lo spazio di archiviazione non è sufficiente. Prova a eliminare alcune registrazioni esistenti." "Memoria mancante" - "Manca parte della memoria utilizzata dal DVR. Collega l\'unità esterna utilizzata prima di riattivare il DVR. In alternativa, puoi scegliere di eliminare la memoria se non è più disponibile." - "Eliminare la memoria?" - "Tutti i contenuti registrati e le pianificazioni andranno persi." "Interrompere la registrazione?" "I contenuti registrati verranno salvati." - - + "La registrazione di %1$s verrà interrotta perché è in conflitto con questo programma. I contenuti registrati verranno salvati." "Registrazione programmata, ma ci sono conflitti" "La registrazione è iniziata, ma ci sono conflitti" "Verrà registrato il programma %1$s." @@ -308,14 +294,27 @@ "È già stata programmata la registrazione dello stesso programma alle %1$s." "Già registrato" "Questo programma è già stato registrato. È disponibile nella raccolta DVR." - - - - - - - - + "Registrazione della serie programmata" + + %1$d registrazioni sono state pianificate per la serie %2$s. + %1$d registrazione è stata pianificata per la serie %2$s. + + + %1$d registrazioni sono state pianificate per la serie %2$s. %3$d non saranno registrate a causa di conflitti. + %1$d registrazione è stata pianificata per la serie %2$s. Non sarà registrata a causa di conflitti. + + + %1$d registrazioni sono state pianificate per la serie %2$s. %3$d puntate di questa e altre serie non saranno registrate a causa di conflitti. + %1$d registrazione è stata pianificata per la serie %2$s. %3$d puntate di questa e altre serie non saranno registrate a causa di conflitti. + + + %1$d registrazioni sono state pianificate per la serie %2$s. Una puntata di un\'altra serie non sarà registrata a causa di conflitti. + %1$d registrazione è stata pianificata per la serie %2$s. Una puntata di un\'altra serie non sarà registrata a causa di conflitti. + + + %1$d registrazioni sono state pianificate per la serie %2$s. %3$d puntate di altre serie non saranno registrate a causa di conflitti. + %1$d registrazione è stata pianificata per la serie %2$s. %3$d puntate di altre serie non saranno registrate a causa di conflitti. + "Programma registrato non trovato." "Registrazioni correlate" "(Nessuna descrizione programma)" @@ -338,6 +337,7 @@ "Interrompere la registrazione della serie?" "Le puntate registrate resteranno disponibili nella raccolta DVR." "Interrompi" + "Nessuna puntata in onda al momento." "Non ci sono episodi a disposizione.\nVerranno registrati non appena saranno disponibili." (%1$d minuti) diff --git a/res/values-iw/strings.xml b/res/values-iw/strings.xml index 743b31fd..35c193f1 100644 --- a/res/values-iw/strings.xml +++ b/res/values-iw/strings.xml @@ -20,9 +20,8 @@ "מונו" "סטריאו" "פקדי הפעלה" - "ערוצים אחרונים" + "ערוצים" "‏אפשרויות TV" - "‏אפשרויות PIP" "פקדי הפעלה אינם זמינים בשביל הערוץ הזה" "הפעל או השהה" "הרץ קדימה" @@ -35,33 +34,15 @@ "כתוביות" "מצב תצוגה" "PIP" - "מופעל" - "כבוי" "מולטי-אודיו" "קבל ערוצים נוספים" "הגדרות" - "מקור" - "החלף" - "מופעל" - "כבוי" - "צליל" - "ראשי" - "‏חלון PIP" - "פריסה" - "שמאל למטה" - "שמאל למעלה" - "ימין למעלה" - "ימין למטה" - "זה לצד זה" - "גודל" - "גדול" - "קטן" - "מקור קלט" "טלוויזיה (אנטנה/כבלים)" "אין פרטי תכנית" "אין מידע" "ערוץ חסום" - "שפה לא ידועה" + "שפה לא ידועה" + "‏כתוביות ב%1$d" "כתוביות" "כבוי" "התאם אישית את הפורמט" @@ -139,6 +120,10 @@ "‏מספר ה-PIN היה שגוי. נסה שוב." "נסה שוב, קוד האימות אינו תואם" + "הזן את המיקוד שלך" + "‏אפליקציית Live TV תשתמש במיקוד כדי לספק לך את לוח השידורים המלא של ערוצי הטלוויזיה." + "הזן את המיקוד שלך" + "מיקוד לא חוקי" "הגדרות" "התאם אישית רשימת ערוצים" "בחר ערוצים ללוח השידורים שלך" @@ -147,6 +132,7 @@ "בקרת הורים" "רישיונות קוד פתוח" "רישיונות קוד פתוח" + "שלח משוב" "גרסה" "‏כדי לצפות בערוץ הזה, לחץ על \'ימין\' והזן את מספר ה-PIN" "‏כדי לצפות בתכנית הזו, לחץ על \'ימין\' והזן את מספר ה-PIN" @@ -189,8 +175,6 @@ "לחץ על \'בחר\'"" כדי לפתוח את תפריט הטלוויזיה." "לא נמצא קלט טלוויזיה" "לא ניתן למצוא את קלט הטלוויזיה" - "‏PIP אינו נתמך" - "‏אין קלט זמין הניתן להצגה באמצעות PIP" "סוג הטיונר אינו מתאים. הפעל את האפליקציה \'ערוצים בשידור חי\' בשביל טיונר מסוג קלט טלוויזיה." "הכוונון נכשל" "לא נמצאה אפליקציה שיכולה לטפל בפעולה הזו." @@ -279,8 +263,6 @@ "שמור" "הקלטות חד-פעמיות הן בעלות העדיפות הגבוהה ביותר" "בטל" - "בטל" - "שכח" "עצור" "הצג לוח זמנים להקלטה" "את התוכנית הזו בלבד" @@ -290,25 +272,29 @@ "הקלטת תוכנית זו במקום זאת" "ביטול הקלטה זאת" "לצפייה עכשיו" + "מחיקת הקלטות…" "ניתנת להקלטה" "ההקלטה מתוזמנת" "התנגשות בין הקלטות" "מקליט" "ההקלטה נכשלה" "קורא תוכניות כדי ליצור לוחות זמנים להקלטות" - "קורא תוכניות" - - + "קורא תוכניות" + "הצגת ההקלטות האחרונות" + "ההקלטה של %1$s חלקית." + "ההקלטות של %1$s ו-%2$s חלקיות." + "ההקלטות של %1$s, %2$s ו-%3$s חלקיות." + "ההקלטה של %1$s לא הושלמה מפני ששטח האחסון אינו מספיק." + "ההקלטות של %1$s ו-%2$s לא הושלמו מפני ששטח האחסון אינו מספיק." + "ההקלטות של %1$s, %2$s ו-%3$s לא הושלמו מפני ששטח האחסון אינו מספיק." "‏DVR זקוק לשטח אחסון נוסף" "‏תוכל להקליט תוכניות עם DVR. אולם, אין מספיק מקום אחסון במכשיר שלך כרגע כדי ש-DVR יעבוד. התחבר לכונן חיצוני בגודל GB%1$s או יותר, ובצע את השלבים לביצוע פורמט כאחסון מכשיר." + "אין מספיק שטח אחסון" + "התוכנית הזאת לא תוקלט מפני שאין מספיק שטח אחסון. נסה למחוק חלק מההקלטות הקיימות." "אחסון חסר" - "‏חלק מהאחסון שנעשה בו שימוש ב-DVR חסר. חבר את הכונן החיצוני שבו השתמשת בעבר כדי להפעיל מחדש את ה-DVR. לחלופין, תוכל לבחור לשכוח את האחסון אם הוא אינו זמין עוד." - "לשכוח את האחסון?" - "כל התוכן ולוחות הזמנים שתיעדת יאבדו." "האם להפסיק את ההקלטה?" "התוכן המוקלט יישמר." - - + "ההקלטה של %1$s תיפסק מפני שהיא מתנגשת עם התוכנית הזאת. התוכן שהוקלט יישמר." "ההקלטה מתוזמנת, אבל ישנן הקלטות מתנגשות." "ההקלטה החלה, אבל ישנן הקלטות מתנגשות" "התוכנית %1$s תוקלט." @@ -328,14 +314,37 @@ "אותה תוכנית כבר תוזמנה להקלטה ב-%1$s." "כבר הוקלט" "‏תוכנית זו כבר הוקלטה. היא זמינה לצפייה בספריית ה-DVR." - - - - - - - - + "הקלטת הסדרה תוזמנה" + + %1$d הקלטות תוזמנו לסדרה %2$s. + %1$d הקלטות תוזמנו לסדרה %2$s. + %1$d הקלטות תוזמנו לסדרה %2$s. + הקלטה %1$d תוזמנה לסדרה %2$s. + + + %1$d הקלטות תוזמנו לסדרה %2$s. %3$d הקלטות מתוכן לא יוקלטו עקב התנגשויות. + %1$d הקלטות תוזמנו לסדרה %2$s. %3$d הקלטות מתוכן לא יוקלטו עקב התנגשויות. + %1$d הקלטות תוזמנו לסדרה %2$s. %3$d הקלטות מתוכן לא יוקלטו עקב התנגשויות. + הקלטה %1$d תוזמנה לסדרה %2$s. היא לא תוקלט עקב התנגשויות. + + + %1$d הקלטות תוזמנו לסדרה %2$s. %3$d פרקים מסדרה זו וסדרה אחרת לא יוקלטו עקב התנגשויות. + %1$d הקלטות תוזמנו לסדרה %2$s. %3$d פרקים מסדרה זו וסדרה אחרת לא יוקלטו עקב התנגשויות. + %1$d הקלטות תוזמנו לסדרה %2$s. %3$d פרקים מסדרה זו וסדרה אחרת לא יוקלטו עקב התנגשויות. + הקלטה %1$d תוזמנה לסדרה %2$s. %3$d פרקים מסדרה זו וסדרה אחרת לא יוקלטו עקב התנגשויות. + + + %1$d הקלטות תוזמנו לסדרה %2$s. פרק אחד מסדרה אחרת לא יוקלט עקב התנגשויות. + %1$d הקלטות תוזמנו לסדרה %2$s. פרק אחד מסדרה אחרת לא יוקלט עקב התנגשויות. + %1$d הקלטות תוזמנו לסדרה %2$s. פרק אחד מסדרה אחרת לא יוקלט עקב התנגשויות. + הקלטה %1$d תזומנה לסדרה %2$s. פרק אחד מסדרה אחרת לא יוקלט עקב התנגשויות. + + + %1$d הקלטות תוזמנו לסדרה %2$s. %3$d פרקים מסדרה אחרת לא יוקלטו עקב התנגשויות. + %1$d הקלטות תוזמנו לסדרה %2$s. %3$d פרקים מסדרה אחרת לא יוקלטו עקב התנגשויות. + %1$d הקלטות תוזמנו לסדרה %2$s. %3$d פרקים מסדרה אחרת לא יוקלטו עקב התנגשויות. + הקלטה %1$d תוזמנה לסדרה %2$s. %3$d פרקים מסדרה אחרת לא יוקלטו עקב התנגשות. + "התוכנית המוקלטת לא נמצאה." "הקלטות קשורות" "(אין תיאור לתוכנית)" @@ -362,6 +371,7 @@ "האם להפסיק את הקלטת הסדרה?" "‏פרקים מוקלטים יישארו זמינים בספריית DVR." "הפסק" + "אף פרק אינו משודר כעת." "אין פרקים זמינים.\nהם יוקלטו כשיהיו זמינים." ‏(%1$d דקות) diff --git a/res/values-ja/strings.xml b/res/values-ja/strings.xml index 9c526c21..c2572978 100644 --- a/res/values-ja/strings.xml +++ b/res/values-ja/strings.xml @@ -20,9 +20,8 @@ "モノラル" "ステレオ" "再生操作" - "最近のチャンネル" + "チャンネル" "テレビオプション" - "PIPオプション" "このチャンネルでは再生操作を使用できません" "再生または一時停止" "早送り" @@ -35,33 +34,15 @@ "字幕" "表示モード" "PIP" - "ON" - "OFF" "マルチオーディオ" "その他のチャンネル" "設定" - "ソース" - "切り替え" - "ON" - "OFF" - "サウンド" - "メイン" - "PIPウィンドウ" - "レイアウト" - "右下" - "右上" - "左上" - "左下" - "横並び" - "サイズ" - "大" - "小" - "入力ソース" "テレビ(アンテナ/ケーブル)" "プログラム情報がありません" "情報はありません" "ブロックされているチャンネル" - "不明な言語" + "不明な言語" + "クローズド キャプション %1$d" "字幕" "OFF" "書式設定のカスタマイズ" @@ -135,6 +116,10 @@ "PINが正しくありません。もう一度お試しください。" "もう一度お試しください。PINが一致しません。" + "郵便番号の入力" + "ライブ チャンネル アプリは、テレビ チャンネルの完全な番組ガイドを提供するために郵便番号を使用します。" + "郵便番号を入力してください" + "郵便番号が無効です" "設定" "チャンネル リストをカスタマイズ" "番組ガイド用のチャンネルを選択します" @@ -143,6 +128,7 @@ "保護者による使用制限" "オープンソース ライセンス" "オープンソースライセンス" + "フィードバックを送信" "バージョン" "このチャンネルを視聴するには、右のボタンを押してPINを入力してください。" "このプログラムを視聴するには、右のボタンを押してPINを入力してください。" @@ -181,8 +167,6 @@ "テレビのメニューにアクセスするには""[SELECT]を押してください""。" "テレビ入力が見つかりませんでした" "テレビ入力が見つかりません" - "PIPがサポートされていません" - "PIPで表示できる有効な入力はありません" "無効なチューナータイプ。チューナー入力のライブチャンネルアプリを起動してください。" "同調に失敗しました" "この操作を行うアプリが見つかりませんでした。" @@ -259,8 +243,6 @@ "保存" "1 回のみの録画を最優先" "キャンセル" - "キャンセル" - "削除" "停止" "録画予約を見る" "この番組のみ" @@ -270,25 +252,29 @@ "この番組を代わりに録画" "この録画をキャンセル" "今すぐ見る" + "録画の削除…" "録画できます" "録画を予約しました" "録画予約が重複しています" "録画" "録画に失敗しました" "録画予約を作成する番組情報を読み取っています" - "番組情報を読み取っています" - - + "番組情報を読み取っています" + "最近の録画を表示" + "「%1$s」の録画を完了できませんでした" + "「%1$s」と「%2$s」の録画を完了できませんでした" + "「%1$s」、「%2$s」、「%3$s」の録画を完了できませんでした" + "ストレージの容量不足のため、「%1$s」の録画を完了できませんでした。" + "ストレージの容量不足のため、「%1$s」と「%2$s」の録画を完了できませんでした。" + "ストレージの容量不足のため、「%1$s」、「%2$s」、「%3$s」の録画を完了できませんでした。" "DVR のストレージ不足" "DVR を使用して番組を録画することはできますが、お使いの端末のストレージが十分ではないため、現在 DVR を使用できません。%1$s GB 以上の外部ストレージを接続し、手順に沿って端末のストレージと同じようにフォーマットしてください。" + "空き容量の不足" + "空き容量が不足しているため、この番組は録画されません。録画済みの番組をいくつか削除してみてください。" "ストレージにアクセスできません" - "DVR で使用しているストレージの一部にアクセスできません。DVR を再度有効にする前に、使用している外部ドライブを接続してください。アクセスできないストレージは削除することもできます。" - "ストレージの削除" - "録画したコンテンツと録画予約がすべて失われます。" "録音の停止" "録画したコンテンツは保存されます。" - - + "「%1$s」の録画は、この番組と重なっているため停止されます。録画したコンテンツは保存されます。" "録画を予約しましたが、他の予約と重なっています" "録画を開始しましたが、他の予約と重なっています" "「%1$s」が録画されます。" @@ -306,14 +292,27 @@ "同じ番組が既に録画予約されています(%1$s)。" "録画済み" "この番組は既に録画され、DVR ライブラリから利用できます。" - - - - - - - - + "シリーズの録画を予約しました" + + %2$s」の録画は %1$d 件予約されています。 + %2$s」の録画は %1$d 件予約されています。 + + + %2$s」の録画は %1$d 件予約されていますが、予約が競合するため %3$d 件は録画されません。 + %2$s」の録画は %1$d 件予約されていますが、予約が競合するため録画されません。 + + + %2$s」の録画は %1$d 件予約されています。予約が競合するため、このシリーズと他のシリーズのエピソード(%3$d 件)は録画されません。 + %2$s」の録画は %1$d 件予約されています。予約が競合するため、このシリーズと他のシリーズのエピソード(%3$d 件)は録画されません。 + + + %2$s」の録画は %1$d 件予約されています。予約が競合するため、他のシリーズのエピソード(1 件)は録画されません。 + %2$s」の録画は %1$d 件予約されています。予約が競合するため、他のシリーズのエピソード(1 件)は録画されません。 + + + %2$s」の録画は %1$d 件予約されています。予約が競合するため、他のシリーズのエピソード(%3$d 件)は録画されません。 + %2$s」の録画は %1$d 件予約されています。予約が競合するため、他のシリーズのエピソード(%3$d 件)は録画されません。 + "録画された番組は見つかりませんでした。" "関連の録画" "(番組の説明はありません)" @@ -336,6 +335,7 @@ "シリーズの録画を中止しますか?" "録画したエピソードは DVR ライブラリから利用できます。" "停止" + "現在、放送中の番組はありません。" "利用可能なエピソードはありません。\n利用可能になると録画されます。" (%1$d分) diff --git a/res/values-ka-rGE/strings.xml b/res/values-ka-rGE/strings.xml index 72608bd4..774691e9 100644 --- a/res/values-ka-rGE/strings.xml +++ b/res/values-ka-rGE/strings.xml @@ -20,9 +20,8 @@ "მონო" "სტერეო" "ჩართვის კონტროლი" - "უახლესი არხები" + "არხები" "TV პარამეტრები" - "PIP პარამეტრები" "ამ არხისთვის არ არის ხელმისაწვდომი ჩართვის კონტროლი" "დაკვრა ან პაუზა" "წინ გადახვევა" @@ -35,33 +34,15 @@ "დახურ.წარწერები" "ჩვენების რეჟიმი" "PIP" - "ჩართული" - "გამორთული" "მულტი-აუდიო" "მეტი არხის მიღება" "პარამეტრები" - "წყარო" - "შენაცვლება" - "ჩართული" - "გამორთული" - "ხმა" - "მთავარი" - "PIP ფანჯარა" - "განლაგება" - "ქვედა მარჯვენა" - "ზედა მარჯვენა" - "ზედა მარცხენა" - "ქვედა მარცხენა" - "გვერდი-გვერდ" - "ზომა" - "დიდი" - "პატარა" - "შემავალი წყარო" "ტელევიზია (ანტენა/კაბელი)" "პროგრამის ინფორმაცია არ არის" "ინფორმაცია არ არის ხელმისაწვდომი" "დაბლოკილი არხი" - "უცნობი ენა" + "უცნობი ენა" + "დახურული სუბტიტრები %1$d" "დახურული სუბტიტრები" "გამორთულია" "ფორმატირების მორგება" @@ -135,6 +116,10 @@ "ეს PIN არასწორი იყო. ისევ სცადეთ." "სცადეთ ისევ, PIN არ შეესაბამება" + "შეიყვანეთ თქვენი ZIP კოდი." + "სატელევიზიო არხების სრული პროგრამის სახელმძღვანელოს მისაწოდებლად, Live TV აპი ZIP კოდს გამოიყენებს." + "შეიყვანეთ თქვენი ZIP კოდი" + "არასწორი ZIP კოდი" "პარამეტრები" "არხების სიის მორგება" "აირჩიეთ არხები პროგრამის სახელმძღვანელოსთვის" @@ -143,6 +128,7 @@ "მშობელთა კონტროლი" "ღია კოდის ლიცენზიები" "ღია კოდის ლიცენზიები" + "გამოხმაურება" "ვერსია" "ამ არხის საყურებლად დააჭირეთ „მარჯვენას“ და შეიყვანოთ თქვენი PIN" "ამ პროგრამის საყურებლად დააჭირეთ „მარჯვენას“ და შეიყვანოთ თქვენი PIN" @@ -181,8 +167,6 @@ "დააჭირეთ არჩევას"" სატელევიზიო მენიუზე წვდომისთვის." "TV შეყვანა ვერ მოიძებნა." "TV შეყვანა ვერ იძებნება." - "არ არის PIP-ის მხარდაჭერა." - "PIP-ში გამოსაჩენი შემავალი სიგნალი მიუწვდომელია." "ტიუნერის ტიპი არ არის შესაფერისი; გთხოვთ გაუშვით პირდაპირი არხების აპი, სატელევიზიო შეტანის ტიუნერის ტიპისათვის." "არხების დაჭერა ვერ მოხერხდა." "ამ მოქმედების შესასრულებლად აპი ვერ მოიძებნა." @@ -259,8 +243,6 @@ "შენახვა" "ერთჯერად ჩანაწერებს მიენიჭოს მეტი პრიორიტეტი" "გაუქმება" - "გაუქმება" - "დავიწყება" "შეწყვეტა" "ჩაწერის განრიგის ნახვა" "მხოლოდ ეს პროგრამა" @@ -270,25 +252,29 @@ "სანაცვლოდ, ჩაიწეროს ეს" "ამ ჩანაწერის გაუქმება" "ახლავე ყურება" + "ჩანაწერების წაშლა…" "შესაძლებელია ჩაწერა" "ჩაწერა დაგეგმილია" "ჩაწერის კონფლიქტი" "მიმდინარეობს ჩაწერა" "ჩაწერა ვერ მოხერხდა" "მიმდინარეობს პროგრამების წაკითხვა ჩაწერის განრიგების შესაქმნელად" - "მიმდინარეობს პროგრამების წაკითხვა" - - + "მიმდინარეობს პროგრამების წაკითხვა" + "ბოლო ჩანაწერების ნახვა" + "%1$s არასრულად ჩაიწერა." + "%1$s და %2$s არასრულად ჩაიწერა." + "%1$s, %2$s და %3$s არასრულად ჩაიწერა." + "არასაკმარისი მეხსიერების გამო, %1$s არასრულად ჩაიწერა." + "არასაკმარისი მეხსიერების გამო, %1$s და %2$s არასრულად ჩაიწერა." + "არასაკმარისი მეხსიერების გამო, %1$s, %2$s და %3$s არასრულად ჩაიწერა." "DVR მეტ მეხსიერებას საჭიროებს" "DVR-ის მეშვეობით პროგრამების ჩაწერა შესაძლებელია, თუმცა თქვენს მოწყობილობაზე ამჟამად ხელმისაწვდომი მეხსიერება DVR-ის მუშაობისთვის არასაკმარისია. გთხოვთ, დააკავშიროთ %1$s გბაიტი ან მეტი მოცულობის დისკწამყვანი და მიჰყვეთ ინსტრუქციას, რომელიც დაგეხმარებათ, დააფორმატოთ ის მოწყობილობის მეხსიერების სახით." + "მეხსიერება არასაკმარისია" + "ეს პროგრამა ვერ ჩაიწერება არასაკმარისი მეხსიერების გამო. ცადეთ არსებული ჩანაწერების წაშლა." "აკლია მეხსიერება" - "DVR-ის მიერ გამოყენებული მეხსიერების ნაწილი ვერ მოიძებნა. DVR-ის ხელახლა ჩასართავად, გთხოვთ, დააკავშიროთ გარე დისკი, რომლითაც ადრე სარგებლობდით. თუ მეხსიერება ხელმისაწვდომი აღარ არის, შეგიძლიათ მეხსიერების დავიწყება აირჩიოთ." - "გსურთ მეხსიერების დავიწყება?" - "მთელი თქვენი ჩაწერილი კონტენტი და ყველა განრიგი დაიკარგება." "გსურთ ჩაწერის შეწყვეტა?" "ჩაწერილი კონტენტი შეინახება." - - + "„%1$s“-ის ჩაწერა შეწყდება ამ პროგრამასთან კონფლიქტის გამო. ჩაწერილი კონტენტი შეინახება." "ჩაწერა დაგეგმილია, თუმცა ის კონფლიქტშია" "ჩაწერა დაიწყო, თუმცა ის კონფლიქტშია" "%1$s ჩაიწერება." @@ -306,14 +292,27 @@ "იგივე პროგრამა უკვე დაიგეგმა ჩასაწერად %1$s-ზე." "უკვე ჩაწერილია" "ეს პროგრამა უკვე ჩაწერილია და DVR ბიბლიოთეკაშია ხელმისაწვდომი." - - - - - - - - + "სერიალის ჩაწერა დაგეგმილია" + + %2$s“-სთვის დაიგეგმა %1$d ჩანაწერი. + %2$s“-სთვის დაიგეგმა %1$d ჩანაწერი. + + + %2$s“-სთვის დაიგეგმა %1$d ჩანაწერი. %3$d მათგანი ვერ ჩაიწერება კონფლიქტების გამო. + %2$s“-სთვის დაიგეგმა %1$d ჩანაწერი. ის ვერ ჩაიწერება კონფლიქტების გამო. + + + %2$s“-სთვის დაიგეგმა %1$d ჩანაწერი. ამ სერიალის %3$d ეპიზოდი და სხვა სერიალები ვერ ჩაიწერება კონფლიქტების გამო. + %2$s“-სთვის დაიგეგმა %1$d ჩანაწერი. ამ სერიალის %3$d ეპიზოდი და სხვა სერიალები ვერ ჩაიწერება კონფლიქტების გამო. + + + %2$s“-სთვის დაიგეგმა %1$d ჩანაწერი. სხვა სერიალის 1 ეპიზოდი ვერ ჩაიწერება კონფლიქტების გამო. + %2$s“-სთვის დაიგეგმა %1$d ჩანაწერი. სხვა სერიალის 1 ეპიზოდი ვერ ჩაიწერება კონფლიქტების გამო. + + + %2$s“-სთვის დაიგეგმა %1$d ჩანაწერი. სხვა სერიალების %3$d ეპიზოდი ვერ ჩაიწერება კონფლიქტების გამო. + %2$s“-სთვის დაიგეგმა %1$d ჩანაწერი. სხვა სერიალების %3$d ეპიზოდი ვერ ჩაიწერება კონფლიქტების გამო. + "ჩაწერილი პროგრამა ვერ მოიძებნა." "დაკავშირებული ჩანაწერები" "(პროგრამის აღწერა არ არის)" @@ -336,6 +335,7 @@ "გსურთ სერიალის ჩაწერის შეწყვეტა?" "ჩაწერილი ეპიზოდები DVR ბიბლიოთეკაში კვლავ იქნება ხელმისაწვდომი." "შეწყვეტა" + "ამჟამად ეთერში არცერთი ეპიზოდი არ გადის." "ეპიზოდები მიუწვდომელია.\nმათი ჩაწერა მოხდება მაშინ, როცა ისინი ხელმისაწვდომი გახდება." (%1$d წუთი) diff --git a/res/values-kk-rKZ/strings.xml b/res/values-kk-rKZ/strings.xml index 2a47972c..98bb482c 100644 --- a/res/values-kk-rKZ/strings.xml +++ b/res/values-kk-rKZ/strings.xml @@ -20,9 +20,8 @@ "моно" "стерео" "Ойын тетіктері" - "Жақ-ғы арналар" + "Арналар" "ТД опциялары" - "PIP опциялары" "Бұл арна үшін ойнатуды басқару элементтерін қол жетімсіз" "Ойнату немесе кідірту" "Жылдам алға айналдыру" @@ -35,33 +34,15 @@ "Жасырын титрлер" "Дисплей режимі" "PIP" - "Қосулы" - "Өшірулі" "Мультиаудио" "Басқа арналар" "Параметрлер" - "Көз" - "Алмастыру" - "Қосулы" - "Өшірулі" - "Дыбыс" - "Негізгі" - "PIP терезесі" - "Орналасу" - "Төменгі оң жақ" - "Жоғарғы оң жақ" - "Жоғарғы сол жақ" - "Төменгі сол жақ" - "Қатар" - "Өлшем" - "Үлкен" - "Кішкентай" - "Енгізу көзі" "ТД (антенна/кабель)" "Бағдарлама туралы ақпарат жоқ" "Ақпарат жоқ" "Бөгелген арна" - "Белгісіз тіл" + "Белгісіз тіл" + "Субтитрлер: %1$d" "Жасырын титрлер" "Өшірулі" "Пішімдеуді теңшеу" @@ -135,6 +116,10 @@ "Бұл PIN қате болды. Әрекетті қайталаңыз." "Әрекетті қайталаңыз, PIN кодтары сәйкес емес" + "Пошта индексін енгізіңіз." + "Тікелей арналар қолданбасы телеарналардың толық бағдарламалар нұсқаулығын ұсыну үшін пошта индексін пайдаланады." + "Индексті енгізіңіз" + "Индекс жарамсыз" "Параметрлер" "Арналар тізімін теңшеу" "Бағдарлама нұсқаулығына арналған арналарды таңдау" @@ -143,6 +128,7 @@ "Ата-аналық бақылау" "Ашық бастапқы код лицензиялары" "Ашық бастапқы код лицензиялары" + "Пікір жіберу" "Нұсқа" "Бұл арнаны көру үшін оңға түймесін басып, PIN кодын енгізіңіз" "Бұл бағдарламаны көру үшін оңға түймесін басып, PIN кодын енгізіңіз" @@ -181,8 +167,6 @@ "Теледидар мәзіріне кіру үшін ТАҢДАУ"" түймесін басыңыз." "ТД кіріс сигналы табылмады" "ТД кіріс сигналын табу мүмкін емес" - "PIP мүмкіндігіне қолдау көрсетілмейді" - "PIP мүмкіндігімен көрсетуге болатын кіріс сигналы жоқ" "Тюнер түрі жарамсыз. Тюнер түрінің ТД кіріс сигналы үшін Тікелей эфир арналары қолданбасын іске қосыңыз." "Арнаға реттелмеді" "Бұл әрекетті орындайтын қолданба табылмады." @@ -259,8 +243,6 @@ "Сақтау" "Бір жолғы жазбалар ең маңызды болып табылады" "Бас тарту" - "Бас тарту" - "Жою" "Тоқтату" "Жазу кестесін көру" "Тек осы бағдарлама" @@ -270,25 +252,29 @@ "Орнына мына бағдарламаны жазу" "Бұл жазып алу кестесінен бас тарту" "Қазір көру" + "Жазбалар жойылуда…" "Жазып алуға болады" "Жазып алынады" "Жазу кестесінде қайшылық бар" "Жазылуда" "Жазу сәтсіз аяқталды" "Жазу кестелерін жасауға арналған оқу бағдарламалары" - "Бағдарлама ақпараты оқылуда" - - + "Бағдарлама ақпараты оқылуда" + "Соңғы жазбаларды көру" + "%1$s бағдарламасын жазу аяқталмады." + "%1$s және %2$s бағдарламасын жазу аяқталмады." + "%1$s, %2$s және %3$s бағдарламасын жазу аяқталмады." + "Жад жеткіліксіз болғандықтан, %1$s бағдарламасын жазу аяқталмады." + "Жад жеткіліксіз болғандықтан, %1$s және %2$s бағдарламасын жазу аяқталмады." + "Жад жеткіліксіз болғандықтан, %1$s, %2$s және %3$s бағдарламасын жазу аяқталмады." "DVR көбірек бос орынды қажет етеді" "Бағдарламаларды DVR арқылы жазып алуға болады. Алайда DVR жұмысы үшін құрылғыда бос орын жеткіліксіз. %1$s ГБ не одан үлкен сыртқы дискіні жалғап, оны құрылғы жады ретінде форматтау қадамдарын орындаңыз." + "Жадта орын жеткіліксіз" + "Жадта орын жеткіліксіз болғандықтан, бұл бағдарлама жазылмайды. Кейбір бұрыннан бар жазбаларды жойыңыз." "Жад жоқ" - "DVR пайдаланатын жадтың бір бөлігі жоқ. DVR құрылғысын қайта іске қосу үшін бұрын пайдаланған сыртқы дискіні жалғаңыз. Сондай-ақ егер жад бұдан әрі қолжетімді болмаса, оны жоюыңызға болады." - "Жад жойылсын ба?" - "Барлық жазылған мазмұндар мен кестелер жойылады." "Жазу тоқтатылсын ба?" "Жазылған мазмұн сақталады." - - + "Осы бағдарламамен қайшылық тудырғандықтан, %1$s жазу тоқтатылды. Жазып алынған мазмұн сақталады." "Жоспарланған, бірақ қайшылықтары бар" "Жазу басталды, бірақ қайшылықтары бар" "%1$s жазылады." @@ -306,14 +292,27 @@ "Бұл бағдарламаны жазып алу басқа уақытқа (%1$s) әлдеқашан жоспарланған." "Бұрын жазылған" "Бұл бағдарлама бұрын жазылған және DVR кітапханасында бар." - - - - - - - - + "Серияларды жазып алу жоспарланды" + + %2$s үшін %1$d жазба жоспарланды. + %2$s үшін %1$d жазба жоспарланды. + + + %2$s үшін %1$d жазба жоспарланды. Олардың ішінде %3$d серия қайшылықтарға байланысты жазылмайды. + %2$s үшін %1$d жазба жоспарланды. Ол қайшылықтарға байланысты жазылмайды. + + + %2$s үшін %1$d жазба жоспарланды. Бұл сериалдың және басқа сериалдың %3$d сериясы қайшылықтарға байланысты жазылмайды. + %2$s үшін %1$d жазба жоспарланды. Бұл сериалдың және басқа сериалдың %3$d сериясы қайшылықтарға байланысты жазылмайды. + + + %2$s үшін %1$d жазба жоспарланды. Басқа сериалдың 1 сериясы қайшылықтарға байланысты жазылмайды. + %2$s үшін %1$d жазба жоспарланды. Басқа сериалдың 1 сериясы қайшылықтарға байланысты жазылмайды. + + + %2$s үшін %1$d жазба .жоспарланды. Басқа сериалдың %3$d сериясы қайшылықтарға байланысты жазылмайды. + %2$s үшін %1$d жазба .жоспарланды. Басқа сериалдың %3$d сериясы қайшылықтарға байланысты жазылмайды. + "Жазылған бағдарлама табылмады." "Қатысты жазбалар" "(Бағдарлама сипаттамасы жоқ)" @@ -336,6 +335,7 @@ "Серияларды жазу тоқтатылсын ба?" "Жазылған серияларды DVR кітапханасынан алуға болады." "Тоқтату" + "Ешқандай серия көрсетіліп жатқан жоқ." "Ешқандай серия қолжетімді емес.\nОларды қолжетімді болған кезде ғана жазып алуға болады." (%1$d минут) diff --git a/res/values-km-rKH/strings.xml b/res/values-km-rKH/strings.xml index 98fa13f5..b92a75ae 100644 --- a/res/values-km-rKH/strings.xml +++ b/res/values-km-rKH/strings.xml @@ -20,9 +20,8 @@ "ម៉ូណូ" "ស្តេរ៉េអូ" "បញ្ជាការចាក់" - "ឆានែល​ថ្មីៗ" + "ឆានែល" "ជម្រើសទូរទស្សន៍" - "ជម្រើស PIP" "ការបញ្ជាលើការចាក់មិនអាចប្រើបានទេសម្រាប់ឆានែលនេះ។" "ចាក់ ឬផ្អាក" "ទៅមុខរហ័ស" @@ -35,33 +34,15 @@ "ចំណងជើង​បិទ" "របៀប​បង្ហាញ" "PIP" - "បើក" - "បិទ" "ពហុ​អូឌីយ៉ូ" "ទទួលយកប៉ុស្តិ៍ជាច្រើនទៀត" "ការកំណត់" - "ប្រភព" - "ប្ដូរ" - "បើក" - "បិទ" - "សំឡេង" - "ចម្បង" - "ផ្ទាំងវិនដូ PIP" - "ប្លង់" - "បាតខាងស្តាំ" - "ចុងខាងស្តាំ" - "ចុងខាងឆ្វេង" - "បាតខាងឆ្វេង" - "នៅក្បែរគ្នា" - "ទំហំ" - "ធំ" - "តូច" - "ប្រភព​ចូល" "ទូរទស្សន៍ (អង់តែន/ខ្សែកាប)" "មិន​មាន​ព័ត៌មាន​កម្មវិធី" "មិនមានព័ត៌មានទេ" "បណ្តាញដែលបានរារាំង" - "ភាសាមិនស្គាល់" + "ភាសាមិនស្គាល់" + "អក្សរ​រត់ %1$d" "បានបិទចំណងជើង" "បិទ" "ប្ដូររាងតាមត្រូវការ" @@ -135,6 +116,10 @@ "កូដ PIN មិន​ត្រឹមត្រូវ។ ព្យាយាម​ម្ដងទៀត។" "ព្យាយាម​ម្ដង​ទៀត កូដ​ PIN មិន​ដូច​គ្នា" + "បញ្ចូល​លេខ​កូដ​តំបន់​របស់អ្នក។" + "កម្មវិធី​ប៉ុស្តិ៍​ផ្សាយ​ផ្ទាល់​នឹង​ប្រើ​លេខ​កូដ​តំបន់ ដើម្បី​ផ្តល់ជូន​នូវ​ការ​ណែនាំ​អំពីកម្មវិធី​ទាំងស្រុង​សម្រាប់​ប៉ុស្តិ៍​ទូរទស្សន៍។" + "បញ្ចូល​លេខកូដ​តំបន់​របស់​អ្នក" + "លេខកូដ​តំបន់​មិនត្រឹមត្រូវ​ទេ" "ការកំណត់" "ប្ដូរបញ្ជីប៉ុស្តិ៍តាមបំណង" "ជ្រើសប៉ុស្តិ៍សម្រាប់ការណែនាំកម្មវិធីរបស់អ្នក" @@ -143,6 +128,7 @@ "ការគ្រប់គ្រងដោយមាតាបិតា" "អាជ្ញាប័ណ្ណប្រភពកូដបើកចំហ" "អាជ្ញាប័ណ្ណប្រភពកូដចំហ" + "ផ្ញើមតិស្ថាបនា" "កំណែ" "ដើម្បី​មើល​ឆានែល​នេះ អ្នក​ត្រូវ​ចុច​កណ្ដុរស្ដាំ រួច​បញ្ចូល​កូដ PIN របស់​អ្នក" "ដើម្បី​មើល​កម្មវិធី​នេះ អ្នក​ត្រូវ​ចុច​កណ្ដុរស្ដាំ រួច​បញ្ចូល​កូដ PIN របស់​អ្នក" @@ -181,8 +167,6 @@ "ចុច ជ្រើសរើស"" ដើម្បីចូលប្រើម៉ឺនុយទូរទស្សន៍។" "រកមិនឃើញបញ្ចូលទូរទស្សន៍ទេ" "រកមិនឃើញបញ្ចូលទូរទស្សន៍ទេ" - "មិនគាំទ្រ PIP ទេ។" - "មិនមានបញ្ចូលដែលអាចបង្ហាញជាមួយ PIP បានទេ។" "ប្រភេទឧបករណ៍ចាប់ប៉ុស្តិ៍មិនសមស្រប។ សូមចាប់ផ្តើមដំណើរការកម្មវិធី ប៉ុស្តិ៍ផ្សាយផ្ទាល់ សម្រាប់ធាតុបញ្ជូលទូរទស្សន៍ប្រភេទឧបករណ៍ចាប់ប៉ុស្តិ៍។" "កាកែសម្រួលប៉ុស្តិ៍បានបរាជ័យ" "រកមិនឃើញកម្មវិធីដើម្បីគ្រប់គ្រងសកម្មភាពនេះទេ។" @@ -259,8 +243,6 @@ "រក្សាទុក" "ការថតមួយដងមានអាទិភាពខ្ពស់បំផុត" "បោះបង់" - "បោះបង់" - "បំភ្លេច" "បញ្ឈប់" "មើលកាលវិភាគថត" "កម្មវិធីមួយនេះ" @@ -270,25 +252,29 @@ "ថតកម្មវិធីនេះជំនួសវិញ" "បោះបង់ការថតនេះ" "មើលឥឡូវនេះ" + "លុបការថត…" "អាចថតបាន" "បានកំណត់ពេលការថត" "ការថតជាន់ម៉ោងគ្នា" "ការថត" "បានបរាជ័យក្នុងការថត" "កំពុងអានកម្មវិធីដើម្បីបង្កើតកាលវិភាគថត" - "កំពុងអានកម្មវិធី" - - + "កំពុងអានកម្មវិធី" + "មើលការថតថ្មីៗ" + "ការថត %1$s មិនទាន់បញ្ចប់ទេ។" + "ការថត %1$s និង %2$s មិនទាន់បញ្ចប់ទេ។" + "ការថត %1$s, %2$s និង %3$s មិនទាន់បញ្ចប់ទេ។" + "ការថត %1$s មិនបានបញ្ចប់ទេ ដោយសារមិនមានទំហំផ្ទុកគ្រប់គ្រាន់។" + "ការថត %1$s និង %2$s មិនបានបញ្ចប់ទេ ដោយសារមិនមានទំហំផ្ទុកគ្រប់គ្រាន់។" + "ការថត %1$s, %2$s និង %3$s មិនបានបញ្ចប់ទេ ដោយសារមិនមានទំហំផ្ទុកគ្រប់គ្រាន់។" "DVR ត្រូវការទំហំផ្ទុកបន្ថែមទៀត" "អ្នកនឹងអាចថតកម្មវិធីដោយប្រើ DVR។ ទោះបីជាយ៉ាងណាក៏ដោយ ឥឡូវនេះមិនមានទំហំផ្ទុកគ្រប់គ្រាន់នៅលើឧបករណ៍របស់អ្នកដើម្បីអនុញ្ញាតឲ្យ DVR ដំណើរការនោះទេ។ សូមភ្ជាប់ទៅឧបករណ៍ផ្ទុកខាងក្រៅដែលមានទំហំផ្ទុក %1$sGB ឬធំជាងនេះ បន្ទាប់មកអនុវត្តតាមជំហានទាំងនេះដើម្បីសម្អាតវាក្នុងនាមជាឧបករណ៍ផ្ទុក។" + "មិនមានទំហំផ្ទុកគ្រប់គ្រាន់ទេ" + "មិនអាចថតកម្មវិធីនេះបានទេ ដោយសារតែមិនមានទំហំផ្ទុកគ្រប់គ្រាន់។ សូមសាកល្បងលុបការថតមួយចំនួនដែលមានស្រាប់។" "ឧបករណ៍ផ្ទុកដែលបានបាត់" - "ឧបករណ៍ផ្ទុកមួយចំនួនដែលប្រើដោយ DVR បានបាត់បង់។ សូមភ្ជាប់ថាសផ្ទុកផ្នែកខាងក្រៅដែលអ្នកបានប្រើពីមុន ដើម្បីបើកដំណើរការ DVR ឡើងវិញ ឬអ្នកអាចជ្រើសរើសធ្វើការបំភ្លេចឧបករណ៍ផ្ទុកនេះ ប្រសិនបើវាមិនអាចប្រើបានតទៅទៀត។" - "បំភ្លេចឧបករណ៍ផ្ទុកឬ?" - "មាតិកា និងកាលវិភាគដែលបានថតទុករបស់អ្នកទាំងអស់នឹងបាត់បង់។" "បញ្ឈប់ការថតឬ?" "មាតិកាដែលបានថតនឹងត្រូវបានរក្សាទុក" - - + "ការថត %1$s នឹងត្រូវបានបញ្ឈប់ ដោយសារតែវាជាន់ម៉ោងគ្នាជាមួយកម្មវិធីនេះ។ មាតិកាដែលបានថតនេះនឹងត្រូវបានរក្សាទុក។" "ការថតបានកំណត់ពេលហើយ ប៉ុន្តែមានម៉ោងជាន់គ្នា" "ការថតបានចាប់ផ្តើមប៉ុន្តែវាជាន់គ្នា" "%1$s នឹងត្រូវបានថត។" @@ -306,14 +292,27 @@ "កម្មវិធីដូចគ្នានេះត្រូវបានកំណត់ពេលថតរួចហើយនៅម៉ោង %1$s" "បានថតរួចហើយ" "កម្មវិធីនេះត្រូវបានថតរួចហើយ។ វាអាចប្រើបាននៅក្នុងបណ្ណាល័យ DVR ។" - - - - - - - - + "បានកំណត់ពេលថតវគ្គ" + + ការថតចំនួន %1$d ត្រូវបានកំណត់ពេលសម្រាប់ %2$s + ការថតចំនួន %1$d ត្រូវបានកំណត់ពេលសម្រាប់ %2$s + + + ការថតចំនួន %1$d ត្រូវបានកំណត់ពេលសម្រាប់ %2$s ។ ការថតចំនួន %3$d ក្នុងចំណោមនោះនឹងមិនត្រូវបានថតទេ ដោយសារម៉ោងជាន់គ្នា។ + ការថតចំនួន %1$d ត្រូវបានកំណត់ពេលសម្រាប់ %2$s ។ វានឹងមិនត្រូវបានថតទេ ដោយសារម៉ោងជាន់គ្នា។ + + + ការថតចំនួន %1$d ត្រូវបានកំណត់ពេលសម្រាប់ %2$s ។ ភាគចំនួន %3$d នៃវគ្គនេះ និងវគ្គផ្សេងទៀតនឹងមិនត្រូវបានថតទេ ដោយសារម៉ោងជាន់គ្នា។ + ការថតចំនួន %1$d ត្រូវបានកំណត់ពេលសម្រាប់ %2$s ។ ភាគចំនួន %3$d នៃវគ្គនេះ និងវគ្គផ្សេងទៀតនឹងមិនត្រូវបានថតទេ ដោយសារម៉ោងជាន់គ្នា។ + + + ការថតចំនួន %1$d ត្រូវបានកំណត់ពេលសម្រាប់ %2$s ។ ភាគចំនួន 1 នៃវគ្គផ្សេងនឹងមិនត្រូវបានថតទេ ដោយសារម៉ោងជាន់គ្នា។ + ការថតចំនួន %1$d ត្រូវបានកំណត់ពេលសម្រាប់ %2$s ។ ភាគចំនួន 1 នៃវគ្គផ្សេងនឹងមិនត្រូវបានថតទេ ដោយសារម៉ោងជាន់គ្នា។ + + + ការថតចំនួន %1$d ត្រូវបានកំណត់ពេលសម្រាប់ %2$s ។ ភាគចំនួន %3$d នៃវគ្គផ្សេងនឹងមិនត្រូវបានថតទេ ដោយសារម៉ោងជាន់គ្នា។ + ការថតចំនួន %1$d ត្រូវបានកំណត់ពេលសម្រាប់ %2$s ។ ភាគចំនួន %3$d នៃវគ្គផ្សេងនឹងមិនត្រូវបានថតទេ ដោយសារម៉ោងជាន់គ្នា។ + "រកមិនឃើញកម្មវិធីដែលបានថតទេ" "ការថតដែលពាក់ព័ន្ធ" "(គ្មានការពិពណ៌នាអំពីកម្មវិធីទេ)" @@ -336,6 +335,7 @@ "បញ្ឈប់ការថតវគ្គឬ?" "ភាគដែលបានថតនឹងនៅតែមាននៅក្នុងបណ្ណាល័យ DVR ។" "បញ្ឈប់" + "ឥឡូវនេះមិនមានការផ្សាយភាគថ្មីទេ។" "មិនមានផ្តល់ជូនភាគណាមួយទេ។\nពួកវានឹងត្រូវបានថតបន្ទាប់ពីមានផ្តល់ជូន។" (%1$d នាទី) diff --git a/res/values-kn-rIN/strings.xml b/res/values-kn-rIN/strings.xml index adc99656..e5afe1fa 100644 --- a/res/values-kn-rIN/strings.xml +++ b/res/values-kn-rIN/strings.xml @@ -20,9 +20,8 @@ "ಮೊನೊ" "ಸ್ಟೀರಿಯೋ" "Play ನಿಯಂತ್ರಣಗಳು" - "ಇತ್ತೀಚಿನ ಚಾನಲ್‌ಗಳು" - "TV ಆಯ್ಕೆಗಳು" - "PIP ಆಯ್ಕೆಗಳು" + "ಚಾನಲ್‌ಗಳು" + "ಟಿವಿ ಆಯ್ಕೆಗಳು" "ಈ ಚಾನಲ್‌ಗೆ ಪ್ಲೇ ನಿಯಂತ್ರಣಗಳು ಲಭ್ಯವಿಲ್ಲ" "ಪ್ಲೇ ಮಾಡಿ ಅಥವಾ ವಿರಾಮಗೊಳಿಸಿ" "ವೇಗವಾಗಿ ಮುಂದಕ್ಕೆ" @@ -35,33 +34,15 @@ "ಮುಚ್ಚಿದ ಶೀರ್ಷಿಕೆಗಳು" "ಪ್ರದರ್ಶನ ಮೋಡ್" "PIP" - "ಆನ್" - "ಆಫ್" "ಬಹು-ಆಡಿಯೊ" "ಹೆಚ್ಚು ಚಾನಲ್‌ ಪಡೆ" "ಸೆಟ್ಟಿಂಗ್‌ಗಳು" - "ಮೂಲ" - "ಸ್ವ್ಯಾಪ್‌ ಮಾಡು" - "ಆನ್" - "ಆಫ್" - "ಶಬ್ದ" - "ಪ್ರಮುಖ" - "PIP ವಿಂಡೋ" - "ಲೇಔಟ್" - "ಕೆಳಗಿನ ಬಲಭಾಗ" - "ಮೇಲಿನ ಬಲಭಾಗ" - "ಮೇಲಿನ ಎಡಭಾಗ" - "ಕೆಳಗಿನ ಎಡಭಾಗ" - "ಅಕ್ಕ ಪಕ್ಕ" - "ಗಾತ್ರ" - "ದೊಡ್ಡದು" - "ಸಣ್ಣ" - "ಇನ್‌ಪುಟ್ ಮೂಲ" - "TV (ಆಂಟೆನಾ/ಕೇಬಲ್)" + "ಟಿವಿ (ಆಂಟೆನಾ/ಕೇಬಲ್)" "ಯಾವುದೇ ಕಾರ್ಯಕ್ರಮದ ಮಾಹಿತಿ ಇಲ್ಲ" "ಯಾವುದೇ ಮಾಹಿತಿ ಇಲ್ಲ" "ನಿರ್ಬಂಧಿಸಲಾದ ಚಾನಲ್" - "ಅಪರಿಚಿತ ಭಾಷೆ" + "ಅಪರಿಚಿತ ಭಾಷೆ" + "ಮುಚ್ಚಿದ ಶೀರ್ಷಿಕೆಗಳು %1$d" "ಮುಚ್ಚಿದ ಶೀರ್ಷಿಕೆಗಳು" "ಆಫ್" "ಫಾರ್ಮ್ಯಾಟ್‌ ಮಾಡುವಿಕೆ ವೈಯಕ್ತೀಕರಿಸಿ" @@ -135,6 +116,10 @@ "ಆ PIN ತಪ್ಪಾಗಿದೆ. ಮತ್ತೆ ಪ್ರಯತ್ನಿಸಿ." "ಮತ್ತೆ ಪ್ರಯತ್ನಿಸಿ, PIN ಹೊಂದಾಣಿಕೆಯಾಗುವುದಿಲ್ಲ" + "ನಿಮ್ಮ ಪಿನ್ ಕೋಡ್ ನಮೂದಿಸಿ." + "ಲೈವ್ ಚಾನಲ್‌ಗಳ ಅಪ್ಲಿಕೇಶನ್ ಟಿವಿ ಚಾನಲ್‌ಗಳಿಗೆ ಸಂಪೂರ್ಣ ಕಾರ್ಯಕ್ರಮ ಮಾರ್ಗದರ್ಶನವನ್ನು ಒದಗಿಸಲು ಪಿನ್ ಕೋಡ್ ಅನ್ನು ಬಳಸುತ್ತದೆ." + "ನಿಮ್ಮ ಪಿನ್ ಕೋಡ್ ನಮೂದಿಸಿ" + "ಅಮಾನ್ಯ ಪಿನ್‌ ಕೋಡ್" "ಸೆಟ್ಟಿಂಗ್‌ಗಳು" "ಚಾನಲ್‌ಪಟ್ಟಿ ಕಸ್ಟಮೈಸ್‌" "ನಿಮ್ಮ ಕಾರ್ಯಕ್ರಮ ಸೂಚಿಗಾಗಿ ಚಾನಲ್‌ಗಳನ್ನು ಆರಿಸಿ" @@ -143,6 +128,7 @@ "ಪೋಷಕ ನಿಯಂತ್ರಣಗಳು" "ಮುಕ್ತ ಮೂಲ ಪರವಾನಗಿಗಳು" "ಮುಕ್ತ ಮೂಲ ಪರವಾನಗಿಗಳು" + "ಪ್ರತಿಕ್ರಿಯೆ ಕಳುಹಿಸಿ" "ಆವೃತ್ತಿ" "ಈ ಚಾನಲ್ ಅನ್ನು ವೀಕ್ಷಿಸಲು, ಬಲಕ್ಕೆ ಒತ್ತಿ ಮತ್ತು ನಿಮ್ಮ PIN ಅನ್ನು ನಮೂದಿಸಿ" "ಈ ಕಾರ್ಯಕ್ರಮವನ್ನು ವೀಕ್ಷಿಸಲು, ಬಲಕ್ಕೆ ಒತ್ತಿ ಮತ್ತು ನಿಮ್ಮ PIN ಅನ್ನು ನಮೂದಿಸಿ" @@ -178,12 +164,10 @@ "ಸರಿ, ಅರ್ಥವಾಯಿತು" - "TV ಮೆನುವನ್ನು ಪ್ರವೇಶಿಸಲು ""ಆಯ್ಕೆ ಮಾಡು ಒತ್ತಿರಿ""." - "ಯಾವುದೇ TV ಇನ್‌ಪುಟ್ ಕಂಡುಬಂದಿಲ್ಲ" - "TV ಇನ್‌ಪುಟ್ ಹುಡುಕಲಾಗಲಿಲ್ಲ" - "PIP ಬೆಂಬಲಿತವಾಗಿಲ್ಲ" - "PIP ನೊಂದಿಗೆ ತೋರಿಸಬಹುದಾದ ಯಾವುದೇ ಇನ್‌ಪುಟ್ ಲಭ್ಯವಿಲ್ಲ" - "ಟ್ಯೂನರ್ ಪ್ರಕಾರವು ಹೊಂದಿಕೆಯಾಗುವುದಿಲ್ಲ. ದಯವಿಟ್ಟು ಟ್ಯೂನರ್ ಪ್ರಕಾರದ TV ಇನ್‌ಪುಟ್‌ಗೆ ಲೈವ್‌ ಚಾನಲ್‌ಗಳ ಅಪ್ಲಿಕೇಶನ್‌ ಪ್ರಾರಂಭಿಸಿ." + "ಟಿವಿ ಮೆನುವನ್ನು ಪ್ರವೇಶಿಸಲು ""ಆಯ್ಕೆ ಮಾಡು ಒತ್ತಿರಿ""." + "ಯಾವುದೇ ಟಿವಿ ಇನ್‌ಪುಟ್ ಕಂಡುಬಂದಿಲ್ಲ" + "ಟಿವಿ ಇನ್‌ಪುಟ್ ಹುಡುಕಲಾಗಲಿಲ್ಲ" + "ಟ್ಯೂನರ್ ಪ್ರಕಾರವು ಹೊಂದಿಕೆಯಾಗುವುದಿಲ್ಲ. ದಯವಿಟ್ಟು ಟ್ಯೂನರ್ ಪ್ರಕಾರದ ಟಿವಿ ಇನ್‌ಪುಟ್‌ಗೆ ಲೈವ್‌ ಚಾನಲ್‌ಗಳ ಅಪ್ಲಿಕೇಶನ್‌ ಪ್ರಾರಂಭಿಸಿ." "ಟ್ಯೂನ್ ವಿಫಲವಾಗಿದೆ" "ಈ ಕ್ರಿಯೆಯನ್ನು ನಿರ್ವಹಿಸಲು ಯಾವುದೇ ಅಪ್ಲಿಕೇಶನ್‌ ಕಂಡುಬಂದಿಲ್ಲ." "ಎಲ್ಲ ಮೂಲ ಚಾನಲ್‌ಗಳನ್ನು ಮರೆಮಾಡಲಾಗಿದೆ.\nವೀಕ್ಷಿಸಲು ಕನಿಷ್ಠ ಒಂದು ಚಾನಲ್‌‌ ಅನ್ನು ಆಯ್ಕೆಮಾಡಿ." @@ -258,9 +242,7 @@ "ಏಕ ಕಾಲದಲ್ಲಿ ರೆಕಾರ್ಡ್‌ ಮಾಡಲು ಸಾಕಷ್ಟು ಕಾರ್ಯಕ್ರಮಗಳು ಇದ್ದ ಸಂದರ್ಭದಲ್ಲಿ, ಹೆಚ್ಚಿನ ಆದ್ಯತೆ ಇರುವುದನ್ನು ಮಾತ್ರ ರೆಕಾರ್ಡ್‌ ಮಾಡಲಾಗುತ್ತದೆ." "ಉಳಿಸು" "ಒಂದು ಬಾರಿ ಮಾಡಿದ ರೆಕಾರ್ಡಿಂಗ್‌ಗಳು ಹೆಚ್ಚಿನ ಆದ್ಯತೆ ಹೊಂದಿರುತ್ತದೆ" - "ರದ್ದುಮಾಡು" - "ರದ್ದುಮಾಡಿ" - "ಮರೆತುಬಿಡು" + "ರದ್ದುಮಾಡಿ" "ನಿಲ್ಲಿಸು" "ರೆಕಾರ್ಡಿಂಗ್ ವೇಳಾಪಟ್ಟಿ ವೀಕ್ಷಿಸಿ" "ಈ ಏಕೈಕ ಪ್ರೋಗ್ರಾಂ" @@ -270,25 +252,29 @@ "ಬದಲಿಗೆ ಇದನ್ನು ರೆಕಾರ್ಡ್ ಮಾಡಿ" "ಈ ರೆಕಾರ್ಡಿಂಗ್ ರದ್ದುಗೊಳಿಸಿ" "ಈಗ ವೀಕ್ಷಿಸಿ" + "ರೆಕಾರ್ಡಿಂಗ್‌‌ಗಳನ್ನು ಅಳಿಸಿ..." "ರೆಕಾರ್ಡ್ ಮಾಡಬಹುದಾದ" "ರೆಕಾರ್ಡಿಂಗ್ ನಿಗದಿಪಡಿಸಲಾಗಿದೆ" "ರೆಕಾರ್ಡಿಂಗ್ ಸಂಘರ್ಷ" "ರೆಕಾರ್ಡ್ ಆಗುತ್ತಿದೆ" "ರೆಕಾರ್ಡಿಂಗ್ ವಿಫಲವಾಗಿದೆ" "ರೆಕಾರ್ಡಿಂಗ್ ವೇಳಾಪಟ್ಟಿಗಳನ್ನು ರಚಿಸಲು ಪ್ರೋಗ್ರಾಂಗಳನ್ನು ರೀಡ್‌ ಮಾಡಲಾಗುತ್ತಿದೆ" - "ಪ್ರೋಗ್ರಾಂಗಳನ್ನು ರೀಡ್‌ ಮಾಡಲಾಗುತ್ತಿದೆ" - - + "ಪ್ರೋಗ್ರಾಂಗಳನ್ನು ರೀಡ್‌ ಮಾಡಲಾಗುತ್ತಿದೆ" + "ಇತ್ತೀಚಿನ ರೆಕಾರ್ಡಿಂಗ್‌ಗಳನ್ನು ವೀಕ್ಷಿಸಿ" + "%1$s ನ ರೆಕಾರ್ಡಿಂಗ್ ಅಪೂರ್ಣವಾಗಿದೆ." + "%1$s ಮತ್ತು %2$s ನ ರೆಕಾರ್ಡಿಂಗ್‌ಗಳು ಅಪೂರ್ಣವಾಗಿವೆ." + "%1$s, %2$s ಮತ್ತು %3$s ನ ರೆಕಾರ್ಡಿಂಗ್‌ಗಳು ಅಪೂರ್ಣವಾಗಿವೆ." + "ಸಾಕಷ್ಟು ಸಂಗ್ರಹಣೆ ಇಲ್ಲದಿರುವ ಕಾರಣ %1$s ನ ರೆಕಾರ್ಡಿಂಗ್ ಪೂರ್ಣಗೊಂಡಿಲ್ಲ." + "ಸಾಕಷ್ಟು ಸಂಗ್ರಹಣೆ ಇಲ್ಲದಿರುವ ಕಾರಣ %1$s ಮತ್ತು %2$s ನ ರೆಕಾರ್ಡಿಂಗ್‌ಗಳು ಪೂರ್ಣಗೊಂಡಿಲ್ಲ." + "ಸಾಕಷ್ಟು ಸಂಗ್ರಹಣೆ ಇಲ್ಲದಿರುವ ಕಾರಣ %1$s, %2$s ಮತ್ತು %3$s ನ ರೆಕಾರ್ಡಿಂಗ್‌ಗಳು ಪೂರ್ಣಗೊಂಡಿಲ್ಲ." "DVR ಗೆ ಹೆಚ್ಚಿನ ಸಂಗ್ರಹಣೆಯ ಅಗತ್ಯವಿದೆ" "DVR ಮೂಲಕ ಪ್ರೋಗ್ರಾಂಗಳನ್ನು ರೆಕಾರ್ಡ್ ಮಾಡಲು ನಿಮಗೆ ಸಾಧ್ಯವಾಗುತ್ತದೆ. ಅದಾಗ್ಯೂ DVR ಗೆ ಕೆಲಸ ಮಾಡಲು ಇದೀಗ ನಿಮ್ಮ ಸಾಧನದಲ್ಲಿ ಸಾಕಷ್ಟು ಸ್ಥಳಾವಕಾಶವಿಲ್ಲ. ದಯವಿಟ್ಟು %1$sGB ಅಥವಾ ಅದಕ್ಕಿಂತ ಹೆಚ್ಚಿನ ಬಾಹ್ಯ ಡ್ರೈವ್‌ಗೆ ಸಂಪರ್ಕಪಡಿಸಿ ಮತ್ತು ಸಾಧನ ಸಂಗ್ರಹಣೆಯಂತೆ ಫಾರ್ಮ್ಯಾಟ್‌ ಮಾಡಲು ಹಂತಗಳನ್ನು ಅನುಸರಿಸಿ." + "ಸಾಕಷ್ಟು ಸಂಗ್ರಹಣೆಯಿಲ್ಲ" + "ಸಾಕಷ್ಟು ಸಂಗ್ರಹಣೆ ಇಲ್ಲದಿರುವ ಕಾರಣ ಈ ಕಾರ್ಯಕ್ರಮವನ್ನು ರೆಕಾರ್ಡ್ ಮಾಡಲಾಗುವುದಿಲ್ಲ. ಅಸ್ತಿತ್ವದಲ್ಲಿರುವ ಕೆಲವು ರೆಕಾರ್ಡಿಂಗ್ ಅಳಿಸಲು ಪ್ರಯತ್ನಿಸಿ." "ಸಂಗ್ರಹಣೆ ಕಾಣೆಯಾಗಿದೆ" - "DVR ಮೂಲಕ ಬಳಸಲಾದ ಕೆಲವು ಸಂಗ್ರಹಣೆಯು ಕಾಣೆಯಾಗಿದೆ. ನೀವು DVR ಮರು-ಸಕ್ರಿಯಗೊಳಿಸುವ ಮೊದಲು ಬಳಸಲಾದ ಬಾಹ್ಯ ಡ್ರೈವ್ ಅನ್ನು ಸಂಪರ್ಕಪಡಿಸಿ. ಪರ್ಯಾಯವಾಗಿ, ಇನ್ನೂ ಮುಂದೆ ಲಭ್ಯವಿಲ್ಲದಿದ್ದರೆ ಸಂಗ್ರಹಣೆಯನ್ನು ಮರೆಯಲು ನೀವು ಆಯ್ಕೆಮಾಡಬಹುದು." - "ಸಂಗ್ರಹಣೆಯನ್ನು ಮರೆತಿರುವಿರಾ?" - "ನಿಮ್ಮ ಎಲ್ಲಾ ರೆಕಾರ್ಡ್ ಮಾಡಲಾದ ವಿಷಯ ಮತ್ತು ವೇಳಾಪಟ್ಟಿಗಳು ಕಳೆದುಹೋಗುತ್ತವೆ." "ರೆಕಾರ್ಡಿಂಗ್ ನಿಲ್ಲಿಸುವುದೇ?" "ರೆಕಾರ್ಡ್‌ ಮಾಡಲಾದ ವಿಷಯವನ್ನು ಉಳಿಸಲಾಗುತ್ತದೆ." - - + "ಈ ಕಾರ್ಯಕ್ರಮದ ಜೊತೆಗೆ ರೆಕಾರ್ಡಿಂಗ್ ಸಂಘರ್ಷಿಸುವ ಕಾರಣದಿಂದಾಗಿ %1$s ರೆಕಾರ್ಡಿಂಗ್ ಅನ್ನು ನಿಲ್ಲಿಸಲಾಗುವುದು. ರೆಕಾರ್ಡ್ ಮಾಡಲಾದ ವಿಷಯವನ್ನು ಉಳಿಸಲಾಗುವುದು." "ರೆಕಾರ್ಡಿಂಗ್ ನಿಗದಿಪಡಿಸಲಾಗಿದೆ ಆದರೆ ಸಂಘರ್ಷಣೆಗಳನ್ನು ಹೊಂದಿದೆ" "ರೆಕಾರ್ಡಿಂಗ್ ಪ್ರಾರಂಭಿಸಲಾಗಿದೆ ಆದರೆ ಸಂಘರ್ಷಣೆಗಳನ್ನು ಹೊಂದಿದೆ" "%1$s ಅನ್ನು ರೆಕಾರ್ಡ್‌ ಮಾಡಲಾಗುತ್ತದೆ." @@ -306,14 +292,27 @@ "ಅದೇ ಕಾರ್ಯಕ್ರಮವನ್ನು ಈಗಾಗಲೇ %1$s ಸಮಯಕ್ಕೆ ರೆಕಾರ್ಡ್ ಮಾಡಲು ನಿಗದಿಪಡಿಸಲಾಗಿದೆ." "ಈಗಾಗಲೇ ರೆಕಾರ್ಡ್ ಮಾಡಲಾಗಿದೆ" "ಈ ಪ್ರೋಗ್ರಾಂ ಅನ್ನು ಈಗಾಗಲೇ ರೆಕಾರ್ಡ್‌ ಮಾಡಲಾಗಿದೆ. ಇದು DVR ಲೈಬ್ರರಿಯಲ್ಲಿ ಲಭ್ಯವಿದೆ." - - - - - - - - + "ಸರಣಿ ರೆಕಾರ್ಡಿಂಗ್ ನಿಗದಿಪಡಿಸಲಾಗಿದೆ" + + %2$s ಗೆ %1$d ರೆಕಾರ್ಡಿಂಗ್‌ಗಳನ್ನು ನಿಗದಿಪಡಿಸಲಾಗಿದೆ. + %2$s ಗೆ %1$d ರೆಕಾರ್ಡಿಂಗ್‌ಗಳನ್ನು ನಿಗದಿಪಡಿಸಲಾಗಿದೆ. + + + %2$s ಗೆ %1$d ರೆಕಾರ್ಡಿಂಗ್‌ಗಳನ್ನು ನಿಗದಿಪಡಿಸಲಾಗಿದೆ. ಸಂಘರ್ಷಗಳ ಕಾರಣದಿಂದಾಗಿ ಅವುಗಳಲ್ಲಿ %3$d ಅನ್ನು ರೆಕಾರ್ಡ್ ಮಾಡಲಾಗುವುದಿಲ್ಲ. + %2$s ಗೆ %1$d ರೆಕಾರ್ಡಿಂಗ್‌ಗಳನ್ನು ನಿಗದಿಪಡಿಸಲಾಗಿದೆ. ಸಂಘರ್ಷಗಳ ಕಾರಣದಿಂದಾಗಿ ಅವುಗಳಲ್ಲಿ %3$d ಅನ್ನು ರೆಕಾರ್ಡ್ ಮಾಡಲಾಗುವುದಿಲ್ಲ. + + + %2$s ಗೆ %1$d ರೆಕಾರ್ಡಿಂಗ್‌ಗಳನ್ನು ನಿಗದಿಪಡಿಸಲಾಗಿದೆ. ಸಂಘರ್ಷಗಳ ಕಾರಣದಿಂದಾಗಿ ಈ ಸರಣಿಯ ಮತ್ತು ಇತರ ಸರಣಿಯ %3$d ಸಂಚಿಕೆಗಳನ್ನು ರೆಕಾರ್ಡ್ ಮಾಡಲಾಗುವುದಿಲ್ಲ. + %2$s ಗೆ %1$d ರೆಕಾರ್ಡಿಂಗ್‌ಗಳನ್ನು ನಿಗದಿಪಡಿಸಲಾಗಿದೆ. ಸಂಘರ್ಷಗಳ ಕಾರಣದಿಂದಾಗಿ ಈ ಸರಣಿಯ ಮತ್ತು ಇತರ ಸರಣಿಯ %3$d ಸಂಚಿಕೆಗಳನ್ನು ರೆಕಾರ್ಡ್ ಮಾಡಲಾಗುವುದಿಲ್ಲ. + + + %2$s ಗೆ %1$d ರೆಕಾರ್ಡಿಂಗ್‌ಗಳನ್ನು ನಿಗದಿಪಡಿಸಲಾಗಿದೆ. ಸಂಘರ್ಷಗಳ ಕಾರಣದಿಂದಾಗಿ ಇತರ ಸರಣಿಯ 1 ಸಂಚಿಕೆಯನ್ನು ರೆಕಾರ್ಡ್ ಮಾಡಲಾಗುವುದಿಲ್ಲ. + %2$s ಗೆ %1$d ರೆಕಾರ್ಡಿಂಗ್‌ಗಳನ್ನು ನಿಗದಿಪಡಿಸಲಾಗಿದೆ. ಸಂಘರ್ಷಗಳ ಕಾರಣದಿಂದಾಗಿ ಇತರ ಸರಣಿಯ 1 ಸಂಚಿಕೆಯನ್ನು ರೆಕಾರ್ಡ್ ಮಾಡಲಾಗುವುದಿಲ್ಲ. + + + %2$s ಗೆ %1$d ರೆಕಾರ್ಡಿಂಗ್‌ಗಳನ್ನು ನಿಗದಿಪಡಿಸಲಾಗಿದೆ. ಸಂಘರ್ಷಗಳ ಕಾರಣದಿಂದಾಗಿ ಇತರ ಸರಣಿಯ %3$d ಸಂಚಿಕೆಗಳನ್ನು ರೆಕಾರ್ಡ್ ಮಾಡಲಾಗುವುದಿಲ್ಲ. + %2$s ಗೆ %1$d ರೆಕಾರ್ಡಿಂಗ್‌ಗಳನ್ನು ನಿಗದಿಪಡಿಸಲಾಗಿದೆ. ಸಂಘರ್ಷಗಳ ಕಾರಣದಿಂದಾಗಿ ಇತರ ಸರಣಿಯ %3$d ಸಂಚಿಕೆಗಳನ್ನು ರೆಕಾರ್ಡ್ ಮಾಡಲಾಗುವುದಿಲ್ಲ. + "ರೆಕಾರ್ಡ್‌ ಮಾಡಲಾದ ಕಾರ್ಯಕ್ರಮ ಕಂಡುಬಂದಿಲ್ಲ." "ಸಂಬಂಧಿಸಿದ ರೆಕಾರ್ಡಿಂಗ್‌ಗಳು" "(ಯಾವುದೇ ಪ್ರೋಗ್ರಾಂ ವಿವರಣೆಯಿಲ್ಲ)" @@ -336,6 +335,7 @@ "ಸರಣಿ ರೆಕಾರ್ಡಿಂಗ್ ನಿಲ್ಲಿಸುವುದೇ?" "ರೆಕಾರ್ಡ್‌ ಮಾಡಲಾದ ಭಾಗಗಳು DVR ಲೈಬ್ರರಿಯಲ್ಲಿ ಲಭ್ಯವಿರುತ್ತವೆ." "ನಿಲ್ಲಿಸಿ" + "ಈಗ ಪ್ರಸಾರ ಮಾಡಲು ಯಾವುದೇ ಸಂಚಿಕೆಗಳಿಲ್ಲ." "ಯಾವುದೇ ಸಂಚಿಕೆಗಳು ಲಭ್ಯವಿಲ್ಲ.\nಅವುಗಳು ಒಮ್ಮೆ ಲಭ್ಯವಾದಾಗ ಅವುಗಳನ್ನು ರೆಕಾರ್ಡ್‌ ಮಾಡಲಾಗುತ್ತದೆ." (%1$d ನಿಮಿಷಗಳು) diff --git a/res/values-ko/strings.xml b/res/values-ko/strings.xml index 4e9fa6a6..d3bc71cd 100644 --- a/res/values-ko/strings.xml +++ b/res/values-ko/strings.xml @@ -20,9 +20,8 @@ "모노" "스테레오" "재생 컨트롤" - "최근 시청한 채널" + "채널" "TV 옵션" - "PIP 옵션" "이 채널에서 재생 컨트롤을 사용할 수 없습니다." "재생 또는 일시중지" "빨리 감기" @@ -35,33 +34,15 @@ "자막" "표시 모드" "PIP" - "사용" - "사용 안함" "멀티 오디오" "채널 더보기" "설정" - "소스" - "전환" - "사용" - "사용 안함" - "소리" - "기본" - "PIP 창" - "레이아웃" - "오른쪽 하단" - "오른쪽 상단" - "왼쪽 상단" - "왼쪽 하단" - "나란히" - "크기" - "크게" - "작게" - "입력 소스" "TV(안테나/케이블)" "프로그램 정보가 없습니다." "정보 없음" "차단된 채널" - "알 수 없는 언어" + "알 수 없는 언어" + "%1$d 자막" "자막" "사용 안함" "포맷 맞춤설정" @@ -137,6 +118,10 @@ "PIN이 잘못되었습니다. 다시 시도해 주세요." "다시 시도해 주세요. PIN이 일치하지 않습니다." + "우편번호 입력" + "실시간 채널 앱에서 TV 채널에 관한 전체 프로그램 가이드를 제공하는 데 우편번호가 사용됩니다." + "우편번호를 입력하세요." + "잘못된 우편번호입니다." "설정" "채널 목록 맞춤설정" "프로그램 가이드용 채널을 선택합니다." @@ -145,6 +130,7 @@ "자녀 보호 기능" "오픈소스 라이선스" "오픈소스 라이선스" + "의견 보내기" "버전" "이 채널을 보려면 오른쪽을 누르고 PIN을 입력하세요." "이 프로그램을 보려면 오른쪽을 누르고 PIN을 입력하세요." @@ -183,8 +169,6 @@ "선택을 눌러"" TV 메뉴에 액세스합니다." "TV 입력이 없습니다." "TV 입력을 찾을 수 없습니다." - "PIP가 지원되지 않습니다." - "PIP로 표시할 수 있는 입력이 없습니다." "튜너 유형이 적합하지 않습니다. 튜너 유형 TV 입력에 실시간 채널 앱을 실행하세요." "조정에 실패했습니다." "이 작업을 처리하는 앱을 찾을 수 없습니다." @@ -261,8 +245,6 @@ "저장" "일회 녹화의 우선순위가 가장 높음" "취소" - "취소" - "삭제" "중지" "녹화 일정 보기" "이 프로그램만" @@ -272,25 +254,29 @@ "대신 이 항목을 녹화하기" "이 녹화 취소하기" "지금 보기" + "녹화된 프로그램 삭제 중..." "녹화 가능" "녹화 예약됨" "녹화 예약 충돌" "녹화 중" "녹화 실패" "프로그램을 확인하고 녹화 일정을 만듭니다." - "프로그램 정보 읽는 중" - - + "프로그램 정보 읽는 중" + "최근 녹화 보기" + "%1$s 녹화를 완료하지 못했습니다." + "%1$s, %2$s 녹화를 완료하지 못했습니다." + "%1$s, %2$s, %3$s 녹화를 완료하지 못했습니다." + "저장용량이 부족하여 %1$s 녹화를 완료하지 못했습니다." + "저장용량이 부족하여 %1$s, %2$s 녹화를 완료하지 못했습니다." + "저장용량이 부족하여 %1$s, %2$s, %3$s 녹화를 완료하지 못했습니다." "DVR에 저장용량이 더 필요합니다." "DVR을 사용하여 프로그램을 녹화할 수 있습니다. 그러나 DVR을 사용하기에는 기기에 남아있는 저장용량이 충분하지 않습니다. %1$sGB 이상의 외부 드라이브를 연결하고 외부 드라이브를 기기 저장소로 포맷하기 위한 단계를 따르세요." + "저장공간 부족" + "저장용량이 부족하여 이 프로그램을 녹화할 수 없습니다. 기존 녹화 프로그램 일부를 삭제해 주세요." "저장소 없음" - "DVR에 사용되던 일부 저장소가 없습니다. 사용하던 외부 드라이브를 연결한 다음 DVR을 다시 사용 설정하시기 바랍니다. 또는 저장소를 더 이상 사용할 수 없는 경우 삭제할 수 있습니다." - "저장소를 삭제하시겠습니까?" - "기록된 모든 콘텐츠 및 일정이 삭제됩니다." "녹화를 중지하시겠습니까?" "녹화된 콘텐츠가 저장됩니다." - - + "%1$s 녹화가 이 프로그램과 충돌하여 중지됩니다. 녹화된 콘텐츠는 저장됩니다." "녹화가 예약되었으나 충돌이 있음" "녹화가 시작되었으나 충돌이 있음" "%1$s이(가) 녹화됩니다." @@ -308,14 +294,27 @@ "동일한 프로그램이 이미 %1$s에 녹화 예약되었습니다." "이미 녹화됨" "이 프로그램이 이미 녹화되었습니다. DVR 라이브러리에서 사용할 수 있습니다." - - - - - - - - + "시리즈 녹화 예약됨" + + %2$s 녹화 %1$d개가 예약되었습니다. + %2$s 녹화 %1$d개가 예약되었습니다. + + + %2$s 녹화 %1$d개가 예약되었습니다. 충돌이 발생하여 이 중 %3$d개는 녹화되지 않습니다. + %2$s 녹화 %1$d개가 예약되었습니다. 충돌이 발생하여 녹화되지 않습니다. + + + %2$s 녹화 %1$d개가 예약되었습니다. 충돌이 발생하여 이 시리즈 및 다른 시리즈의 에피소드 %3$d개는 녹화되지 않습니다. + %2$s 녹화 %1$d개가 예약되었습니다. 충돌이 발생하여 이 시리즈 및 다른 시리즈의 에피소드 %3$d개는 녹화되지 않습니다. + + + %2$s 녹화 %1$d개가 예약되었습니다. 충돌이 발생하여 다른 시리즈의 에피소드 1개는 녹화되지 않습니다. + %2$s 녹화 %1$d개가 예약되었습니다. 충돌이 발생하여 다른 시리즈의 에피소드 1개는 녹화되지 않습니다. + + + %2$s 녹화 %1$d개가 예약되었습니다. 충돌이 발생하여 다른 시리즈의 에피소드 %3$d개는 녹화되지 않습니다. + %2$s 녹화 %1$d개가 예약되었습니다. 충돌이 발생하여 다른 시리즈의 에피소드 %3$d개는 녹화되지 않습니다. + "녹화된 프로그램을 찾을 수 없습니다." "관련 녹화" "(프로그램 설명 없음)" @@ -338,6 +337,7 @@ "시리즈 녹화를 중지하시겠습니까?" "녹화된 에피소드는 DVR 라이브러리에서 계속 사용할 수 있습니다." "중지" + "현재 방송 중인 에피소드가 없습니다." "사용할 수 있는 에피소드가 없습니다.\n사용 가능한 에피소드가 생기면 녹화됩니다." (%1$d분) diff --git a/res/values-ky-rKG/strings.xml b/res/values-ky-rKG/strings.xml index 2b9d37ab..600a2d38 100644 --- a/res/values-ky-rKG/strings.xml +++ b/res/values-ky-rKG/strings.xml @@ -20,9 +20,8 @@ "моно" "стерео" "Ойнотууну башкаруу" - "Акыркы каналдар" + "Каналдар" "Сынлг прметрлр" - "PIP параметрлери" "Бул канал үчүн ойнотуу көзөмөлдөрү жеткиликтүү эмес" "Ойнотуу же тындыруу" "Алдыга түрүү" @@ -35,33 +34,15 @@ "Жабык субттрлр" "Көрсөтүү режими" "PIP" - "Күйүк" - "Өчүк" "Мульти-аудио" "Дагы канлдрд алуу" "Жөндөөлөр" - "Булак" - "Алмаштыруу" - "Күйүк" - "Өчүк" - "Үн" - "Негизги" - "PIP терезеси" - "Үлгү" - "Төмөнкү оң" - "Жогорку оң" - "Жогорку сол" - "Төмөнкү сол" - "Тушма-туш" - "Өлчөмү" - "Чоң" - "Кичине" - "Киргизүү булагы" "Сыналгы (антенна/кабель)" "Программанын маалыматы жок" "Маалымат жок" "Бөгөттөлгөн канал" - "Белгисиз тил" + "Белгисиз тил" + "Коштомо жазуулар %1$d" "Коштомо жазуулар" "Өчүк" "Формттоону өзгөчөлшт" @@ -135,6 +116,10 @@ "Ал PIN туура эмес. Дагы бир жолу киргизиңиз." "PIN дал келбей жатат, дагы бир жолу аракет кылыңыз" + "Почтаңыздын индексин киргизиңиз." + "Түз ободогу каналдар колдонмосу сыналгы каналдары боюнча программалардын толук тизмесин түзүү үчүн почта индексин пайдаланат." + "Почтаңыздын индексин киргизиңиз" + "Почта индекси жараксыз" "Жөндөөлөр" "Канал тизмесин ыңгайлаштыруу" "Программа жетегиңиз үчүн каналдарды тандаңыз" @@ -143,6 +128,7 @@ "Ата-эненин көзөмөлү" "Ачык программа уруксаттамалары" "Ачык программа уруксаттамалары" + "Пикир билдирүү" "Версиясы" "Бул каналды көрүү үчүн, Оңго басып, PIN-иңизди киргизиңиз" "Бул программаны көрүү үчүн, Оңго басып, PIN-иңизди киргизиңиз" @@ -181,8 +167,6 @@ "Сыналгынын менюсун ачуу үчүн ""ТАНДОО"" баскычын басыңыз." "Сыналгыга киргизме табылган жок" "Сыналгыга киргизме табылбай жатат" - "PIP колдоого алынбайт" - "PIP аркылуу көрсөтүлө турган эч нерсе киргизилген жок." "Тюнердин түрү ылайыксыз. Тюнердин түрүндөгү сыналгы киргизмеси үчүн Жандуу каналдар колдонмосун ишке киргизиңиз." "Жөндөлбөй калды" "Бул ишти аткара турган бир дагы колдонмо табылган жок." @@ -259,8 +243,6 @@ "Сактоо" "Бир жолку жаздыруулар жогорку артыкчылыкка ээ болот" "Баш тартуу" - "Жокко чыгаруу" - "Унутуу" "Токтотуу" "Жаздыруу графигин көрүү" "Жалгыз ушул программа" @@ -270,25 +252,29 @@ "Анын ордуна бул жаздырылсын" "Бул жаздыруу жокко чыгарылсын" "Азыр көрүңүз" + "Жаздырылган көрсөтүүлөрдү өчүрүү" "Жаздырууга болот" "Пландаштырылган жаздыруу" "Жаздырууда дал келбестик бар" "Жаздырууда" "Жаздырылбай калды" "Ырааттамаларды жаздырууну түзүү үчүн программаларды окуу" - "Программалар окулууда" - - + "Программалар окулууда" + "Акыркы жаздырууларды көрүңүз" + "%1$s жаздырылып бүтпөй калды." + "%1$s жана %2$s жаздырылып бүтпөй калды." + "%1$s, %2$s жана %3$s жаздырылып бүтпөй калды." + "Сактагычта орун жетишсиз болгондуктан %1$s жаздырылып бүтпөй калды." + "Сактагычта орун жетишсиз болгондуктан %1$s жана %2$s жаздырылып бүтпөй калды." + "Сактагычта орун жетишсиз болгондуктан %1$s, %2$s жана %3$s жаздырылып бүтпөй калды." "DVR көбүрөөк орунду талап кылат" "Сиз DVR менен программаларды жаздыра аласыз. Бирок, DVR\'ды иштетүү үчүн түзмөгүңүздө бош орун калбай калды. Көлөмү %1$sГб же андан ашык болгон тышкы драйверге туташыңыз да, аны түзмөктүн сактагычы катары жөндөп алыңыз." + "Сактагычта орун жетишсиз" + "Сактагычта орун жетишсиз болгондуктан, бул көрсөтүү жаздырылбайт. Мурда жаздырылган көрсөтүүлөрдү өчүрүп көрүңүз." "Сактагыч жок болуп жатат" - "DVR колдонгон сактагычтын айрым бөлүмдөрү жок болуп жатат. DVR\'ды кайра иштетүү үчүн колдонулган тышкы драйверди туташтырыңыз. Же болбосо, жеткиликсиз сактагыч унутулсун дегенди тандасаңыз болот." - "Сактагыч унутулсунбу?" - "Бардык жаздырылган мазмун жана графиктериңиз жоголот." "Жаздыруу токтотулсунбу?" "Тартылган мазмун сакталып калат." - - + "Убакыты бул программаныкына дал келип калгандыктан %1$s жаздыруусу токтотулат. Жаздырылган мазмун сакталып калат." "Пландаштырылган жаздыруу, бирок бир убакытка коюлуп калган" "Жаздыруу башталды, бирок башка программа менен дал келип калган" "%1$s жаздырылат." @@ -306,14 +292,27 @@ "Ушул эле программа буга чейин %1$s жаздыруу графигине кошулган." "Буга чейин жаздырылган" "Бул программа буга чейин жазылган. Ал DVR китепканасында жеткиликтүү." - - - - - - - - + "Сериалдын жаздыруусу пландаштырылды" + + %2$s сериалын %1$d жолу жаздыруу пландаштырылган. + %2$s сериалын %1$d жолу жаздыруу пландаштырылган. + + + %2$s сериалын %1$d жолу жаздыруу пландаштырылган. Убакыттары дал келип калгандыктан, алардын %3$d сериясы жаздырылбайт. + %2$s сериалын %1$d жолу жаздыруу пландаштырылган. Убакыты дал келип калгандыктан, ал жаздырылбайт. + + + %2$s сериалын %1$d жолу жаздыруу пландаштырылган. Убакыттары дал келип калгандыктан, бул сериалдын %3$d сериясы жана башка сериал жаздырылбайт. + %2$s сериалын %1$d жолу жаздыруу пландаштырылган. Убакыттары дал келип калгандыктан, бул сериалдын %3$d сериясы жана башка сериал жаздырылбайт. + + + %2$s сериалын %1$d жолу жаздыруу пландаштырылган. Убакыттары дал келип калгандыктан, башка сериалдын 1 сериясы жаздырылбайт. + %2$s сериалын %1$d жолу жаздыруу пландаштырылган. Убакыттары дал келип калгандыктан, башка сериалдын 1 сериясы жаздырылбайт. + + + %2$s сериалын %1$d жолу жаздыруу пландаштырылган. Убакыттары дал келип калгандыктан, башка сериалдын %3$d сериясы жаздырылбайт. + %2$s сериалын %1$d жолу жаздыруу пландаштырылган. Убакыттары дал келип калгандыктан, башка сериалдын %3$d сериясы жаздырылбайт. + "Жаздырылган программа табылган жок." "Окшош жаздыруулар" "(Программанын сүрөттөлүшү жок)" @@ -336,6 +335,7 @@ "Сериал тартуу токтотулсунбу?" "Тартылган сериялар DVR китепканасында сакталып калат." "Токтотуу" + "Азыр эч кандай серия көрсөтүлбөй турат" "Учурда бир да эпизод жок.\nАлар жеткиликтүү болоору менен жаздырылат." (%1$d мүнөт) diff --git a/res/values-lo-rLA/strings.xml b/res/values-lo-rLA/strings.xml index 05aba1ce..176de1f7 100644 --- a/res/values-lo-rLA/strings.xml +++ b/res/values-lo-rLA/strings.xml @@ -20,9 +20,8 @@ "​ໂມ​ໂນ" "ສະ​ເຕ​ຣິ​ໂອ" "ຄວບ​ຄຸມ​ການ​ຫຼິ້ນ" - "ຊ່ອງ​ບໍ່ດົນ​ມານີ້" + "​ຊ່ອງ" "ໂຕເລືອກ​ໂທລະພາບ" - "​ຕົວ​ເລືອກ PIP" "ຫຼິ້ນ​ການ​ຄວບ​ຄຸມ​ບໍ່​ມີ​ໃຫ້​ສຳ​ລັບ​ຊ່ອງ​ນີ້" "ຫຼິ້ນ​ ຫລື​ຢຸດ​ຊົ່ວ​ຄາວ" "ເລື່ອນ​ໄປ​ໜ້າ" @@ -35,33 +34,15 @@ "ຄຳ​ບັນຍາຍ​ແບບ​ປິດ" "ໂໝດ​ການ​ສະແດງຜົນ" "PIP" - "​ເປີດ" - "ປິດ" "ຫຼາຍສຽງ" "ເອົາຊ່ອງເພີ່ມເຕີມ" "ການ​ຕັ້ງ​ຄ່າ" - "ທີ່​ມາ" - "ສະຫຼັບ" - "​ເປີດ" - "ປິດ" - "ສຽງ" - "ຫຼັກ" - "ໜ້າ​ຕ່າງ PIP" - "ຮູບ​ແບບ" - "ເບື້ອງ​ຂວາ​ທາງ​ລຸ່ມ" - "ເບື້ອງ​ຂວາ​ດ້ານ​ເທິງ" - "ເບື້ອງ​ຊ້າຍ​ດ້ານ​ເທິງ" - "ເບື້ອງ​ຊ້າຍທາງ​ລຸ່ມ" - "ເທື່ອ​ລະ​ດ້ານ" - "ຂະໜາດ" - "ໃຫຍ່" - "ນ້ອຍ" - "​ແຫຼ່ງ​ສັນ​ຍານ" "TV (ເສົາ​ສັນ​ຍານ/ສາຍ​ເຄ​ເບິນ)" "ບໍ່​ມີ​ຂໍ້​ມູນ​ລາຍ​ການ" "ບໍ່ມີຂໍ້ມູນ" "ຊ່ອງ​ທີ່​ຖືກບ​ລັອກ" - "ພາ​ສາ​ບໍ່​ຮູ້​ຈັກ" + "ພາ​ສາ​ບໍ່​ຮູ້​ຈັກ" + "ຄຳບັນຍາຍ %1$d" "ຄຳ​ບັນຍາຍ​ແບບ​ປິດ" "ປິດ" "ປັບແຕ່ງການຈັດຮູບແບບ" @@ -135,6 +116,10 @@ "ລະຫັດ PIN ນັ້ນບໍ່​ຖືກ​ຕ້ອງ, ກະ​ລຸ​ນາລອງ​ໃຫມ່​ອີກ​ຄັ້ງ." "ລະ​ຫັດ PIN ບໍ່​ກົງ​ກັນ, ກະ​ລຸ​ນາ​ລອງ​ໃໝ່." + "ລະບຸລະຫັດ ZIP ຂອງທ່ານ." + "ແອັບ Live TV ຈະໃຊ້ລະຫັດ ZIP ເພື່ອສະໜອງຄຳແນະນຳລາຍການທີ່ສົມບູນໃຫ້ກັບຊ່ອງໂທລະທັດຕ່າງໆ." + "ໃສ່ລະຫັດ ZIP ຂອງທ່ານ" + "ລະຫັດ ZIP ບໍ່ຖືກຕ້ອງ" "ການ​ຕັ້ງ​ຄ່າ" "ປັບແຕ່ງ​ລາຍ​ຊື່​ຊ່ອງ" "ເລືອກ​ຊ່ອງ​ສຳລັບການ​ແນະນຳລາຍການ​ຂອງ​ທ່ານ" @@ -143,6 +128,7 @@ "ການ​ຄວບຄຸມ​ຂອງພໍ່ແມ່" "​ໃບ​ອະ​ນຸ​ຍາດ​ Open source" "​ໃບ​ອະ​ນຸ​ຍາດ​ແຫຼ່ງ​ເປີດ" + "ສົ່ງຄຳຕິຊົມ" "ເວີຊັນ" "​ເພື່ອ​ເບິ່ງ​ຊ່ອງ​ນີ້, ກົດ​ປຸ່ມ ຂວາ ແລະ ປ້ອນ​ລະ​ຫັດ PIN ຂອງ​ທ່ານ" "​ເພື່ອ​ເບິ່ງ​ລາຍ​ການ​ນີ້, ໃຫ້ກົດ​ປຸ່ມ ຂວາ ແລະ ປ້ອນ​ລະ​ຫັດ PIN ຂອງ​ທ່ານ" @@ -181,8 +167,6 @@ "ກົດ SELECT"" ເພື່ອ​ເຂົ້າ​ຫາ​ເມ​ນູ TV." "ບໍ່​ພົບການ​ປ້ອນ​ເຂົ້າ TV" "ບໍ່​ສາ​ມາດ​ຊອກ​ຫາການ​ປ້ອນ​ເຂົ້າ TV ​ໄດ້" - "ບໍ່​ຮອງ​ຮັບ PIP" - "ບໍ່ມີການຮັບສັນຍານທີ່ສາມາດສະແດງໄດ້ດ້ວຍຮູບພາບຊ້ອນຮູບພາບ PIP" "ປະ​ເພດ​ເຄື່ອງ​ຈູນ​ບໍ່​ເໝາະ​ສົມ; ກະ​ລຸ​ນາ​ເປີດ​ໃຊ້​ແອັບ​ຊ່ອງ​ສົດ​ສຳ​ລັບ​ການ​ປ້ອນ​ເຂົ້າ TV ປະ​ເພດ​ເຄື່ອງ​ຈູນ." "ການ​ປັບ​ຊ່ອງ​ລົ້ມ​ເຫລວ" "ບໍ່ພົບແອັບຯທີ່ໃຊ້ເພື່ອດຳເນີນການ." @@ -259,8 +243,6 @@ "ບັນທຶກ" "ການບັນທຶກຕາມເວລາມີຄວາມສຳຄັນສູງສຸດ" "​ຍົກເລີກ" - "ຍົກເລີກ" - "ລືມ" "ຢຸດ" "ເບິ່ງຕາຕາລາງການບັນທຶກ" "ນີ້ເປັນລາຍການດ່ຽວ" @@ -270,25 +252,29 @@ "ບັນທຶກອັນນີ້ແທນ" "ຍົກເລີກການບັນທຶກນີ້" "ເບິ່ງດຽວນີ້" + "ລຶບການບັນທຶກອອກ…" "ບັນທຶກໄດ້" "ຕັ້ງເວລາບັນທຶກແລ້ວ" "ເກີດຄວາມຂັດແຍ່ງໃນການບັນທຶກ" "ກຳລັງບັນທຶກ" "ບັນທຶກບໍ່ສຳເລັດ" "ກຳລັງອ່ານເນື້ອຫາລາຍການເພື່ອຕັ້ງເວລາບັນທຶກ" - "ກຳລັງອ່ານລາຍການ" - - + "ກຳລັງອ່ານລາຍການ" + "ເບິ່ງການບັນທຶກຫຼ້າສຸດ" + "ບັນທຶກ %1$s ບໍ່ສົມບູນ." + "ບັນທຶກ %1$s ແລະ %2$s ບໍ່ສົມບູນ." + "ບັນທຶກ %1$s, %2$s ແລະ %3$s ບໍ່ສົມບູນ." + "ບັນທຶກ %1$s ບໍ່ສຳເລັດເນື່ອງຈາກບ່ອນຈັດເກັບຂໍ້ມູນບໍ່ພຽງພໍ." + "ບັນທຶກ %1$s ແລະ %2$s ບໍ່ສຳເລັດເນື່ອງຈາກບ່ອນຈັດເກັບຂໍ້ມູນບໍ່ພຽງພໍ." + "ບັນທຶກ %1$s, %2$s ແລະ %3$s ບໍ່ສຳເລັດເນື່ອງຈາກບ່ອນຈັດເກັບຂໍ້ມູນບໍ່ພຽງພໍ." "DVR ຕ້ອງໃຊ້ບ່ອນຈັດເກັບຂໍ້ມູນເພີ່ມເຕີມ" "ທ່ານຈະສາມາດບັນທຶກລາຍການດ້ວຍ DVR ໄດ້. ຢ່າງໃດກໍຕາມ, ອຸປະກອນຂອງທ່ານບໍ່ມີບ່ອນຈັດເກັບຂໍ້ມູນພຽງພໍໃຫ້ DVR ເຮັດວຽກໄດ້. ກະລຸນາເຊື່ອມຕໍ່ຫາໄດຣຟ໌ພາຍນອກທີ່ມີຂະໜາດ %1$sGB ຫຼືໃຫຍ່ກວ່າ ແລ້ວເຮັດຕາມຂັ້ນຕອນໃນການໃຊ້ມັນເປັນອຸປະກອນຈັດເກັບຂໍ້ມູນ." + "ບ່ອນຈັດເກັບຂໍ້ມູນບໍ່ພຽງພໍ" + "ຈະບໍ່ບັນທຶກລາຍການນີ້ເນື່ອງຈາກມີບ່ອນຈັດເກັບຂໍ້ມູນບໍ່ພຽງພໍ. ໃຫ້ລອງລຶບການບັນທຶກບາງອັນອອກກ່ອນ." "ບໍ່ພົບບ່ອນຈັດເກັບຂໍ້ມູນ" - "ບໍ່ພົບບ່ອນຈັດເກັບຂໍ້ມູນບາງອັນທີ່ໃຊ້ໂດຍ DVR. ກະລຸນາເຊື່ອມຕໍ່ໄດຣຟ໌ພາຍນອກທີ່ທ່ານໃຊ້ກ່ອນຈະເປີດໃຊ້ DVR ຄືນໃໝ່. ຫຼືອີກວິທີໜຶ່ງ, ທ່ານສາມາດເລືອກໃຫ້ລືມບ່ອນຈັດເກັບຂໍ້ມູນດັ່ງກ່າວໄດ້ຫາກມັນບໍ່ມີໃຫ້ໃຊ້ອີກຕໍ່ໄປແລ້ວ." - "ລືມການຈັດເກັບຂໍ້ມູນບໍ?" - "ທ່ານຈະສູນເສຍເນື້ອຫາ ແລະ ການຕັ້ງເວລາທັງໝົດທີ່ທ່ານບັນທຶກໄວ້ແລ້ວ." "ຢຸດການບັນທຶກໄວ້ບໍ?" "ເນື້ອຫາທີ່ອັດໄວ້ແລ້ວຈະຖືກບັຍທຶກໄວ້." - - + "ການບັນທຶກ %1$s ຈະຖືກຢຸດໄວ້ເພາະມັນຂັດແຍ່ງກັບລາຍການນີ້. ເນື້ອຫາທີ່ບັນທຶກໄປແລ້ວຈະຖືກຈັດເກັບໄວ້." "ຕັ້ງເວລາການບັນທຶກແລ້ວແຕ່ມີຂໍ້ຂັດແຍ່ງເກີດຂຶ້ນ" "ເລີ່ມການບັນທຶກແລ້ວແຕ່ມີຂໍ້ຂັດແຍ່ງ" "%1$s ຈະຖືກບັນທຶກໄວ້." @@ -306,14 +292,27 @@ "ລາຍການດຽວກັນນີ້ໄດ້ຕັ້ງເວລາໃຫ້ບັນທຶກໄວ້ແລ້ວເວລາ %1$s." "ບັນທຶກໄປກ່ອນແລ້ວ" "ລາຍການນີ້ຖືກບັນທຶກໄປກ່ອນແລ້ວ. ມັນສາມາດເບິ່ງໄດ້ໃນຫ້ອງສະໝຸດ DVR." - - - - - - - - + "ຕັ້ງເວລາບັນທຶກຊີຣີແລ້ວ" + + ຕັ້ງເວລາບັນທຶກ %1$d ລາຍການສຳລັບ %2$s ແລ້ວ. + ຕັ້ງເວລາບັນທຶກ %1$d ລາຍການສຳລັບ %2$s ແລ້ວ. + + + ຕັ້ງເວລາບັນທຶກ %1$d ລາຍການສຳລັບ %2$s ແລ້ວ. %3$d ລາຍການຈະບໍ່ຖືກບັນທຶກເນື່ອງຈາກມີຂໍ້ຂັດແຍ່ງ. + ຕັ້ງເວລາບັນທຶກ %1$d ລາຍການສຳລັບ %2$s ແລ້ວ. ມັນຈະບໍ່ຖືກບັນທຶກເນື່ອງຈາກມີຂໍ້ຂັດແຍ່ງ. + + + ຕັ້ງເວລາບັນທຶກ %1$d ລາຍການສຳລັບ %2$s ແລ້ວ. ຈະບໍ່ບັນທຶກເອັບພິໂສດ %3$d ຕອນຂອງຊີຣີອື່ນເນື່ອງຈາກເວລາຂັດແຍ່ງກັນ. + ຕັ້ງເວລາບັນທຶກ %1$d ລາຍການສຳລັບ %2$s ແລ້ວ. ຈະບໍ່ບັນທຶກເອັບພິໂສດ %3$d ຕອນຂອງຊີຣີອື່ນເນື່ອງຈາກເວລາຂັດແຍ່ງກັນ. + + + ຕັ້ງເວລາບັນທຶກ %1$d ລາຍການສຳລັບ %2$s ແລ້ວ. ຈະບໍ່ບັນທຶກເອັບພິໂສດ 1 ຕອນຂອງຊີຣີອື່ນເນື່ອງຈາກເວລາຂັດແຍ່ງກັນ. + ຕັ້ງເວລາບັນທຶກ %1$d ລາຍການສຳລັບ %2$s ແລ້ວ. ຈະບໍ່ບັນທຶກເອັບພິໂສດ 1 ຕອນຂອງຊີຣີອື່ນເນື່ອງຈາກເວລາຂັດແຍ່ງກັນ. + + + ຕັ້ງເວລາບັນທຶກ %1$d ລາຍການສຳລັບ %2$s ແລ້ວ. ຈະບໍ່ບັນທຶກເອັບພິໂສດ %3$d ຕອນຂອງຊີຣີອື່ນເນື່ອງຈາກເວລາຂັດແຍ່ງກັນ. + ຕັ້ງເວລາບັນທຶກ %1$d ລາຍການສຳລັບ %2$s ແລ້ວ. ຈະບໍ່ບັນທຶກເອັບພິໂສດ %3$d ຕອນຂອງຊີຣີອື່ນເນື່ອງຈາກເວລາຂັດແຍ່ງກັນ. + "ບໍ່ພົບໂປຣແກຣມທີ່ບັນທຶໄວ້." "ການບັນທຶກທີ່ກ່ຽວຂ້ອງ" "(ບໍ່ມີຄຳອະທິບາຍລາຍການ)" @@ -336,6 +335,7 @@ "ຢຸດການບັນທຶກຊີຣີບໍ່?" "ເອັບພິໂສດທີ່ບັນທຶກໄວ້ແລ້ວຈະຍັງຄົງສາມາດເບິ່ງໄດ້ໃນຫ້ອງສະໝຸດ DVR ຢູ່." "ຢຸດ" + "ບໍ່ມີເອັບພິໂສດໃດທີ່ກຳລັງສາຍຢູ່ໃນຕອນນີ້." "ຍັງບໍ່ມີເອບພິໂສດທີ່ສາມາດເບິ່ງໄດ້ເທື່ອ.\nພວກມັນຈະຖືກບັນທຶອຶກເມື່ອມີການສາຍ." (%1$d ນາທີ) diff --git a/res/values-lt/strings.xml b/res/values-lt/strings.xml index 88afdec0..acbe31cb 100644 --- a/res/values-lt/strings.xml +++ b/res/values-lt/strings.xml @@ -20,9 +20,8 @@ "monofon." "stereof." "Leidimo valdikliai" - "Naujausi kanal." + "Kanalai" "TV parinktys" - "PIP parinktys" "Leidimo valdikliai negalimi šiame kanale" "Leisti arba pristabdyti" "Sukti pirmyn" @@ -35,33 +34,15 @@ "Subtitrai" "Rodymo režimas" "PIP" - "Įjungta" - "Išjungta" "Keli garso įr." "Gauti daug. kan." "Nustatymai" - "Šaltinis" - "Sukeisti" - "Įjungta" - "Išjungta" - "Garsas" - "Pagrindinis" - "PIP langas" - "Išdėstymas" - "Apač. dešinėje" - "Virš. dešinėje" - "Viršuje kairėje" - "Apač. kairėje" - "Šalia" - "Dydis" - "Didelis" - "Mažas" - "Įvesties šaltinis" "TV (antena / kabelis)" "Nėra informacijos apie programą" "Informacijos nėra." "Užblokuotas kanalas" - "Nežinoma kalba" + "Nežinoma kalba" + "Subtitrai %1$d" "Subtitrai" "Išjungti" "Tinkinti formatavimą" @@ -139,6 +120,10 @@ "Tas PIN kodas buvo netinkamas. Bandykite dar kartą." "Bandykite dar kartą, PIN kodas neatitinka" + "Įveskite pašto kodą." + "Tiesioginių kanalų programa naudos pašto kodą, kad galėtų pateikti išsamų TV kanalų programų vadovą." + "Įveskite pašto kodą" + "Netinkamas pašto kodas" "Nustatymai" "Tinkinti kanalų sąrašą" "Pasirinkite programų vadovo kanalus" @@ -147,6 +132,7 @@ "Tėvų kontrolė" "Atvirojo šaltinio licencijos" "Atvirojo šaltinio licencijos" + "Siųsti atsiliepimą" "Versija" "Jei norite žiūrėti šį kanalą, paspauskite „Tinkamas“ ir įveskite PIN kodą" "Jei norite žiūrėti šią programą, paspauskite „Tinkama“ ir įveskite PIN kodą" @@ -189,8 +175,6 @@ "Paspauskite PASIRINKTI,"" kad pasiektumėte TV meniu." "Nerasta jokių TV įvesčių" "Nepavyksta rasti TV įvesties" - "PIP nepalaikoma" - "Nėra galimos įvesties, kurią galima rodyti kartu su PIP" "Netinkamas derintuvo tipas. Paleiskite derintuvo tipo TV įvestį, skirtą programai „Live TV“." "Derinimas nepavyko" "Nerasta jokių programų šiam veiksmui apdoroti." @@ -279,8 +263,6 @@ "Išsaugoti" "Vienkartinis įrašymo veiksmas turi didžiausią prioritetą" "Atšaukti" - "Atšaukti" - "Pamiršti" "Sustabdyti" "Žr. įrašymo tvarkaraštį" "Ši viena programa" @@ -290,25 +272,30 @@ "Vietoj tos įrašyti šią" "Atšaukti šį įrašymą" "Žiūrėti dabar" + "Ištrinti įrašus…" "Galima įrašyti" "Įrašymas suplanuotas" "Įrašo nesuderinamumas" "Įrašoma" "Įrašyti nepavyko" "Nuskaitomos programos, kad būtų sukurti įrašymo tvarkaraščiai" - "Nuskaitomos programos" - + "Nuskaitomos programos" + + "„%1$s“ įrašymo procesas nebaigtas." + "„%1$s“ ir „%2$s“ įrašymo procesas nebaigtas." + "„%1$s“, „%2$s“ ir „%3$s“ įrašymo procesas nebaigtas." + "„%1$s“ įrašymo procesas nebaigtas, nes nepakanka saugyklos vietos." + "„%1$s“ ir „%2$s“ įrašymo procesas nebaigtas, nes nepakanka saugyklos vietos." + "„%1$s“, „%2$s“ ir „%3$s“ įrašymo procesas nebaigtas, nes nepakanka saugyklos vietos." "Norint naudoti DVR reikia daugiau saugyklos vietos" "Naudodami DVR galėsite įrašyti programas. Tačiau dabar įrenginyje nepakanka saugyklos vietos, kad DVR veiktų. Prijunkite išorinį diską, kuris yra %1$s GB arba didesnis, ir atlikite veiksmus, kad formatuotumėte jį kaip įrenginio saugyklą." + "Trūksta saugyklos vietos" + "Ši programa nebus įrašyta, nes nepakanka saugyklos vietos. Pabandykite ištrinti kai kuriuos esamus įrašus." "Nėra saugyklos" - "Nėra kai kurių saugyklų, kurias naudoja DVR. Prijunkite anksčiau naudotą išorinį diską, kad iš naujo įgalintumėte DVR. Taip pat galite pasirinkti pamiršti saugyklą, jei ji nebepasiekiama." - "Pamiršti saugyklą?" - "Visas įrašytas turinys ir tvarkaraščiai bus prarasti." "Sustabdyti įrašymą?" "Įrašytas turinys bus išsaugotas." - - + "„%1$s“ įrašas bus sustabdytas dėl prieštaravimų su šia programa. Įrašytas turinys bus išsaugotas." "Įrašymas suplanuotas, bet yra neatitikimų" "Įrašymo procesas pradėtas, bet yra neatitikimų" "Programa „%1$s“ bus įrašyta." @@ -328,14 +315,37 @@ "Ta pati programa jau suplanuota įrašyti %1$s." "Jau įrašyta" "Ši programa jau buvo įrašyta. Ji pasiekiama DVR bibliotekoje." - - - - - - - - + "Suplanuotas serijos įrašas" + + Suplanuotas %1$d%2$s“ įrašas. + Suplanuoti %1$d%2$s“ įrašai. + Suplanuota %1$d%2$s“ įrašo. + Suplanuota %1$d%2$s“ įrašų. + + + Suplanuotas %1$d%2$s“ įrašas. %3$d iš jų nebus įraš. dėl nesuderinamo tvarkaraščio. + Suplanuoti %1$d%2$s“ įrašai. %3$d iš jų nebus įraš. dėl nesuderinamo tvarkaraščio. + Suplanuota %1$d%2$s“ įrašo. %3$d iš jų nebus įraš. dėl nesuderinamo tvarkaraščio. + Suplanuota %1$d%2$s“ įrašų. %3$d iš jų nebus įraš. dėl nesuderinamo tvarkaraščio. + + + Suplanuotas %1$d%2$s“ įrašas. Šio ir kitų serialų serijos (%3$d) nebus įrašytos dėl nesuderinamo tvarkaraščio. + Suplanuoti %1$d%2$s“ įrašai. Šio ir kitų serialų serijos (%3$d) nebus įrašytos dėl nesuderinamo tvarkaraščio. + Suplanuota %1$d%2$s“ įrašo. Šio ir kitų serialų serijos (%3$d) nebus įrašytos dėl nesuderinamo tvarkaraščio. + Suplanuota %1$d%2$s“ įrašų. Šio ir kitų serialų serijos (%3$d) nebus įrašytos dėl nesuderinamo tvarkaraščio. + + + Suplanuotas %1$d%2$s“ įrašas. 1 kitų serialų serija nebus įrašyta dėl nesuderinamo tvarkaraščio. + Suplanuoti %1$d%2$s“ įrašai. 1 kitų serialų serija nebus įrašyta dėl nesuderinamo tvarkaraščio. + Suplanuota %1$d%2$s“ įrašo. 1 kitų serialų serija nebus įrašyta dėl nesuderinamo tvarkaraščio. + Suplanuota %1$d%2$s“ įrašų. 1 kitų serialų serija nebus įrašyta dėl nesuderinamo tvarkaraščio. + + + Suplanuotas %1$d%2$s“ įrašas. Kitų serialų serijos (%3$d) nebus įrašytos dėl nesuderinamo tvarkaraščio. + Suplanuoti %1$d%2$s“ įrašai. Kitų serialų serijos (%3$d) nebus įrašytos dėl nesuderinamo tvarkaraščio. + Suplanuota %1$d%2$s“ įrašo. Kitų serialų serijos (%3$d) nebus įrašytos dėl nesuderinamo tvarkaraščio. + Suplanuota %1$d%2$s“ įrašų. Kitų serialų serijos (%3$d) nebus įrašytos dėl nesuderinamo tvarkaraščio. + "Įrašyta programa nerasta." "Susiję įrašai" "(Nėra laidos aprašo)" @@ -362,6 +372,8 @@ "Sustabdyti serijos įrašymą?" "Įrašytos serijos bus pasiekiamos DVR bibliotekoje." "Sustabdyti" + + "Nėra jokių serijų.\nSerijos bus įrašytos, kai jų bus." (%1$d minutė) diff --git a/res/values-lv/strings.xml b/res/values-lv/strings.xml index 00021bca..39a5a380 100644 --- a/res/values-lv/strings.xml +++ b/res/values-lv/strings.xml @@ -20,9 +20,8 @@ "mono" "stereo" "Atskaņošanas vadīklas" - "Nesenie kanāli" + "Kanāli" "TV iespējas" - "PIP opcijas" "Šim kanālam nav pieejamas atskaņošanas vadīklas." "Atskaņot vai pauzēt" "Pārtīt uz priekšu" @@ -35,33 +34,15 @@ "Slēgtie paraksti" "Attēla režīms" "PIP" - "Ieslēgts" - "Izslēgts" "Multiaudio" "Vairāk kanālu" "Iestatījumi" - "Avots" - "Mainīt" - "Ieslēgts" - "Izslēgts" - "Skaņa" - "Galvenais" - "PIP logs" - "Izkārtojums" - "Apakšā pa labi" - "Augšā pa labi" - "Augšā pa kreisi" - "Apakšā pa kreisi" - "Līdzās" - "Izmēri" - "Liels" - "Mazs" - "Ievades avots" "TV (antena/kabelis)" "Nav informācijas par programmu" "Nav informācijas" "Bloķēts kanāls" - "Nezināma valoda" + "Nezināma valoda" + "Slēgtie paraksti %1$d" "Slēgtie paraksti" "Izslēgti" "Pielāgot formatēšanu" @@ -137,6 +118,10 @@ "PIN kods nav pareizs. Mēģiniet vēlreiz." "Neatbilstošs PIN. Mēģiniet vēlreiz." + "Pasta indeksa ievadīšana" + "Lietotnē Live Channels pasta indekss tiks izmantots, lai nodrošinātu visu TV kanālu programmu ceļvedi." + "Ievadiet pasta indeksu" + "Nederīgs pasta indekss" "Iestatījumi" "Pielāgot kanālu sarakstu" "Izvēlieties kanālus programmu ceļvedim." @@ -145,6 +130,7 @@ "Vecāku kontrole" "Atklātā pirmkoda licences" "Atklātā pirmkoda licences" + "Sūtīt atsauksmes" "Versija" "Lai skatītos šo kanālu, nospiediet pa labi vērsto bultiņu un ievadiet PIN kodu." "Lai skatītos šo programmu, nospiediet pa labi vērsto bultiņu un ievadiet PIN kodu." @@ -185,8 +171,6 @@ "Nospiediet ATLASĪT"", lai piekļūtu TV izvēlnei." "Nav atrasta neviena TV ieeja." "Nevar atrast TV ieeju." - "Funkcija PIP netiek atbalstīta." - "Nav ievades, ko parādīt, izmantojot PIP." "Kanālu meklētāja veids nav piemērots. Lūdzu, palaidiet lietotni “Tiešraides kanāli” kanālu meklētāja veida TV ievadei." "Kanālu meklēšana neizdevās." "Netika atrasta neviena lietotne šīs darbības veikšanai." @@ -269,8 +253,6 @@ "Saglabāt" "Vienreizējiem ierakstiem ir visaugstākā prioritāte" "Atcelt" - "Atcelt" - "Aizmirst" "Apturēt" "Skatīt ierakstīšanas grafiku" "Šī viena programma" @@ -280,25 +262,29 @@ "Tā vietā ierakstīt tālāk norādīto" "Atcelt šo ierakstīšanu" "Skatīties tūlīt" + "Dzēst ierakstus…" "Var ierakstīt" "Ierakstīšana ir ieplānota" "Ierakstīšanas konflikts" "Ierakstīšana" "Neizdevās ierakstīt" "Tiek lasītas programmas, lai izveidotu ierakstīšanas grafikus." - "Tiek lasītas programmas" - - + "Tiek lasītas programmas" + "Skatīt nesenos ierakstus" + "“%1$s” ierakstīšana netika pabeigta." + "“%1$s” un “%2$s” ierakstīšana netika pabeigta." + "“%1$s”, “%2$s” un “%3$s” ierakstīšana netika pabeigta." + "“%1$s” ierakstīšana netika pabeigta, jo krātuvē nepietiek vietas." + "“%1$s” un “%2$s” ierakstīšana netika pabeigta, jo krātuvē nepietiek vietas." + "“%1$s”, “%2$s” un “%3$s” ierakstīšana netika pabeigta, jo krātuvē nepietiek vietas." "Ciparvideo ierakstītājam nepieciešama lielāka krātuve" "Jūs varēsiet ierakstīt programmas, izmantojot ciparvideo ierakstītāju. Taču pašlaik jūsu ierīces krātuvē nav pietiekami daudz vietas, lai tas darbotos. Lūdzu, pievienojiet vismaz %1$s GB lielu ārējo disku un izpildiet sniegtos norādījumus, lai formatētu to kā ierīces krātuvi." + "Krātuvē nepietiek vietas" + "Šī programma netiks ierakstīta, jo krātuvē nepietiek vietas. Izdzēsiet dažus esošos ierakstus." "Trūkst krātuves" - "Trūkst ciparvideo ierakstītāja izmantotās krātuves. Lūdzu, pievienojiet ārējo disku, ko izmantojāt iepriekš ciparvideo ierakstītāja atkārtotai iespējošanai. Varat arī izvēlēties aizmirst krātuvi, ja tā vairs nav pieejama." - "Vai aizmirst krātuvi?" - "Viss jūsu ierakstītais saturs un grafiki tiks zaudēti." "Vai apturēt ierakstīšanu?" "Ierakstītais saturs tiks saglabāts." - - + "Seriāla “%1$s” ierakstīšana tiks apturēta, jo ir konflikts ar šo programmu. Ierakstītais saturs tiks saglabāts." "Ierakstīšana ir ieplānota, taču ir konflikti" "Ierakstīšana tika sākta, taču ir konflikti" "Programma %1$s tiks ierakstīta." @@ -314,14 +300,32 @@ "Šo pārraidi jau ir plānots ierakstīt plkst. %1$s." "Jau tika ierakstīta" "Šī pārraide jau ir ierakstīta. Tā ir pieejama DVR bibliotēkā." - - - - - - - - + "Ir ieplānota seriāla ierakstīšana" + + Seriālam %2$s ir ieplānoti %1$d ieraksti. + Seriālam %2$s ir ieplānots %1$d ieraksts. + Seriālam %2$s ir ieplānoti %1$d ieraksti. + + + Seriālam %2$s ir ieplānoti %1$d ieraksti. Konfliktu dēļ netiks ierakstīts(-i) %3$d no tiem. + Seriālam %2$s ir ieplānots %1$d ieraksts. Konfliktu dēļ netiks ierakstīts(-i) %3$d no tiem. + Seriālam %2$s ir ieplānoti %1$d ieraksti. Konfliktu dēļ netiks ierakstīts(-i) %3$d no tiem. + + + Seriālam %2$s ir ieplānoti %1$d ieraksti. Konfliktu dēļ netiks ierakstītas %3$d šī un cita(-u) seriāla(-u) sērijas. + Seriālam %2$s ir ieplānots %1$d ieraksts. Konfliktu dēļ netiks ierakstītas %3$d šī un cita(-u) seriāla(-u) sērijas. + Seriālam %2$s ir ieplānoti %1$d ieraksti. Konfliktu dēļ netiks ierakstītas %3$d šī un cita(-u) seriāla(-u) sērijas. + + + Seriālam %2$s ir ieplānoti %1$d ieraksti. Konfliktu dēļ netiks ierakstīta 1 cita seriāla sērija. + Seriālam %2$s ir ieplānots %1$d ieraksts. Konfliktu dēļ netiks ierakstīta 1 cita seriāla sērija. + Seriālam %2$s ir ieplānoti %1$d ieraksti. Konfliktu dēļ netiks ierakstīta 1 cita seriāla sērija. + + + Seriālam %2$s ir ieplānoti %1$d ieraksti. Konfliktu dēļ netiks ierakstītas %3$d cita seriāla sērijas. + Seriālam %2$s ir ieplānots %1$d ieraksts. Konfliktu dēļ netiks ierakstītas %3$d cita seriāla sērijas. + Seriālam %2$s ir ieplānoti %1$d ieraksti. Konfliktu dēļ netiks ierakstītas %3$d cita seriāla sērijas. + "Ierakstītā programma netika atrasta." "Saistītie ieraksti" "(Nav programmas apraksta)" @@ -346,6 +350,7 @@ "Vai apturēt sērijas ierakstīšanu?" "Ierakstītās sērijas būs pieejamas DVR bibliotēkā." "Apturēt" + "Šobrīd nav iznākusi neviena sērija." "Nav pieejama neviena sērija.\nTās tiks ierakstītas, kad būs pieejamas." (%1$d minūtes) diff --git a/res/values-mk-rMK/strings.xml b/res/values-mk-rMK/strings.xml index 566377a8..9c3cfbc9 100644 --- a/res/values-mk-rMK/strings.xml +++ b/res/values-mk-rMK/strings.xml @@ -20,9 +20,8 @@ "моно" "стерео" "Контроли за игри" - "Последни канали" + "Канали" "ТВ опции" - "Опции за ПИП" "Контролите за репродуцирање се недостапни за овој канал" "Пуштање или паузирање" "Брзо премотување напред" @@ -35,33 +34,15 @@ "Затворени титли" "Реж. на прикаж." "ПИП" - "Вклучено" - "Исклучено" "Мултиаудио" "Добиј уште канали" "Поставки" - "Извор" - "Замени" - "Вклучено" - "Исклучено" - "Звук" - "Главен" - "ПИП прозорец" - "Распоред" - "Долу десно" - "Горе десно" - "Горе лево" - "Долу лево" - "Едно до друго" - "Големина" - "Голема" - "Мала" - "Влезен извор" "ТВ (антена/кабел)" "Нема информации за програмата" "Нема информации" "Блокиран канал" - "Непознат јазик" + "Непознат јазик" + "Затворени титлови %1$d" "Затворени титли" "Исклучено" "Прилаг. форматирање" @@ -135,6 +116,10 @@ "PIN-кодот е погрешен. Обидете се повторно." "Обидете се повторно, PIN-кодот не се совпаѓа" + "Внесете го поштенскиот број." + "Апликацијата Live TV ќе го користи поштенскиот број за да обезбеди целосен програмски водич за ТВ-каналите." + "Внесете го поштенскиот број" + "Неважечки поштенски број" "Поставки" "Приспособи го списокот канали" "Изберете канали за програмскиот водич" @@ -143,6 +128,7 @@ "Родителски надзор" "Лиценци за софтвер со отворен код" "Лиценци за отворен код" + "Испратете повратни информации" "Верзија" "За да го гледате овој канал, притиснете Во ред и внесете го вашиот PIN" "За да ја гледате оваа програма, притиснете Во ред и внесете го вашиот PIN" @@ -181,8 +167,6 @@ "Притиснете ИЗБЕРИ"" за да пристапите до ТВ-менито." "Не е пронајден ТВ-влез" "ТВ-влезот не може да се најде" - "ПИП не е поддржано" - "Нема достапен влез што може да се прикаже со ПИП" "Типот приемник не е соодветен. Стартувајте ја апликацијата Live TV за ТВ-влез од типот приемник." "Бирањето не успеа" "Не е пронајдена апликација што ќе се справи со ова дејство." @@ -259,8 +243,6 @@ "Зачувај" "Еднократните снимања имаат највисок приоритет" "Откажи" - "Откажи" - "Заборави" "Сопри" "Прикажи распоред на снимање" "Само оваа програма" @@ -270,25 +252,29 @@ "Наместо неа, снимај ја оваа" "Откажете го снимањево" "Гледајте сега" + "Избришете ги снимките…" "Може да се снима" "Снимањето е закажано" "Конфликт при снимање" "Се снима" "Неуспешно снимање" "Се читаат програми за да се создадат распореди за снимање" - "Се читаат програми" - - + "Се читаат програми" + "Прегледајте ги последните снимки" + "Снимањето на %1$s е незавршено." + "Снимањата на %1$s и %2$s се незавршени." + "Снимањата на %1$s, %2$s и %3$s се незавршени." + "Снимањето на %1$s не заврши поради недоволен простор за складирање." + "Снимањата %1$s и %2$s не завршија поради недоволен простор за складирање." + "Снимањата %1$s, %2$s и %3$s не завршија поради недоволен простор за складирање." "Потребна е поголема меморија за DVR" "Ќе може да снимате програми со DVR. Но во моментов нема доволно простор на вашиот уред за да може DVR да функционира. Поврзете надворешна податочна едница од %1$s GB или повеќе и следете ги чекорите за да ја форматирате како меморија на уредот." + "Нема доволно меморија" + "Програмава нема да се сними бидејќи нема доволно меморија. Обидете се да избришете постоечки снимки." "Недостасува простор" - "Недостасува дел од просторот што го користи DVR. Поврзете го надворешниот диск што го користевте претходно за да овозможите DVR повторно. Во спротивно, може да изберете да се заборави просторот ако веќе не е достапен." - "Да се заборави просторот?" - "Сите ваши снимени содржини и распореди ќе се изгубат." "Да се сопре со снимање?" "Снимените содржини ќе се зачуваат." - - + "Снимањето на %1$s ќе прекине поради конфликти со програмава. Снимената содржина ќе се зачува." "Снимањето е закажано, но постојат конфликти" "Снимањето започна, но постојат конфликти" "%1$s ќе се сними." @@ -306,14 +292,27 @@ "Истата програма е веќе закажана за снимање во %1$s." "Веќе е снимена" "Програмава е веќе снимена. Достапна е во DVR-збирката." - - - - - - - - + "Закажано е снимање серија" + + %1$d снимање е закажано за %2$s. + %1$d снимања се закажани за %2$s. + + + %1$d снимање е закажано за %2$s. %3$d од нив нема да се снимат поради конфликти. + %1$d снимања се закажани за %2$s. %3$d од нив нема да се снимат поради конфликти. + + + %1$d снимање е закажано за %2$s. %3$d епизоди од оваа и други серии нема да се снимат поради конфликти. + %1$d снимања се закажани за %2$s. %3$d епизоди од оваа и други серии нема да се снимат поради конфликти. + + + %1$d снимање е закажано за %2$s. 1 епизода од други серии нема да се сними поради конфликти. + %1$d снимања се закажани за %2$s. 1 епизода од други серии нема да се сними поради конфликти. + + + %1$d снимање е закажано за %2$s. %3$d епизоди од други серии нема да се снимат поради конфликти. + %1$d снимања се закажани за %2$s. %3$d епизоди од други серии нема да се снимат поради конфликти. + "Снимената програма не е пронајдена." "Поврзани снимки" "(Нема опис на програмата)" @@ -336,6 +335,7 @@ "Да се сопре снимањето на серијата?" "Снимените епизоди ќе останат достапни во DVR-збирката." "Сопри" + "Не се емитуваат епизоди во моментов." "Не се достапни епизоди.\nЌе се снимат штом ќе бидат достапни." (%1$d минута) diff --git a/res/values-ml-rIN/strings.xml b/res/values-ml-rIN/strings.xml index e29a8755..236413a0 100644 --- a/res/values-ml-rIN/strings.xml +++ b/res/values-ml-rIN/strings.xml @@ -20,9 +20,8 @@ "മോണോ" "സ്‌റ്റീരിയോ" "പ്ലേ നിയന്ത്രണങ്ങൾ" - "ഏറ്റവും പുതിയ ചാനലുകൾ" + "ചാനലുകൾ" "ടിവി ഓപ്‌ഷനുകൾ" - "PIP ഓ‌പ്‌ഷനുകൾ" "ഈ ചാനലിന് പ്ലേ നിയന്ത്രണങ്ങൾ ലഭ്യമല്ല" "പ്ലേ ചെയ്യുക അല്ലെങ്കിൽ താല്‍‌ക്കാലികമായി നിര്‍‌ത്തുക" "വേഗത്തിലുള്ള കൈമാറൽ" @@ -35,33 +34,15 @@ "അടച്ച അടിക്കുറിപ്പുകൾ" "ഡിസ്‌പ്ലേ മോഡ്" "PIP" - "ഓണാണ്" - "ഓഫാണ്" "മൾട്ടി ഓഡിയോ" "കൂടുതൽ ചാനൽ സ്വീകരിക്കൂ" "ക്രമീകരണം" - "ഉറവിടം" - "സ്വാപ്പുചെയ്യുക" - "ഓണാണ്" - "ഓഫാണ്" - "ശബ്ദം" - "പ്രധാന വിൻഡോ" - "PIP വിൻഡോ" - "ലേ‌ഔട്ട്" - "ചുവടെ വലതുഭാഗത്ത്" - "മുകളില്‍‌ വലതുഭാഗത്ത്" - "മുകളില്‍‌ ഇടതുഭാഗത്ത്" - "ചുവടെ ഇടതുഭാഗത്ത്" - "വശങ്ങളിലായി" - "വലുപ്പം" - "വലുത്" - "ചെറുത്" - "ഇൻപുട്ട് ഉറവിടം" "ടിവി (ആന്റിന/കേബിൾ)" "പ്രോഗ്രാം വിവരമൊന്നുമില്ല" "വിവരമൊന്നുമില്ല" "തടഞ്ഞ ചാനൽ" - "അറിയാത്ത ഭാഷ" + "അറിയാത്ത ഭാഷ" + "അടച്ച അടിക്കുറിപ്പുകൾ %1$d" "അടച്ച അടിക്കുറിപ്പുകൾ" "ഓഫ്" "ഫോർമാറ്റുചെയ്യൽ ഇഷ്‌ടാനുസൃതമാക്കുക" @@ -133,6 +114,10 @@ "നൽകിയ പിൻ തെറ്റാണ്. വീണ്ടും ശ്രമിക്കുക." "പിൻ യോജിക്കുന്നില്ല, വീണ്ടും ശ്രമിക്കുക" + "നിങ്ങളുടെ തപാൽ കോഡ് നൽകുക." + "ടിവി ചാനലുകൾക്ക് സമ്പൂർണ്ണ പ്രോഗ്രാം ഗൈഡ് നൽകുന്നതിന് തത്സമയ ചാനലുകൾ ആപ്പ്, തപാൽ കോഡ് ഉപയോഗിക്കും." + "നിങ്ങളുടെ തപാൽ കോഡ് നൽകുക" + "തെറ്റായ തപാൽ കോഡ്" "ക്രമീകരണം" "ചാനൽ ലിസ്റ്റ് ഇഷ്‌ടാനുസൃതമാക്കൂ" "നിങ്ങളുടെ പ്രോഗ്രാം ഗൈഡിനായി ചാനലുകൾ തിരഞ്ഞെടുക്കുക" @@ -141,6 +126,7 @@ "രക്ഷാകർതൃ നിയന്ത്രണങ്ങൾ" "ഓപ്പൺ സോഴ്‌സ് ലൈസൻസ്" "ഓപ്പൺ സോഴ്‌സ് ലൈസൻസുകൾ" + "ഫീഡ്‍ബാക്ക് അയയ്ക്കുക" "പതിപ്പ്" "ഈ ചാനൽ കാണുന്നതിന് വലതുവശത്ത് അമർത്തിക്കൊണ്ട് നിങ്ങളുടെ പിൻ നൽകുക" "ഈ പ്രോഗ്രാം കാണുന്നതിന് വലതുവശത്ത് അമർത്തിക്കൊണ്ട് നിങ്ങളുടെ പിൻ നൽകുക" @@ -179,8 +165,6 @@ "ടിവി മെനു ആക്‌സസ്സ് ചെയ്യാൻ ""\'തിരഞ്ഞെടുക്കുക\' അമർത്തുക""." "ടിവി ഇൻപുട്ടൊന്നും കണ്ടെത്തിയില്ല" "ടിവി ഇൻപുട്ട് കണ്ടെത്താനാകില്ല" - "PIP പിന്തുണയ്‌ക്കുന്നില്ല" - "PIP-യിൽ കാണിക്കാനാകുന്ന ഇൻപുട്ടൊന്നും ലഭ്യമല്ല" "ട്യൂണർ തരം അനുയോജ്യമല്ല. ട്യൂണർ തരം ടിവി ഇൻപുട്ടിനായി തത്സമയ ചാനലുകളുടെ അപ്ലിക്കേഷൻ സമാരംഭിക്കുക." "ട്യൂൺ ചെയ്യൽ പരാജയപ്പെട്ടു" "ഈ പ്രവർത്തനം കൈകാര്യം ചെയ്യാൻ ആപ്പുകളൊന്നും കണ്ടെത്തിയില്ല." @@ -257,8 +241,6 @@ "സംരക്ഷിക്കുക" "ഒറ്റത്തവണ റെക്കോർഡിംഗുകൾക്ക് ഏറ്റവും ഉയർന്ന മുൻഗണന" "റദ്ദാക്കൂ" - "റദ്ദാക്കുക" - "മറക്കുക" "നിർത്തുക" "റെക്കോർഡിംഗ് ഷെഡ്യൂൾ കാണുക" "ഈ ഒരൊറ്റ പ്രോഗ്രാം" @@ -268,25 +250,29 @@ "പകരം, ഇത് റെക്കോർഡ് ചെയ്യുക" "ഈ റെക്കോർഡിംഗ് റദ്ദാക്കുക" "ഇപ്പോൾ കാണുക" + "റെക്കോർഡിംഗുകൾ ഇല്ലാതാക്കുക…" "റെക്കോർഡുചെയ്യാവുന്നത്" "റെക്കോർഡിംഗ് ഷെഡ്യൂൾചെയ്‌തു" "റെക്കോർഡിംഗ് പൊരുത്തക്കേട്" "റെക്കോർഡിംഗ്" "റെക്കോർഡുചെയ്യൽ പരാജയപ്പെട്ടു" "റെക്കോർഡിംഗ് ഷെഡ്യൂളുകൾ സൃഷ്ടിക്കാൻ പ്രോഗ്രാമുകൾ വായിക്കുന്നു" - "വായനാ പ്രോഗ്രാമുകൾ" - - + "വായനാ പ്രോഗ്രാമുകൾ" + "സമീപകാല റെക്കോർഡിംഗുകൾ കാണുക" + "%1$s റെക്കോർഡുചെയ്യൽ പൂർണ്ണമായില്ല." + "%1$s, %2$s എന്നിവയുടെ റെക്കോർഡുചെയ്യൽ പൂർണ്ണമായില്ല." + "%1$s, %2$s, %3$s എന്നിവയുടെ റെക്കോർഡുചെയ്യൽ പൂർണ്ണമായില്ല." + "വേണ്ടത്ര സ്റ്റോറേജ് ഇല്ലാത്തതിനാൽ %1$s റെക്കോർഡുചെയ്യൽ പൂർത്തിയായില്ല." + "വേണ്ടത്ര സ്റ്റോറേജ് ഇല്ലാത്തതിനാൽ %1$s, %2$s എന്നിവയുടെ റെക്കോർഡുചെയ്യൽ പൂർത്തിയായില്ല." + "വേണ്ടത്ര സ്റ്റോറേജ് ഇല്ലാത്തതിനാൽ %1$s, %2$s, %3$s എന്നിവയുടെ റെക്കോർഡുചെയ്യൽ പൂർത്തിയായില്ല." "DVR-ന് കൂടുതൽ സ്റ്റോറേജ് ആവശ്യമാണ്" "DVR ഉപയോഗിച്ച് നിങ്ങൾക്ക് പ്രോഗ്രാമുകൾ റെക്കോർഡുചെയ്യാനാകും. എന്നിരുന്നാലും, DVR പ്രവർത്തിക്കുന്നതിന് നിങ്ങളുടെ ഉപകരണത്തിൽ ഇപ്പോൾ വേണ്ടത്ര സ്റ്റോറേജ് ഇല്ല. %1$sGB-യോ കൂടുതലോ ഉള്ള ഒരു എക്സ്റ്റേണൽ ഡ്രൈവ് കണക്റ്റുചെയ്യുകയും ഉപകരണ സ്റ്റോറേജായി അത് ഫോർമാറ്റുചെയ്യുന്നതിന് നിർദ്ദേശങ്ങൾ പിന്തുടരുകയും ചെയ്യുക." + "മതിയായ സ്റ്റോറേജ് ഇല്ല" + "മതിയായ സ്റ്റോറേജ് ഇല്ലാത്തതിനാൽ ഈ പ്രോഗ്രാം റെക്കോർഡ് ചെയ്യപ്പെടില്ല. നിലവിലുള്ള റെക്കോർഡിംഗുകളിൽ ചിലത് ഇല്ലാതാക്കിക്കൊണ്ട് ശ്രമിച്ചുനോക്കുക." "നഷ്ടമായ സ്റ്റോറേജ്" - "DVR ഉപയോഗിക്കുന്ന സ്റ്റോറേജിന്റെ ചിലത് നഷ്ടമായിരിക്കുന്നു. വീണ്ടും DVR പ്രവർത്തനക്ഷമമാക്കുന്നതിന്, നിങ്ങൾ മുമ്പ് ഉപയോഗിച്ച എക്സ്റ്റേണൽ ഡ്രൈവ് കണക്റ്റുചെയ്യുക. സ്റ്റോറേജ് തുടർന്നങ്ങോട്ട് ലഭ്യമല്ലെങ്കിൽ, ഇതരമാർഗ്ഗമെന്ന നിലയിൽ, നിങ്ങൾക്ക് മറക്കുന്നതിന് തിരഞ്ഞെടുക്കാവുന്നതാണ്." - "സ്റ്റോറേജ് മറക്കണോ?" - "നിങ്ങളുടെ റെക്കോർഡ് ചെയ്തിട്ടുള്ള എല്ലാ ഉള്ളടക്കവും ഷെഡ്യൂളുകളും നഷ്ടപ്പെടും." "റെക്കോർഡിംഗ് നിർത്തണോ?" "റെക്കോർഡുചെയ്തിട്ടുള്ള ഉള്ളടക്കം സംരക്ഷിക്കപ്പെടും." - - + "ഈ പ്രോഗ്രാമുമായി പൊരുത്തക്കേട് ഉള്ളതിനാൽ %1$s എന്ന പ്രോഗ്രാമിന്റെ റെക്കോർഡിംഗ് അവസാനിപ്പിക്കും. റെക്കോർഡുചെയ്ത ഉള്ളടക്കം സംരക്ഷിക്കപ്പെടും." "റെക്കോർഡിംഗ് ഷെഡ്യൂൾ ചെയ്തിരിക്കുന്നു, എന്നാൽ പൊരുത്തക്കേടുകൾ ഉണ്ട്" "റെക്കോർഡിംഗ് ആരംഭിച്ചിരിക്കുന്നു, എന്നാൽ പൊരുത്തക്കേടുകൾ ഉണ്ട്" "%1$s റെക്കോർഡുചെയ്യപ്പെടും." @@ -304,14 +290,27 @@ "ഇതേ പ്രോഗ്രാം %1$s-ന് റെക്കോർഡ് ചെയ്യുന്നതിനായി ഇതിനകം തന്നെ ഷെഡ്യൂൾ ചെയ്തിട്ടുണ്ട്." "ഇതിനകം തന്നെ റെക്കോർഡ് ചെയ്തു" "ഈ പ്രോഗ്രാം ഇതിനകം തന്നെ റെക്കോർഡ് ചെയ്തിട്ടുണ്ട്. DVR ലൈഒബ്രറിയിൽ ഇത് ലഭ്യമാണ്." - - - - - - - - + "പരമ്പരയുടെ റെക്കോർഡിംഗ് ഷെഡ്യൂൾ ചെയ്‌തു" + + %2$s എന്ന പരമ്പരയുടെ %1$d റെക്കോർഡിംഗുകൾ ഷെഡ്യൂൾ ചെയ്തിട്ടുണ്ട്. + %2$s എന്ന പരമ്പരയുടെ %1$d റെക്കോർഡിംഗ് ഷെഡ്യൂൾ ചെയ്തിട്ടുണ്ട്. + + + %2$s എന്ന പരമ്പരയുടെ %1$d റെക്കോർഡിംഗുകൾ ഷെഡ്യൂൾ ചെയ്തിട്ടുണ്ട്. പൊരുത്തക്കേടുകൾ ഉള്ളതിനാൽ അവയിലെ %3$d എണ്ണം റെക്കോർഡുചെയ്യപ്പെടില്ല. + %2$s എന്ന പരമ്പരയുടെ %1$d റെക്കോർഡിംഗ് ഷെഡ്യൂൾ ചെയ്തിട്ടുണ്ട്. പൊരുത്തക്കേടുകൾ ഉള്ളതിനാൽ അത് റെക്കോർഡ് ചെയ്യപ്പെടില്ല. + + + %2$s എന്ന പരമ്പരയുടെ %1$d റെക്കോർഡിംഗുകൾ ഷെഡ്യൂൾ ചെയ്തിട്ടുണ്ട്. പൊരുത്തക്കേടുകൾ ഉള്ളതിനാൽ ഈ പരമ്പരയുടെയും മറ്റ് പരമ്പരയുടെയും %3$d എപ്പിസോഡുകൾ റെക്കോർഡുചെയ്യപ്പെടില്ല. + %2$s എന്ന പരമ്പരയുടെ %1$d റെക്കോർഡിംഗ് ഷെഡ്യൂൾ ചെയ്തിട്ടുണ്ട്. പൊരുത്തക്കേടുകൾ ഉള്ളതിനാൽ ഈ പരമ്പരയുടെയും മറ്റ് പരമ്പരയുടെയും %3$d എപ്പിസോഡുകൾ റെക്കോർഡുചെയ്യപ്പെടില്ല. + + + %2$s എന്ന പരമ്പരയുടെ %1$d റെക്കോർഡിംഗുകൾ ഷെഡ്യൂൾ ചെയ്തിട്ടുണ്ട്. പൊരുത്തക്കേടുകൾ ഉള്ളതിനാൽ മറ്റ് പരമ്പരയുടെ ഒരു എപ്പിസോഡ് റെക്കോർഡുചെയ്യപ്പെടില്ല. + %2$s എന്ന പരമ്പരയുടെ %1$d റെക്കോർഡിംഗ് ഷെഡ്യൂൾ ചെയ്തിട്ടുണ്ട്. പൊരുത്തക്കേടുകൾ ഉള്ളതിനാൽ മറ്റ് പരമ്പരയുടെ ഒരു എപ്പിസോഡ് റെക്കോർഡുചെയ്യപ്പെടില്ല. + + + %2$s എന്ന പരമ്പരയുടെ %1$d റെക്കോർഡിംഗുകൾ ഷെഡ്യൂൾ ചെയ്തിട്ടുണ്ട്. പൊരുത്തക്കേടുകൾ ഉള്ളതിനാൽ മറ്റ് പരമ്പരകളുടെ %3$d എപ്പിസോഡുകൾ റെക്കോർഡുചെയ്യപ്പെടില്ല. + %2$s എന്ന പരമ്പരയുടെ %1$d റെക്കോർഡിംഗ് ഷെഡ്യൂൾ ചെയ്തിട്ടുണ്ട്. പൊരുത്തക്കേടുകൾ ഉള്ളതിനാൽ മറ്റ് പരമ്പരകളുടെ %3$d എപ്പിസോഡുകൾ റെക്കോർഡുചെയ്യപ്പെടില്ല. + "റെക്കോർഡുചെയ്ത പ്രോഗ്രാം കണ്ടെത്തിയില്ല." "ബന്ധപ്പെട്ട റെക്കോർഡിംഗുകൾ" "(പ്രോഗ്രാം വിവരണമില്ല)" @@ -334,6 +333,7 @@ "സീരീസ് റെക്കോർഡുചെയ്യുന്നത് നിർത്തണോ?" "റെക്കോർഡുചെയ്ത എപ്പിസോഡുകൾ DVR ലൈബ്രറിയിൽ ലഭ്യമാകുന്നത് തുടരും." "നിർത്തുക" + "എപ്പിസോഡുകളൊന്നും ഇപ്പോൾ പ്രക്ഷേപണം ചെയ്യുന്നില്ല." "എപ്പിസോഡുകളൊന്നും ലഭ്യമല്ല.\nലഭ്യമായിക്കഴിഞ്ഞാൽ അവ റെക്കോർഡ് ചെയ്യപ്പെടും." (%1$d മിനിറ്റ്) diff --git a/res/values-mn-rMN/strings.xml b/res/values-mn-rMN/strings.xml index e99f8a0e..2aa79038 100644 --- a/res/values-mn-rMN/strings.xml +++ b/res/values-mn-rMN/strings.xml @@ -20,9 +20,8 @@ "моно" "стерео" "Play хяналт" - "Саяхны сувгууд" + "Сувгууд" "ТВ-н сонголтууд" - "PIP Сонголтууд" "Энэ сувагт тоглуулах хяналтыг ашиглах боломжгүй байна" "Тоглуулах эсвэл түр зогсоох" "Хурдан урагшлуулах" @@ -35,33 +34,15 @@ "Хаалттай капшн" "Дэлгэцийн горим" "PIP" - "Идэвхтэй" - "Идэвхгүй" "Мульти-аудио" "Нэмж суваг авах" "Тохиргоо" - "Эх сурвалж" - "Солих" - "Идэвхтэй" - "Идэвхгүй" - "Дуу" - "Үндсэн" - "PIP цонх" - "Байрлал" - "Баруун доод" - "Баруун дээд" - "Зүүн дээд" - "Зүүн доод" - "Зэрэгцсэн" - "Хэмжээ" - "Том" - "Жижиг" - "Оролтын эх үүсвэр" "TВ (Антен /Кабел)" "Хөтөлбөрийн мэдээлэл байхгүй" "Мэдээлэл байхгүй" "Хаагдсан суваг" - "Үл мэдэгдэх хэл" + "Үл мэдэгдэх хэл" + "Хаалттай тайлбар %1$d" "Хаалттай тайлбар" "Идэвхгүй" "Форматыг тааруулах" @@ -135,6 +116,10 @@ "Энэ PIN буруу байна. Дахин оролдоно уу." "Дахин оролдоно уу, PIN таарахгүй байна" + "ЗИП кодоо оруулна уу." + "ТВ сувагт хөтөлбөрийн хуваарийг бүрнээр нь олгохын тулд Шууд сувгийн апп ЗИП код ашиглах болно." + "ЗИП кодоо оруулна уу" + "ЗИП код буруу байна" "Тохиргоо" "Сувгийн жагсаалтыг тохируулах" "ТВ хөтөлбөрт оруулах сувгуудыг сонгоно уу" @@ -143,6 +128,7 @@ "Эцэг эхийн хяналт" "Нээлттэй эхийн лиценз" "Нээлттэй эхийн лиценз" + "Санал хүсэлт илгээх" "Хувилбар" "Энэ сувгийг үзэхийн тулд Баруун товчийг дараад өөрийн PIN-г оруулна уу" "Энэ хөтөлбөрийг үзэхийн тулд Баруун товчийг дараад өөрийн PIN-г оруулна уу" @@ -181,8 +167,6 @@ "ТВ цэс рүү хандахын тулд ""СОНГОХ гэснийг дарна уу" "ТВ оролт олдсонгүй." "ТВ оролтыг олж чадахгүй байна." - "PIP дэмжигдээгүй байна." - "PIP-тай харуулж болох бэлэн оролт байхгүй байна." "Дуу тохируулагчийн төрөл тохирохгүй байна. Зурагтын оролтын дуу тохируулагчийн төрөлд зориулан Шууд Сувгуудын апликейшнийг эхлүүл." "Дуу тааруулж чадсангүй." "Энэ үйлдлийг гүйцэтгэх апп олдсонгүй." @@ -257,8 +241,6 @@ "Хадгалах" "Нэг удаагийн бичлэг өндөр ач холбогдолтой" "Цуцлах" - "Цуцлах" - "Мартах" "Зогсоох" "Бичих хуваарийг харах" "Зөвхөн энэ хөтөлбөр" @@ -268,25 +250,29 @@ "Оронд нь үүнийг бичих" "Энэ бичлэгийг цуцлах" "Одоо үзэх" + "Бичлэгийг устгах..." "Дүрс бичих боломжтой" "Дүрс бичихээр товлосон" "Дүрс бичих боломжгүй" "Бичиж байна" "Бичиж чадсангүй" "Бичлэгийн хуваарь үүсгэхийн тулд хөтөлбөрийг уншиж байна" - "Хөтөлбөрийг уншиж байна" - - + "Хөтөлбөрийг уншиж байна" + "Сүүлийн бичлэгийг харах" + "%1$s-г бичиж чадсангүй." + "%1$s, %2$s-г бичиж чадсангүй." + "%1$s, %2$s, %3$s-г бичиж чадсангүй." + "Багтаамж хангалтгүйн улмаас %1$s-г бичиж чадсангүй." + "Багтаамж хангалтгүйн улмаас %1$s, %2$s-г бичиж чадсангүй." + "Багтаамж хангалтгүйн улмаас %1$s, %2$s, %3$s-г бичиж чадсангүй." "DVR-д илүү багтаамж шаардлагатай" "Та DVR-р хөтөлбөр бичих боломжтой болно. Гэсэн хэдий ч таны төхөөрөмжид DVR ажиллуулах хангалттай багтаамж алга. %1$sгигабайт, эсвэл үүнээс дээш багтаамжтай гадаад драйв холбож, үүнийг төхөөрөмжийн сан болгож хэлбэршүүлэхийн тулд зааврыг дагана уу." + "Хангалттай сан алга" + "Сангийн багтаамж хангалтгүй байгаа тул энэ хөтөлбөрийг бичихгүй. Зарим шаардлагагүй бичлэгийг устгана уу." "Сан алга" - "DVR-н ашигласан зарим сан алга. DVR-г дахин идэвхжүүлэхээс өмнө ашигласан гадаад драйвыг холбоно уу. Хэрэв сан байхгүй бол үүнийг мартах боломжтой." - "Санг мартах уу?" - "Таны бичсэн агуулга, хуваарь устах болно." "Бичлэгийг зогсоох уу?" "Бичсэн агуулгыг хадгална." - - + "%1$s-н бичлэгийг энэ хөтөлбөртэй зөрчилдөж байгаа тул зогсоох болно. Бичсэн агуулгыг хадгална." "Бичихээр товлосон ч зөрчилтэй байна" "Бичлэг эхлүүлсэн хэдий ч зөрчилтэй байна" "%1$s-г бичих болно." @@ -304,14 +290,27 @@ "Үүнтэй ижил хөтөлбөрийг %1$s-д бичихээр товлосон байна." "Аль хэдийн бичсэн" "Энэ хөтөлбөрийг аль хэдийн бичсэн байна. Энэ нь DVR санд боломжтой." - - - - - - - - + "Цувралын бичлэгийг товлосон" + + %2$s%1$d бичлэг товлосон. + %2$s%1$d бичлэг товлосон. + + + %2$s%1$d бичлэг товлосон. Тэдгээрийн %3$d-г зөрчлийн улмаас бичихгүй. + %2$s%1$d бичлэг товлосон. Үүнийг зөрчлийн улмаас бичихгүй. + + + %2$s%1$d бичлэг товлосон. Энэ болон бусад цувралын %3$d ангийг зөрчлийн улмаас бичихгүй. + %2$s%1$d бичлэг товлосон. Энэ болон бусад цувралын %3$d ангийг зөрчлийн улмаас бичихгүй. + + + %2$s%1$d бичлэг товлосон. Бусад цувралын 1 ангийг зөрчлийн улмаас бичихгүй. + %2$s%1$d бичлэг товлосон. Бусад цувралын 1 ангийг зөрчлийн улмаас бичихгүй. + + + %2$s%1$d бичлэг товлосон. Бусад цувралын %3$d ангийг зөрчлийн улмаас бичихгүй. + %2$s%1$d бичлэг товлосон. Бусад цувралын %3$d ангийг зөрчлийн улмаас бичихгүй. + "Бичсэн хөтөлбөр олдсонгүй." "Холбоотой бичлэг" "(Хөтөлбөрийн тодорхойлолт алга)" @@ -334,6 +333,7 @@ "Цувралыг бичихээ зогсоох уу?" "Бичсэн цувралыг DVR санд хадгална." "Зогсоох" + "Одоо ямар ч ангийг бичээгүй байна." "Ямар ч анги алга.\nТэд боломжтой болсон үедээ бичих болно." (%1$d минут) diff --git a/res/values-mr-rIN/strings.xml b/res/values-mr-rIN/strings.xml index 1888e9de..713064c6 100644 --- a/res/values-mr-rIN/strings.xml +++ b/res/values-mr-rIN/strings.xml @@ -20,9 +20,8 @@ "एक" "स्टिरिओ" "प्ले नियंत्रणे" - "अलीकडील चॅनेल" + "चॅनेल" "टीव्‍ही पर्याय" - "PIP पर्याय" "या चॅनेलसाठी अनुपलब्ध असलेली नियंत्रणे प्ले करा" "प्ले करा किंवा विराम द्या" "फास्ट फॉरवर्ड करा" @@ -35,33 +34,15 @@ "उपशीर्षक" "प्रदर्शन मोड" "PIP" - "चालू" - "बंद" "मल्टी-ऑडिओ" "अधिक चॅनेल मिळवा" "सेटिंग्ज" - "स्त्रोत" - "अदलाबदल करा" - "चालू" - "बंद" - "ध्वनी" - "मुख्य" - "PIP विंडो" - "लेआउट" - "तळाशी उजवीकडे" - "शीर्षस्थानी उजवीकडे" - "शीर्षस्थानी डावीकडे" - "तळाशी डावीकडे" - "शेजारी शेजारी" - "आकार" - "मोठा" - "लहान" - "इनपुट स्त्रोत" "टीव्ही (अँटेना/केबल)" "कोणतीही कार्यक्रम माहिती नाही" "कोणतीही माहिती नाही" "अवरोधित चॅनेल" - "अज्ञात भाषा" + "अज्ञात भाषा" + "बंद मथळा %1$d" "बंद मथळा" "बंद" "स्वरुपन सानुकूलित करा" @@ -123,7 +104,7 @@ "उप रेटिंग" "हे चॅनेल पाहण्‍यासाठी आपला पिन प्रविष्‍ट करा" "हा कार्यक्रम पाहण्‍यासाठी आपला पिन प्रविष्‍ट करा" - "हा प्रोग्राम %1$s रेट केलेला आहे. हा प्रोग्राम पाहण्यासाठी आपला PIN प्रविष्ट करा" + "हा प्रोग्राम %1$s रेट केलेला आहे. हा प्रोग्राम पाहण्यासाठी आपला पिन प्रविष्ट करा" "आपला पिन प्रविष्‍ट करा" "पालक नियंत्रणे सेट करण्यासाठी, एक पिन तयार करा" "नवीन पिन प्रविष्ट करा" @@ -135,6 +116,10 @@ "तो पिन चुकीचा होता. पुन्हा प्रयत्न करा." "पुन्हा प्रयत्न करा, पिन जुळत नाही" + "आपला पिन कोड प्रविष्ट करा." + "टीव्ही चॅनेलसाठी संपूर्ण कार्यक्रम मार्गदर्शक प्रदान करण्यासाठी थेट चॅनेल अॅप पिन कोड वापरेल." + "आपला पिन कोड प्रविष्ट करा" + "अवैध पिन कोड" "सेटिंग्ज" "चॅनेल सूची सानुकूल करा" "आपल्या कार्यक्रम मार्गदर्शकासाठी चॅनेल निवडा" @@ -143,6 +128,7 @@ "पालक नियंत्रणे" "मुक्त स्त्रोत परवाने" "मुक्त स्त्रोत परवाने" + "अभिप्राय पाठवा" "आवृत्ती" "हे चॅनेल पाहण्यासाठी, उजवे दाबा आणि आपला पिन प्रविष्‍ट करा" "हा कार्यक्रम पाहण्‍यासाठी, उजवे दाबा आणि आपला पिन प्रविष्‍ट करा" @@ -181,8 +167,6 @@ "टीव्ही मेनूवर प्रवेश करण्यासाठी ""निवडा दाबा""." "कोणतेही टीव्ही इनपुट आढळले नाही" "टीव्ही इनपुट शोधू शकत नाही" - "PIP समर्थित नाही" - "PIP सह दर्शविले जाऊ शकते असे कोणतेही उपलब्ध इनपुट नाही" "ट्यूनर प्रकार अनुकूल नाही. कृपया ट्यूनर प्रकार टीव्ही इनपुटसाठी थेट चॅनेल अॅप लाँच करा." "ट्यून अयशस्वी झाले" "ही क्रिया हाताळण्यासाठी कोणताही अ‍ॅप आढळला नाही." @@ -259,8 +243,6 @@ "जतन करा" "एक-वेळ रेकॉर्डिंगला सर्वोच्च प्राधान्य आहे" "रद्द करा" - "रद्द करा" - "विसरा" "थांबा" "रेकॉर्डिंग अनुसूची पहा" "हा एक कार्यक्रम" @@ -270,25 +252,29 @@ "त्याऐवजी हे रेकॉर्ड करा" "हे रेकॉर्डिंग रद्द करा" "आता पहा" + "रेकॉर्डिंग हटवा..." "रेकॉर्ड करण्यायोग्य" "रेकॉर्डिंग अनुसूचित केले" "रेकॉर्डिंग संबंधी विरोध" "रेकॉर्डिंग" "रेकॉर्डिंग अयशस्वी झाले" "रेकॉर्डिंग अनुसूची तयार करण्यासाठी प्रोग्राम वाचत आहे" - "वाचन कार्यक्रम" - - + "वाचन कार्यक्रम" + "अलीकडील रेकॉर्डिंग पहा" + "%1$s चे रेकॉर्डिंग अपूर्ण आहे." + "%1$s आणि %2$s चे रेकॉर्डिंग अपूर्ण आहे." + "%1$s, %2$s आणि %3$s चे रेकॉर्डिंग अपूर्ण आहे." + "अपुर्‍या संचयामुळे %1$s चे रेकॉर्डिंग पूर्ण झाले नाही." + "अपुर्‍या संचयामुळे %1$s आणि %2$s चे रेकॉर्डिंग पूर्ण झाले नाही." + "अपुर्‍या संचयामुळे %1$s, %2$s, %3$s चे रेकॉर्डिंग पूर्ण झाले नाही." "DVR साठी आणखी संचय आवश्यक आहे" "आपण DVR ने प्रोग्राम रेकॉर्ड करण्यात सक्षम असाल. तथापि DVR ने कार्य करण्यासाठी आपल्या डिव्हाइसवर आता पुरेसा संचय नाही. कृपया %1$sGB किंवा त्यापेक्षा मोठ्या बाह्य ड्राइव्हशी कनेक्ट करा आणि त्यास डिव्हाइस संचय म्हणून स्वरूपित करण्‍यासाठी चरणांचे अनुसरण करा." + "पुरेसा संचय नाही" + "पुरेसा संचय नसल्याने हा कार्यक्रम रेकॉर्ड केला जाणार नाही. काही विद्यमान रेकॉर्डिंग हटवून पहा." "संचय गहाळ आहे" - "DVR ने वापरलेला काही संचय गहाळ आहे. कृपया DVR पुन्हा सक्षम करण्‍यासाठी आपण पूर्वी वापरलेला बाह्य ड्राइव्ह कनेक्ट करा. वैकल्पिकपणे, यापुढे संचय उपलब्ध नसल्यास आपण तो विसरणे निवडू शकता." - "संचय विसरलात?" - "आपली सर्व रेकॉर्ड केलेली सामग्री आणि अनुसूची गमावल्या जातील." "रेकॉर्डिंग थांबवायचे?" "रेकॉर्ड केलेली सामग्री जतन केली जाईल." - - + "%1$s चे रेकॉर्डिंग थांबवले जाईल कारण ते या कार्यक्रमासह संघर्ष करते. रेकॉर्ड केलेली सामग्री जतन केली जाईल." "रेकॉर्डिंगची अनुसूची केली परंतु त्यासंबंधी विरोध आहेत" "रेकॉर्डिंग सुरू झाली परंतु त्यासंबंधी विरोध आहेत" "%1$s रेकॉर्ड केला जाईल." @@ -306,14 +292,27 @@ "%1$s रोजी रेकॉर्ड करण्यासाठी तोच कार्यक्रम आधीच शेड्यूल केला आहे." "आधीच रेकॉर्ड केला आहे" "हा कार्यक्रम आधीच रेकॉर्ड केला गेला आहे. तो DVR लायब्ररीमध्ये उपलब्ध आहे." - - - - - - - - + "मालिका रेकॉर्डिंग अनुसूचित केले" + + %2$s साठी %1$d रेकॉर्डिंगची अनुसूची केली गेली. + %2$s साठी %1$d रेकॉर्डिंगची अनुसूची केली गेली. + + + %2$s साठी %1$d रेकॉर्डिंगची अनुसूची केली गेली. त्यांंच्यापैकी %3$d विरोधांमुळे रेकॉर्ड केले जाणार नाहीत. + %2$s साठी %1$d रेकॉर्डिंगची अनुसूची केली गेली. त्यांंच्यापैकी %3$d विरोधांमुळे रेकॉर्ड केले जाणार नाहीत. + + + %2$s साठी %1$d रेकॉर्डिंगची अनुसूची केली गेली. या मालिकेचे आणि अन्य मालिकांचे %3$d भाग विरोधांमुळे रेकॉर्ड केले जाणार नाहीत. + %2$s साठी %1$d रेकॉर्डिंगची अनुसूची केली गेली. या मालिकेचे आणि अन्य मालिकांचे %3$d भाग विरोधांमुळे रेकॉर्ड केले जाणार नाहीत. + + + %2$s साठी %1$d रेकॉर्डिंगची अनुसूची केली गेली. अन्य मालिकांचा 1 भाग विरोधांमुळे रेकॉर्ड केला जाणार नाही. + %2$s साठी %1$d रेकॉर्डिंगची अनुसूची केली गेली. अन्य मालिकांचा 1 भाग विरोधांमुळे रेकॉर्ड केला जाणार नाही. + + + %2$s साठी %1$d रेकॉर्डिंगची अनुसूची केली गेली. अन्य मालिकेचे %3$d भाग विरोधांंमुळे रेकॉर्ड केले जाणार नाहीत. + %2$s साठी %1$d रेकॉर्डिंगची अनुसूची केली गेली. अन्य मालिकेचे %3$d भाग विरोधांंमुळे रेकॉर्ड केले जाणार नाहीत. + "रेकॉर्ड केलेला प्रोग्राम सापडला नाही." "संबंधित रेकॉर्डिंग" "(कोणत्याही प्रोग्रामचे वर्णन नाही)" @@ -336,6 +335,7 @@ "मालिका रेकॉर्ड करणे थांबवायचे?" "रेकॉर्ड केलेले भाग DVR लायब्ररी मध्ये उपलब्ध राहतील." "थांबा" + "आता कोणत्याही भागांचे प्रसारण होत नाही." "कोणतेही भाग उपलब्ध नाहीत.\nते उपलब्ध झाल्यावर रेकॉर्ड केले जातील." (%1$d मिनिट) diff --git a/res/values-ms-rMY/strings.xml b/res/values-ms-rMY/strings.xml index 816dee35..932021dd 100644 --- a/res/values-ms-rMY/strings.xml +++ b/res/values-ms-rMY/strings.xml @@ -20,9 +20,8 @@ "mono" "stereo" "Kawalan main" - "Saluran terbaru" + "Saluran" "Pilihan TV" - "Pilihan PIP" "Kawalan main tidak tersedia untuk saluran ini" "Main atau jeda" "Mara laju" @@ -35,33 +34,15 @@ "Sari kata" "Mod paparan" "PIP" - "Hidupkan" - "Matikan" "Berbilang audio" "Dptkn lg saluran" "Tetapan" - "Sumber" - "Silih" - "Hidupkan" - "Matikan" - "Bunyi" - "Utama" - "Tetingkap PIP" - "Reka Letak" - "Kanan bawah" - "Kanan atas" - "Kiri atas" - "Kiri bawah" - "Bersebelahan" - "Saiz" - "Besar" - "Kecil" - "Sumber input" "TV (antena/kabel)" "Maklumat program tiada" "Tiada maklumat" "Saluran disekat" - "Bahasa tidak diketahui" + "Bahasa tidak diketahui" + "Sari kata %1$d" "Sari kata" "Dimatikan" "Sesuaikan format" @@ -135,6 +116,10 @@ "PIN itu salah. Cuba lagi." "Cuba lagi, PIN tidak sepadan" + "Masukkan Poskod anda." + "Apl Saluran Langsung akan menggunakan Poskod untuk menyediakan panduan lengkap rancangan saluran TV." + "Masukkan Poskod anda" + "Poskod tidak sah" "Tetapan" "Sesuaikan senarai saluran" "Pilih saluran untuk panduan rancangan anda" @@ -143,6 +128,7 @@ "Kawalan ibu bapa" "Lesen sumber terbuka" "Lesen sumber terbuka" + "Hantar maklum balas" "Versi" "Untuk menonton saluran ini, tekan Kanan dan masukkan PIN anda" "Untuk menonton rancangan ini, tekan Kanan dan masukkan PIN anda" @@ -181,8 +167,6 @@ "Tekan PILIH"" untuk mengakses menu TV." "Input TV tidak ditemui" "Tidak menemui input TV" - "PIP tidak disokong" - "Tiada input tersedia yang boleh ditunjukkan dengan PIP" "Jenis penala tidak sesuai; Sila lancarkan apl Saluran Langsung untuk input TV jenis penala." "Penalaan gagal" "Tiada apl ditemui untuk mengendalikan tindakan ini." @@ -259,8 +243,6 @@ "Simpan" "Rakaman bersifat sekali sahaja mendapat keutamaan tertinggi" "Batal" - "Batal" - "Lupakan" "Berhenti" "Lihat jadual rakaman" "Program ini sahaja" @@ -270,25 +252,29 @@ "Sebaliknya rakamkan yang ini" "Batalkan rakaman ini" "Tonton sekarang" + "Padam rakaman..." "Boleh rakam" "Rakaman dijadualkan" "Konflik rakaman" "Merakam" "Perakaman gagal" "Membaca rancangan untuk membuat jadual rakaman" - "Membaca rancangan" - - + "Membaca rancangan" + "Lihat rakaman terbaharu" + "Rakaman %1$s tidak lengkap." + "Rakaman %1$s dan %2$s tidak lengkap." + "Rakaman %1$s, %2$s dan %3$s tidak lengkap." + "Rakaman %1$s tidak lengkap kerana storan tidak mencukupi." + "Rakaman %1$s dan %2$s tidak lengkap kerana storan tidak mencukupi." + "Rakaman %1$s, %2$s dan %3$s tidak lengkap kerana storan tidak mencukupi." "DVR memerlukan storan yang lebih besar" "Anda boleh merakam rancangan menggunakan DVR. Walau bagaimanapun storan pada peranti anda kini tidak mencukupi untuk DVR berfungsi. Sila sambungkan pemacu luaran %1$sGB atau lebih besar dan ikut langkah untuk memformat pemacu itu sebagai storan peranti." + "Storan tidak mencukupi" + "Rancangan ini tidak akan dirakamkan kerana storan tidak mencukupi. Cuba padamkan beberapa rakaman sedia ada." "Storan hilang" - "Beberapa storan yang digunakan oleh DVR telah hilang. Sila sambungkan pemacu luaran yang anda gunakan sebelum ini untuk mendayakan semula DVR. Secara alternatif, anda boleh memilih untuk melupakan storan jika storan itu tidak lagi tersedia." - "Lupakan storan?" - "Semua kandungan dan jadual anda yang dirakamkan akan hilang." "Berhenti merakam?" "Kandungan yang dirakamkan akan disimpan." - - + "Rakaman %1$s akan dihentikan kerana wujud konflik dengan rancangan ini. Kandungan yang dirakamkan akan disimpan." "Rakaman dijadualkan tetapi wujud konflik" "Rakaman telah bermula tetapi wujud konflik" "%1$s akan dirakamkan." @@ -306,14 +292,27 @@ "Rancangan yang sama telah dijadualkan akan dirakamkan pada %1$s." "Sudah dirakamkan" "Rancangan ini telah dirakamkan dan tersedia di pustaka DVR." - - - - - - - - + "Rakaman siri telah dijadualkan" + + %1$d rakaman telah dijadualkan untuk %2$s. + %1$d rakaman telah dijadualkan untuk %2$s. + + + %1$d rakaman telah dijadualkan untuk %2$s. %3$d daripadanya tidak akan dirakamkan kerana berlaku konflik. + %1$d rakaman telah dijadualkan untuk %2$s. Rakaman ini tidak akan dijalankan kerana berlaku konflik. + + + %1$d rakaman telah dijadualkan untuk %2$s. %3$d episod siri ini dan siri yang lain tidak akan dirakamkan kerana berlaku konflik. + %1$d rakaman telah dijadualkan untuk %2$s. %3$d episod siri ini dan siri yang lain tidak akan dirakamkan kerana berlaku konflik. + + + %1$d rakaman telah dijadualkan untuk %2$s. 1 episod siri yang lain tidak akan dirakamkan kerana berlaku konflik. + %1$d rakaman telah dijadualkan untuk %2$s. 1 episod siri yang lain tidak akan dirakamkan kerana berlaku konflik. + + + %1$d rakaman telah dijadualkan untuk %2$s. %3$d episod siri yang lain tidak akan dirakamkan kerana berlaku konflik. + %1$d rakaman telah dijadualkan untuk %2$s. %3$d episod siri yang lain tidak akan dirakamkan kerana berlaku konflik. + "Program yang dirakam tidak ditemui." "Rakaman yang berkaitan" "(Tiada perihalan program)" @@ -336,6 +335,7 @@ "Berhenti merakam siri?" "Episod yang dirakamkan akan kekal tersedia dalam pustaka DVR." "Berhenti" + "Tiada episod sedang dalam siaran." "Tiada episod yang tersedia.\nEpisod ini akan dirakamkan apabila sudah tersedia." (%1$d minit) diff --git a/res/values-my-rMM/strings.xml b/res/values-my-rMM/strings.xml index 1ab6baf7..8e9cc4dd 100644 --- a/res/values-my-rMM/strings.xml +++ b/res/values-my-rMM/strings.xml @@ -20,9 +20,8 @@ "မိုနို" "စတီရီယို" "Play ထိန်းချုပ်မှုများ" - "မကြာမီက ချာနယ်" + "ချာနယ်များ" "TV ရွေးစရာများ" - "PIP ရွေးချယ်စရာများ" "ဤလိုင်းအတွက် အဖွင့်ထိန်းချုပ်ခြင်းများ မရနိုင်ပါ" "ဖွင့်ပါ သို့မဟုတ် ခဏရပ်ပါ" "ရှေ့သို့ အမြန်သွားရန်" @@ -35,33 +34,15 @@ "စာတမ်းထိုးများ" "မြင်ကွင်း မုဒ်" "PIP" - "ဖွင့်ထား" - "ပိတ်ထား" "အသံစုံ" "နောက်ထပ်ချန်နယ်များ ရယူရန်" "ဆက်တင်များ" - "ရင်းမြစ်" - "လဲပြောင်းသည်" - "ဖွင့်ထား" - "ပိတ်ထား" - "အသံ" - "အဓိက" - "PIP ဝင်းဒိုး" - "အဆင်အပြင်" - "အောက်ညာ" - "အပေါ်ညာ" - "အပေါ်ဘယ်" - "အောက်ဘယ်" - "ကပ်လျက်" - "ဆိုက်" - "ကြီး" - "သေး" - "ထည့်သွင်းမှု ရင်းမြစ်" "တီဗီ (ဧရီယာတိုင်/ကြိုး)" "အစီအစဉ် အချက်အလက်များ မရှိ" "သတင်းအချက်အလက် မရှိပါ" "ပိတ်ဆို့ ချာနယ်" - "အမည်မသိဘာသာစကား" + "အမည်မသိဘာသာစကား" + "စာတန်းထိုး %1$d" "စာတမ်းထိုးများ" "ပိတ်ထား" "စိတ်ကြိုက်ပုံစံချရန်" @@ -135,6 +116,10 @@ "ထို PIN မှာ မှားနေသည်။ ထပ်ကြိုးစားပါ။" "PIN မှာ မတိုက်ဆိုင်ပါ၊ ထပ်ပြီး စမ်းပါ။" + "စာတိုက်ကုဒ်ကို ထည့်ပါ။" + "TV ချန်နယ်များအတွက် ပြီးပြည့်စုံသည့် အစီအစဉ်များကို ပံ့ပိုးပေးရန် Live TV က စာတိုက်ကုဒ်ကိုသုံးပါမည်။" + "စာတိုက်ကုဒ်ကို ထည့်ပါ" + "စာတိုက်သင်္က​ေတ မမှန်ပါ" "ဆက်တင်များ" "ချန်နယ်စာရင်းကို စိတ်တိုင်းကျပြုပြင်ရန်" "သင့်ပရိုဂရမ်လမ်းညွှန်အတွက် ချန်နယ်များရွေးချယ်ပါ" @@ -143,6 +128,7 @@ "မိဘ ထိန်းချုပ်မှု" "အခမဲ့အရင်းအမြစ်လိုင်စင်များ" "အခမဲ့ ရင်းမြစ် လိုင်စင်များ" + "တုံ့ပြန်ချက် ပေးပို့ပါ" "ဗားရှင်း" "ဤချာနယ်ကို ကြည့်ရန်၊ ညာဘက် နှိပ်ပြီး PIN ရိုက်ထည့်ပါ" "ဤအစီအစဉ်ကို ကြည့်ရန်၊ ညာဘက် နှိပ်ပြီး PIN ရိုက်ထည့်ပါ" @@ -181,8 +167,6 @@ " ရွေးချယ်ပါအားနှိပ်ပြီး"" တီဗီမန်နယူးကိုဝင်ရောက်ကြည့်ရှုပါ။" "တီဗီ ထည့်သွင်းမှု ရှာမတွေ့ပါ။" "တီဗီ ထည့်သွင်းမှု ရှာမတွေ့နိုင်ပါ။" - "PIP ကို ပံ့ပိုးမထားပါ။" - "PIP နှင့် ပြနိုင်သည့် ထည့်သွင်းစရာ မရှိပါ။" "သင့်တော်သည့် တျူနာ အမျိုးအစား မဟုတ်ပါ။ တီဗွီ အဝင်ပေါက်အတွက် တိုက်ရိုက်လွှင့် ချန်နယ်များ အက်ပ်အား ဖွင့်ပါ။" "ညှိမှု မအောင်မြင်ပါ" "ဤလုပ်ဆောင်ချက်ကို ကိုင်တွယ်နိုင်သည့် အက်ပ်မရှိပါ။" @@ -259,8 +243,6 @@ "သိမ်းရန်" "တစ်ကြိမ်တစ်ခါတည်း ဖမ်းယူခြင်းသည် ဦးစားပေးမှုအမြင့်ဆုံးဖြစ်သည်" "မလုပ်တော့" - "မလုပ်တော့" - "မေ့ပစ်ရန်" "ရပ်ရန်" "ဖမ်းယူခြင်းအချိန်ဇယားကို ကြည့်ရန်" "ဤအစီအစဉ် တစ်ခုတည်း" @@ -270,25 +252,29 @@ "၎င်းအစား ဤတစ်ခုကို ဖမ်းယူပါ" "ဤဖမ်းယူမှုကို ပယ်ဖျက်ရန်" "ယခုကြည့်ရန်" + "ရုပ်သံရိုက်ကူးမှုများ ဖျက်ပါ..." "ရိုက်ကူးနိုင်သည်" "ရိုက်ကူးရေးအတွက် စီစဉ်ထားပါသည်" "ရိုက်ကူးရေးအစီအစဉ်တိုက်နေပါသည်" "ဖမ်းယူနေသည်" "ဖမ်းယူခြင်း မအောင်မြင်ပါ" "ရိုက်ကူးရေး အချိန်ဇယားများ သတ်မှတ်ရန် အစီအစဉ်များကို ဖတ်နေသည်" - "ပရိုဂရမ်များကို ဖတ်နေသည်" - - + "ပရိုဂရမ်များကို ဖတ်နေသည်" + "မကြာသေးမီက ရိုက်ကူးမှုများကို ကြည့်ပါ" + "%1$s ကို ကူးယူမှု မပြီးဆုံးခဲ့ပါ။" + "%1$s နှင့် %2$s တို့ကို ကူးယူမှု မပြီးဆုံးခဲ့ပါ။" + "%1$s%2$s နှင့် %3$s တို့ကို ကူးယူမှု မပြီးဆုံးခဲ့ပါ။" + "သိုလှောင်ခန်း လုံလောက်မှု မရှိသည့်အတွက် %1$s ကို ပြီးဆုံးအောင် မကူးယူနိုင်ခဲ့ပါ။" + "သိုလှောင်ခန်း လုံလောက်မှု မရှိသည့်အတွက် %1$s နှင့် %2$s တို့ကို ပြီးစီးအောင် မကူးယူနိုင်ခဲ့ပါ။" + "သိုလှောင်ခန်း လုံလောက်မှု မရှိသည့်အတွက် %1$s%2$s နှင့် %3$s တို့ကို ပြီးစီးအောင် မကူးယူနိုင်ခဲ့ပါ။" "DVR သည် နောက်ထပ်သိုလှောင်ရန်နေရာလွတ် လိုအပ်နေသည်" "အစီအစဉ်များကို DVR နှင့် ဖမ်းယူသိမ်းဆည်းထားနိုင်ပါသည်။ သို့သော် DVR ကို အသုံးပြုနိုင်ရန် သင့်စက်ပစ္စည်းတွင် လုံလောက်သော နေရာလွတ် လောလောဆယ်မရှိပါ။ %1$sဂစ်ဂါဘိုက် သို့မဟုတ် ၎င်းနှင့်အထက်ရှိသော ပြင်ပသိုလှောင်ကိရိယာနှင့် ချိတ်ဆက်ပြီး ၎င်းကို သိုလှောင်ခန်းစက်ပစ္စည်းအဖြစ် ပြင်ဆင်သတ်မှတ်ရန် ညွှန်ကြားချက်များအတိုင်း လိုက်နာပါ။" + "သိုလှောင်ခန်း မလုံလောက်ပါ" + "သိုလှောင်ခန်း လုံလောက်မှုမရှိသည့်အတွက် ဤအစီအစဉ်ကို ရိုက်ကူးမည်မဟုတ်ပါ။ လက်ရှိရိုက်ကူးချက်အချို့ကို ဖျက်ကြည့်ပါ။" "သိုလှောင်မှုများ ပျောက်ဆုံးနေခြင်း" - "DVR က အသုံးပြုသော သိုလှောင်ခန်းအချို့မှာ ပျောက်ဆုံးနေသည်။ DVR ကို ပြန်ဖွင့်ရန်အတွက် ယခင်က အသုံးပြုခဲ့သော ပြင်ပသုံးအခွေဖွင့်စက်နှင့် ချိတ်ဆက်ပါ။ နောက်တစ်နည်းအနေဖြင့် ၎င်းကို အသုံးပြု၍ မရတော့လျှင် မေ့ပစ်ရန် ရွေးချယ်နိုင်ပါသည်။" - "သိုလှောင်ခန်းကို မေ့ပစ်မလား။" - "သင်မှတ်တမ်းတင်ထားသော အကြောင်းအရာနှင့် အချိန်ဇယားများအားလုံး ဆုံးရှုံးသွားလိမ့်မည်။" "ရိုက်ကူးခြင်းကို ရပ်လိုပါသလား။" "ဖမ်းယူထားသည့် အကြောင်းအရာကို သိမ်းဆည်းထားပါမည်။" - - + "%1$s ကို ဖမ်းယူခြင်းသည် ဤပရိုဂရမ်နှင့် ပဋိပက္ခများ ဖြစ်နေသောကြောင့် ၎င်းသည် ရပ်တန့်သွားပါမည်။ ဖမ်းယူထားသည့် အကြောင်းအရာကို သိမ်းဆည်းသွားပါမည်။" "ဖမ်းယူရန် စီစဉ်ထားသော်လည်း အချိန်ဇယားတိုက်နေပါသည်" "ဖမ်းယူမှုကို စတင်လိုက်ပါပြီ။ သို့သော် အချိန်ဇယားတိုက်နေပါသည်" "%1$s ကို ဖမ်းယူသွားပါမည်။" @@ -306,14 +292,27 @@ "တူညီသည့် ပရိုဂရမ်ကို %1$s ၌ ဖမ်းယူရန် စီစဉ်ထားပြီး ဖြစ်ပါသည်။" "ဖမ်းယူပြီးပါပြီ" "ဤပရိုဂရမ်ကို ဖမ်းယူပြီးပါပြီ။ ၎င်းကို DVR စာကြည့်တိုက်တွင် ကြည့်ရှုနိုင်ပါသည်။" - - - - - - - - + "စီးရီးများကို ဖမ်းယူခြင်းကို စီစဉ်ထားပါသည်" + + ရိုက်ကူးမှု %1$d ခုကို %2$s အတွက် စီစဉ်ထားပါသည်။ + ရိုက်ကူးမှု %1$d ခုကို %2$s အတွက် စီစဉ်ထားပါသည်။ + + + ရိုက်ကူးမှု %1$d ခုကို %2$s အတွက် စီစဉ်ထားပါသည်။ ပဋိပက္ခများ ရှိနေသဖြင့် ၎င်းတို့အနက်မှ %3$d ခုကို ကူးယူသွားမည် မဟုတ်ပါ။ + ရိုက်ကူးမှု %1$d ခုကို %2$s အတွက် စီစဉ်ထားပါသည်။ ပဋိပက္ခများ ရှိနေသဖြင့် ၎င်းကို ကူးယူသွားမည် မဟုတ်ပါ။ + + + ရိုက်ကူးမှု %1$d ခုကို %2$s အတွက် စီစဉ်ထားပါသည်။ ပဋိပက္ခများ ရှိနေသဖြင့် ဤစီးရီးနှင့် အခြားစီးရီးများ၏ ဇာတ်လမ်းပိုင်း %3$d ပိုင်းကို ကူးယူသွားမည်မဟုတ်ပါ။ + ရိုက်ကူးမှု %1$d ခုကို %2$s အတွက် စီစဉ်ထားပါသည်။ ပဋိပက္ခများ ရှိနေသဖြင့် ဤစီးရီးနှင့် အခြားစီးရီးများ၏ ဇာတ်လမ်းပိုင်း %3$d ပိုင်းကို ကူးယူသွားမည်မဟုတ်ပါ။ + + + ရိုက်ကူးမှု %1$d ခုကို %2$s အတွက် စီစဉ်ထားပါသည်။ ပဋိပက္ခများ ရှိနေသဖြင့် အခြားစီးရီးများ၏ ဇာတ်လမ်းပိုင်း ၁ ပိုင်းကို ကူးယူသွားမည်မဟုတ်ပါ။ + ရိုက်ကူးမှု %1$d ခုကို %2$s အတွက် စီစဉ်ထားပါသည်။ ပဋိပက္ခများ ရှိနေသဖြင့် အခြားစီးရီးများ၏ ဇာတ်လမ်းပိုင်း ၁ ပိုင်းကို ကူးယူသွားမည်မဟုတ်ပါ။ + + + ရိုက်ကူးမှု %1$d ခုကို %2$s အတွက် စီစဉ်ထားပါသည်။ ပဋိပက္ခများ ရှိနေသဖြင့် အခြားစီးရီးများ၏ ဇာတ်လမ်းပိုင်း %3$d ပိုင်းကို ကူးယူသွားမည်မဟုတ်ပါ။ + ရိုက်ကူးမှု %1$d ခုကို %2$s အတွက် စီစဉ်ထားပါသည်။ ပဋိပက္ခများ ရှိနေသဖြင့် အခြားစီးရီးများ၏ ဇာတ်လမ်းပိုင်း %3$d ပိုင်းကို ကူးယူသွားမည်မဟုတ်ပါ။ + "ရိုက်ကူးထားသည့်ပရိုဂရမ်ကို မတွေ့ပါ။" "ဆက်စပ်နေသည့် ရိုက်ကူးမှုများ" "(ပရိုဂရမ် ဖော်ပြချက်မရှိပါ)" @@ -336,6 +335,7 @@ "ဇာတ်လမ်းတွဲဖမ်းယူခြင်းကို ရပ်မလား။" "ဖမ်းယူထားသည့် အပိုင်းများသည် DVR စာကြည့်တိုက်တွင် ရှိနေဦးမည်ဖြစ်သည်။" "ရပ်ရန်" + "ယခု မည်သည့် ဇာတ်လမ်းပိုင်းကိုမျှ လွှင့်နေခြင်း မရှိပါ။" "အပိုင်းငယ်များ မရနိုင်သေးပါ။\nရနိုင်သည်နှင့် ၎င်းတို့ကို ဖမ်းယူသွားပါမည်။" (%1$d မိနစ်) diff --git a/res/values-nb/strings.xml b/res/values-nb/strings.xml index a2cf46cc..d3f3324a 100644 --- a/res/values-nb/strings.xml +++ b/res/values-nb/strings.xml @@ -20,9 +20,8 @@ "mono" "stereo" "Play-kontroller" - "Nylige kanaler" + "Kanaler" "TV-alternativer" - "PIP-alternativer" "Play-kontroller er ikke tilgjengelige for denne kanalen" "Spill av eller sett på pause" "Spol fremover" @@ -35,33 +34,15 @@ "Teksting" "Visningsmodus" "PIP" - "På" - "Av" "Flere lydspor" "Få flere kanaler" "Innstillinger" - "Kilde" - "Bytt" - "På" - "Av" - "Lyd" - "Hovedkontroll" - "PIP-vindu" - "Utforming" - "Nede til høyre" - "Oppe til høyre" - "Oppe til venstre" - "Nede til venstre" - "Side om side" - "Størrelse" - "Stor" - "Liten" - "Inndatakilde" "TV (antenne/kabel)" "Ingen programinformasjon" "Ingen informasjon" "Blokkert kanal" - "Ukjent språk" + "Ukjent språk" + "Teksting %1$d" "Teksting for hørselshemmede" "Av" "Tilpass formatering" @@ -135,6 +116,10 @@ "Prøv på nytt, PIN-koden er feil." "Prøv på nytt, PIN-koden er feil" + "Skriv inn postnummeret ditt." + "Direkte-TV-appen bruker postnummeret til å oppgi en fullstendig programoversikt for TV-kanalene." + "Skriv inn postnummeret ditt" + "Ugyldig postnummer" "Innstillinger" "Tilpass kanallisten" "Velg kanaler for programoversikten din" @@ -143,6 +128,7 @@ "Foreldrekontroll" "Lisenser for åpen kildekode" "Åpen kildekode-lisenser" + "Send tilbakemelding" "Versjon" "For å se på denne kanalen, trykk til høyre og skriv inn PIN-koden din" "For å se på dette programmet, trykk til høyre og skriv inn PIN-koden din" @@ -181,8 +167,6 @@ "Trykk på «SELECT» (VELG)"" for å åpne TV-menyen." "Kunne ikke finne noen TV-inngang" "Kunne ikke finne TV-inngangen" - "PIP støttes ikke" - "Det finnes ingen tilgjengelige inndata som kan vises med PIP" "Tuner-typen kan ikke brukes. Kjør Live TV-appen med tuner-typen for TV-inndata." "Justering mislyktes." "Kunne ikke finne noen app som kan håndtere denne handlingen." @@ -259,8 +243,6 @@ "Lagre" "Engangsopptak har høyeste prioritet" "Avbryt" - "Avbryt" - "Glem" "Stopp" "Se tidsplanen for opptak" "Bare dette programmet" @@ -270,25 +252,29 @@ "Spill inn dette i stedet" "Kanseller dette opptaket" "Se nå" + "Slett opptak …" "Opptaksbar" "Opptak planlagt" "Opptakskonflikt" "Tar opp" "Opptaket mislyktes" "Leser av programmer for å opprette tidsplaner for opptak" - "Leser av programmer" - - + "Leser av programmer" + "Se nylige opptak" + "Opptaket av %1$s er ufullstendig." + "Opptakene av %1$s og %2$s er ufullstendige." + "Opptakene av %1$s, %2$s og %3$s er ufullstendige." + "Opptaket av %1$s ble ikke fullført på grunn av utilstrekkelig lagringsplass." + "Opptakene av %1$s og %2$s ble ikke fullført på grunn av utilstrekkelig lagringsplass." + "Opptakene av %1$s, %2$s og %3$s ble ikke fullført på grunn av utilstrekkelig lagringsplass." "DVR trenger mer lagringsplass" "Det kommer til å være mulig til å ta opp programmer med DVR. Det er imidlertid ikke nok lagringsplass på enheten din til at DVR kan fungere. Koble til en ekstern stasjon på %1$s GB eller mer, og følg trinnene for å formatere den som lagringsenhet." + "Ikke nok lagringsplass" + "Dette programmet blir ikke tatt opp fordi det ikke er nok lagringsplass. Prøv å slette noen eksisterende opptak." "Manglende lagringsplass" - "Noe av lagringsplassen som brukes av DVR, mangler. Koble til den eksterne disken du bruke tidligere, for å slå på DVR igjen. Eventuelt kan du velge å glemme lagringsplassen hvis den ikke er tilgjengelig lenger." - "Vil du glemme lagring?" - "Alt innspilt innhold og alle tidsplanene går tapt." "Vil du stoppe opptaket?" "Det innspilte innholdet blir lagret." - - + "Opptaket av %1$s stoppes fordi det har konflikter med dette programmet. Det innspilte innholdet blir lagret." "Opptak er planlagt, men har konflikter" "Opptak har startet, men har konflikter" "%1$s blir tatt opp." @@ -306,14 +292,27 @@ "Det samme programmet er allerede planlagt for opptak %1$s." "Allerede tatt opp" "Dette programmet er allerede tatt opp. Det er tilgjengelig i DVR-biblioteket." - - - - - - - - + "Opptak av serie er planlagt" + + %1$d opptak er planlagt for %2$s. + %1$d opptak er planlagt for %2$s. + + + %1$d opptak er planlagt for %2$s. %3$d av dem blir ikke tatt opp på grunn av konflikter. + %1$d opptak er planlagt for %2$s. Det blir ikke tatt opp på grunn av konflikter. + + + %1$d opptak er planlagt for %2$s. %3$d episoder av denne serien og andre serier blir ikke tatt opp på grunn av konflikter. + %1$d opptak er planlagt for %2$s. %3$d episoder av denne serien og andre serier blir ikke tatt opp på grunn av konflikter. + + + %1$d opptak er planlagt for %2$s. Én episode av andre serier blir ikke tatt opp på grunn av konflikter. + %1$d opptak er planlagt for %2$s. Én episode av andre serier blir ikke tatt opp på grunn av konflikter. + + + %1$d opptak er planlagt for %2$s. %3$d episoder av andre serier blir ikke tatt opp på grunn av konflikter. + %1$d opptak er planlagt for %2$s. %3$d episoder av andre serier blir ikke tatt opp på grunn av konflikter. + "Finner ikke programopptaket." "Relaterte opptak" "(Ingen programbeskrivelse)" @@ -336,6 +335,7 @@ "Vil du stoppe opptaket av serien?" "Episoder som er tatt opp, er fremdeles tilgjengelige i DVR-biblioteket." "Stopp" + "Ingen episoder er på luften nå." "Ingen episoder er tilgjengelige.\nDe blir tatt opp når de er tilgjengelige." (%1$d minutter) diff --git a/res/values-ne-rNP/strings.xml b/res/values-ne-rNP/strings.xml index 142dfade..c11820f4 100644 --- a/res/values-ne-rNP/strings.xml +++ b/res/values-ne-rNP/strings.xml @@ -20,9 +20,8 @@ "मोनो" "स्टेरियो" "प्ले नियन्त्रणहरु" - "भर्खरैका च्यानलहरू" + "च्यानलहरू" "टिभी विकल्पहरू" - "PIP विकल्पहरू" "यस च्यानलका लागि प्ले नियन्त्रणहरू अनुपलब्ध" "प्ले वा पज" "फास्ट फर्वार्ड" @@ -35,33 +34,15 @@ "बन्द क्याप्सनहरु" "डिस्प्ले मोड" "PIP" - "खुला" - "बन्द" "मल्टि-अडियो" "अझ बढी च्यानलहरू प्राप्त गर्नुहोस्" "सेटिङहरू" - "स्रोत" - "स्वाप" - "खुला" - "बन्द" - "आवाज" - "मुख्य" - "PIP सन्झ्याल" - "लेआउट" - "तल्लो दायाँ" - "माथिल्लो दायाँ" - "माथिल्लो बायाँ" - "तल्लो बायाँ" - "सँगसँगै" - "आकार" - "ठूलो" - "सानो" - "इनपुट स्रोत" "टिभी (एन्टेना/केबल)" "कुनै पनि कार्यक्रम जानकारी छैन" "केही सूचना छैन" "अवरुद्ध च्यानल" - "अज्ञात भाषा" + "अज्ञात भाषा" + "उपशीर्षकहरू %1$d" "उपशीर्षक" "बन्द गर्नुहोस्" "फर्‍म्याटिङ अनुकूलित" @@ -73,7 +54,7 @@ "५.१ सराउन्ड" "७.१ सराउन्ड" "%d च्यानलहरु" - "च्यानल सूची अनुकूलन गर्नुहोस्" + "च्यानल सूची आफू अनुकूल गर्नुहोस्" "समूह छान्नुहोस्" "समूह अचयन नगर्नुहोस्" "द्वारा समूह" @@ -112,7 +93,7 @@ "उच्च प्रतिबन्धहरू" "मध्यम प्रतिबन्धहरू" "न्यून प्रतिबन्धहरू" - "अनुकूलन" + "आफू अनुकूल" "बालबालिकाका लागि उपयुक्त सामग्री" "ठूला बच्चाहरुका लागि उपयुक्त सामग्री" "किशोर किशोरीहरुका लागि उपयुक्त सामग्री" @@ -135,14 +116,19 @@ "त्यो PIN गलत थियो। पुनः प्रयास गर्नुहोस्।" "पुनः प्रयास गर्नुहोस्, PIN मेल खाँदैन" + "आफ्नो जिप कोड प्रविष्ट गर्नुहोस्।" + "TV च्यानलहरूको कार्यक्रमको बारेमा समग्र मार्गदर्शन प्रदान गर्न लाइभ च्यानल अनुप्रयोगले उक्त जिप कोड प्रयोग गर्नेछ।" + "आफ्नो जिप कोड प्रविष्ट गर्नुहोस्" + "अमान्य ZIP कोड" "सेटिङहरू" - "च्यानलको सूची अनुकूलन गर्नुहोस्" + "च्यानलको सूची आफू अनुकूल गर्नुहोस्" "आफ्नो कार्यक्रम निर्देशिकाको लागि च्यानलहरू छनौट गर्नुहोस्" "च्यानलका स्रोतहरू" "नयाँ च्यानलहरू उपलब्ध छन्" "अभिभावकीय नियन्त्रणहरू" "स्रोतका इजाजतपत्रहरू खोल्नुहोस्" - "खुला स्रोत इजाजतपत्रहरू" + "खुला स्रोतका इजाजतपत्रहरू" + "प्रतिक्रिया पठाउनुहोस्" "संस्करण" "यो च्यानल हेर्न, दायाँ प्रेस गर्नुहोस् र आफ्नो पिन प्रविष्ट गर्नुहोस्" "यो कार्यक्रम हेर्न, दायाँ प्रेस गर्नुहोस् र आफ्नो पिन प्रविष्ट गर्नुहोस्" @@ -181,8 +167,6 @@ "टिभी मेनु खोल्न ""SELECT थिच्नुहोस्""।" "कुनै पनि टिभी स्रोत भेटिएन।" "टिभी स्रोत भेटिएन लागेन" - "PIP समर्थित छैन" - "PIP सँग देखाउन सकिने कुनै स्रोत उपलब्ध छैन।" "ट्यूनर प्रकार अनुपयुक्त। कृपया ट्यूनर प्रकार टिभी स्रोतको लागि Live TV को अनुप्रयोग सुरु गर्नुहोस।" "ट्युन गर्न असफल" "यो कार्य सम्हाल्न कुनै पनि अनुप्रयोग भेटिएन।" @@ -259,8 +243,6 @@ "सुरक्षित गर्नुहोस्" "एक-पटके रेकर्डिङहरूलाई सबैभन्दा उच्च प्राथमिकता दिइन्छ" "रद्द गर्नु" - "रद्द गर्नुहोस्" - "बिर्सनुहोस्" "रोक्नुहोस्" "रेकर्डिङको समयतालिका हेर्नुहोस्" "यो कार्यक्रम मात्र" @@ -270,25 +252,29 @@ "बरु यो रेकर्ड गर्नुहोस्" "यस रेकर्डिङलाई रद्द गर्नुहोस्" "अहिले हेर्नुहोस्" + "रेकर्डिङहरू मेट्नुहोस्…" "रेकर्ड गर्न मिल्ने" "रेकर्डिङको कार्यतालिका निर्धारण गरिएको छ" "रेकर्डिङ सम्बन्धी असहमति" "रेकर्ड गर्दै" "रेकर्डिङ गर्न सकिएन" "रेकर्डिङका समय तालिकाहरू सिर्जना गर्न कार्यक्रमहरू पढ्दै" - "कार्यक्रमहरूको जानकारी पढ्दै" - - + "कार्यक्रमहरूको जानकारी पढ्दै" + "हालका रेकर्डिङहरू हेर्नुहोस्" + "%1$s को रेकर्डिङ अपूर्ण छ।" + "%1$s%2$s का रेकर्डिङहरू अपूर्ण छन्।" + "%1$s, %2$s%3$s का रेकर्डिङहरू अपूर्ण छन्।" + "भण्डारण स्थान अपर्याप्त भएकाले %1$s को रेकर्डिङ पूरा भएन।" + "भण्डारण स्थान अपर्याप्त भएकाले %1$s%2$s का रेकर्डिङहरू पूरा भएनन्।" + "भण्डारण स्थान अपर्याप्त भएकाले %1$s, %2$s%3$s का रेकर्डिङहरू पूरा भएनन्।" "DVR लाई थप भण्डारण चाहिन्छ" "तपाईँले DVR मार्फत कार्यक्रमहरू रेकर्ड गर्न सक्नुहुनेछ। यद्यपि, अहिले तपाईँको यन्त्रमा DVR ले काम गर्न पुग्ने गरी पर्याप्त भण्डारण छैन। कृपया %1$s जि.बि.वा सो भन्दा बढी भण्डारण क्षमता भएको कुनै बाह्य ड्राइभ जडान गर्नुहोस् र त्यसलाई यन्त्रको भण्डारणका रूपमा फर्म्याट गर्न आवश्यक कदमहरू चाल्नुहोस्।" + "पर्याप्त भण्डारण छैन" + "पर्याप्त भण्डारण उपलब्ध नभएको हुनाले यस कार्यक्रमलाई रेकर्ड गरिने छैन। केही विद्यमान रेकर्डिङहरू मेटाई हेर्नुहोस्।" "भण्डारण उपलब्ध छैन" - "DVR ले प्रयोग गरेको केही भण्डारण उपलब्ध छैन। DVR लाई पुन: सक्षम पार्न कृपया तपाईँले पहिले प्रयोग गर्नुभएको बाह्य ड्राइभलाई जडान गर्नुहोस्। वैकल्पिक रूपमा, यदि अब भण्डारण उपलब्ध छैन भने तपाईँ त्यसलाई बिर्सने विकल्प छान्न सक्नुहुन्छ।" - "भण्डारण बिर्सने हो?" - "तपाईँका रेकर्ड गरिएका सबै सामग्री र समय सहितका कार्यतालिकाहरू हराउने छन्।" "रेकर्डिङ रोक्ने हो?" "रेकर्ड गरिएको सामग्रीलाई सुरक्षित गरिनेछ।" - - + "यस कार्यक्रमसँग परस्पर विरोधी भएकाले %1$s को रेकर्डिङ रोकिने छ। रेकर्ड गरिएका सामग्रीहरू सुरक्षित गरिने छन्।" "रेकर्डिङको कार्यतालिका निर्धारण गरिएको छ तर यसमा असहमतिहरू छन्" "रेकर्डिङ सुरु भएको छ तर यसमा असहमतिहरू छन्" "%1$s लाई रेकर्ड गरिनेछ।" @@ -306,14 +292,27 @@ "यस कार्यक्रमलाई पहिले नै %1$s मा रेकर्ड गर्न समय सहितको कार्यतालिका निर्धारण गरिएको छ।" "पहिले नै रेकर्ड गरिएको छ" "यस कार्यक्रमलाई पहिले नै रेकर्ड गरिएको छ। यो DVR को लाइब्रेरीमा उपलब्ध छ।" - - - - - - - - + "शृङ्खलाहरूको रेकर्डिङको समय तालिका बनाइयो" + + %2$s का %1$d रेकर्डिङहरूको समय तालिका तय गरिएको छ। + %2$s को %1$d रेकर्डिङको समय तालिका तय गरिएको छ। + + + %2$s का %1$d रेकर्डिङहरूको समय तालिका तय गरिएको छ। तालमेल नमिलेका कारण ती मध्ये %3$d लाई रेकर्ड गरिने छैन। + %2$s को %1$d रेकर्डिङको समय तालिका तय गरिएको छ। तालमेल नमिलेका कारण यसलाई रेकर्ड गरिने छैन। + + + %2$s का %1$d रेकर्डिङहरूको समय तालिका तय गरिएको छ। तालमेल नमिलेका कारण यस शृङ्खला र अन्य शृङ्खलाका %3$d एपिसोडहरूलाई रेकर्ड गरिने छैन। + %2$s को %1$d रेकर्डिङको समय तालिका तय गरिएको छ। तालमेल नमिलेका कारण यस शृङ्खला र अन्य शृङ्खलाका %3$d एपिसोडहरूलाई रेकर्ड गरिने छैन। + + + %2$s का %1$d रेकर्डिङहरूको समय तालिका तय गरिएको छ। तालमेल नमिलेका कारण अन्य शृङ्खलाको १ एपिसोडलाई रेकर्ड गरिने छैन। + %2$s को %1$d रेकर्डिङको समय तालिका तय गरिएको छ। तालमेल नमिलेका कारण अन्य शृङ्खलाको १ एपिसोडलाई रेकर्ड गरिने छैन। + + + %2$s का %1$d रेकर्डिङहरूको समय तालिका तय गरिएको छ। तालमेल नमिलेका कारण अन्य शृङ्खलाका %3$d एपिसोडहरूलाई रेकर्ड गरिने छैन। + %2$s को %1$d रेकर्डिङको समय तालिका तय गरिएको छ। तालमेल नमिलेका कारण अन्य शृङ्खलाका %3$d एपिसोडहरूलाई रेकर्ड गरिने छैन। + "रेकर्ड गरिएको कार्यक्रम भेट्टिएन।" "सम्बन्धित रेकर्डिङहरू" "(कार्यक्रम सम्बन्धी वर्णन छैन)" @@ -336,6 +335,7 @@ "शृंखलाको रेकर्डिङ रोक्ने हो?" "रेकर्ड गरिएका एपिसोडहरू DVR सम्बन्धी लाइब्रेरीमा उपलब्ध रहनेछन्।" "रोक्नुहोस्" + "अहिले कुनै एपिसोड प्रसारण भइरहेको छैन।" "कुनै एपिसोड उपलब्ध छैन।\nएपिसोडहरू उपलब्ध भएपछि तिनीहरूलाई रेकर्ड गरिनेछ।" (%1$d मिनेट) diff --git a/res/values-nl/strings.xml b/res/values-nl/strings.xml index f24ed1c3..218fbb9c 100644 --- a/res/values-nl/strings.xml +++ b/res/values-nl/strings.xml @@ -20,9 +20,8 @@ "mono" "stereo" "Afspeelknoppen" - "Recente kanalen" + "Kanalen" "Tv-opties" - "PIP-opties" "Er zijn geen afspeelknoppen beschikbaar voor dit kanaal" "Afspelen of onderbreken" "Vooruitspoelen" @@ -32,36 +31,18 @@ "Programmagids" "Nieuwe kanalen beschikbaar" "%1$s openen" - "Ondertiteling" + "Ondertitels" "Weergavemodus" "PIP" - "Aan" - "Uit" "Multi-audio" "Kanalen ophalen" "Instellingen" - "Bron" - "Wisselen" - "Aan" - "Uit" - "Geluid" - "Hoofd" - "PIP-venster" - "Lay-out" - "Rechtsonder" - "Rechtsboven" - "Linksboven" - "Linksonder" - "Naast elkaar" - "Formaat" - "Groot" - "Klein" - "Invoerbron" "Tv (antenne/kabel)" "Geen programmagegevens" "Geen informatie" "Geblokkeerd kanaal" - "Onbekende taal" + "Onbekende taal" + "Ondertiteling %1$d" "Ondertiteling" "Uit" "Opmaak aanpassen" @@ -135,6 +116,10 @@ "Die pincode is onjuist. Probeer het opnieuw." "Probeer het opnieuw; de pincode komt niet overeen" + "Je postcode opgeven" + "De app Live tv gebruikt je postcode om een volledige programmagids voor de tv-kanalen aan te leveren." + "Geef je postcode op" + "Ongeldige postcode" "Instellingen" "Kanaallijst aanpassen" "Kanalen kiezen voor je tv-gids" @@ -143,6 +128,7 @@ "Ouderlijk toezicht" "Open-sourcelicenties" "Open-sourcelicenties" + "Feedback verzenden" "Versie" "Als je dit kanaal wilt bekijken, druk je rechts en geef je je pincode op" "Als je dit programma wilt bekijken, druk je rechts en geef je je pincode op" @@ -181,15 +167,13 @@ "Druk op SELECTEREN"" voor toegang tot het tv-menu." "Geen tv-invoer gevonden" "Kan de tv-invoer niet vinden" - "PIP wordt niet ondersteund" - "Geen beschikbare invoer die kan worden weergegeven met PIP" - "Tunertype is geen geschikte optie. Start de app Live kanalen voor tv-invoer van het type tuner." + "Tunertype is geen geschikte optie. Start de Live tv-app voor tv-invoer van het type tuner." "Afstemmen mislukt" "Er is geen app gevonden om deze actie uit te voeren." "Alle bronkanalen zijn verborgen.\nSelecteer ten minste één kanaal om te bekijken." "De video is onverwacht niet beschikbaar" "De toets TERUG is voor verbonden apparaten. Druk op de knop HOME om te sluiten." - "De app \'Live kanalen\' heeft toestemming nodig om tv-vermeldingen te lezen." + "De app \'Live tv\' heeft toestemming nodig om tv-vermeldingen te lezen." "Je bronnen configureren" "Live tv combineert de functionaliteit van traditionele tv-kanalen met streaming kanalen die worden geleverd door apps. \n\nJe kunt aan de slag gaan door de kanaalbronnen te configureren die al zijn geïnstalleerd. Of browse in de Google Play Store voor meer apps die live tv aanbieden." "Opnamen en planningen" @@ -259,8 +243,6 @@ "Opslaan" "Eenmalige opnamen krijgen de hoogste prioriteit" "Annuleren" - "Annuleren" - "Vergeten" "Stoppen" "Opnameschema bekijken" "Dit afzonderlijke programma" @@ -270,25 +252,29 @@ "Dit programma opnemen" "Deze opname annuleren" "Nu bekijken" + "Opnamen verwijderen" "Kan worden opgenomen" "Opname gepland" "Opnameconflict" "Opnemen" "Opname mislukt" "Programma\'s lezen om opnameplanningen te maken" - "Programma\'s lezen" - - + "Programma\'s lezen" + "Recente opnamen bekijken" + "De opname van %1$s is niet voltooid." + "De opnamen van %1$s en %2$s zijn niet voltooid." + "De opnamen van %1$s, %2$s en %3$s zijn niet voltooid." + "De opname van %1$s is niet voltooid vanwege onvoldoende opslagruimte." + "De opnamen van %1$s en %2$s zijn niet voltooid vanwege onvoldoende opslagruimte." + "De opnamen van %1$s, %2$s en %3$s zijn niet voltooid vanwege onvoldoende opslagruimte." "Voor DVR is meer opslagruimte nodig" "Je kunt programma\'s opnemen met DVR. Er is echter momenteel onvoldoende opslagruimte beschikbaar op je apparaat om DVR te gebruiken. Sluit een externe schijf aan die %1$s GB of groter is en volg de stappen om deze te formatteren als apparaatopslag." + "Onvoldoende opslagruimte" + "Dit programma wordt niet opgenomen omdat er onvoldoende opslagruimte is. Verwijder een aantal bestaande opnamen." "Opslag ontbreekt" - "Een deel van de opslagruimte ontbreekt die door de DVR wordt gebruikt. Sluit de externe schijf aan die je eerder hebt gebruikt om DVR opnieuw in te schakelen. Je kunt er ook voor kiezen de opslagruimte te vergeten als deze niet langer beschikbaar is." - "Opslag vergeten?" - "Al je opgenomen content en planningen gaan verloren." "Opname stoppen?" "De opgenomen content wordt opgeslagen." - - + "De opname van %1$s wordt gestopt omdat deze conflicteert met dit programma. De opgenomen content wordt opgeslagen." "Opname ingepland, maar heeft conflicten" "De opname is gestart, maar heeft conflicten" "%1$s wordt opgenomen." @@ -306,14 +292,27 @@ "Hetzelfde programma is al ingepland voor opname om %1$s." "Al opgenomen" "Dit programma is al opgenomen. Het is beschikbaar in de DVR-bibliotheek." - - - - - - - - + "Serie-opname gepland" + + Er zijn %1$d opnamen gepland voor %2$s. + Er is %1$d opname gepland voor %2$s. + + + Er zijn %1$d opnamen gepland voor %2$s. %3$d hiervan worden niet opgenomen vanwege conflicten. + Er is %1$d opname gepland voor %2$s. Dit wordt niet opgenomen vanwege conflicten. + + + Er zijn %1$d opnamen gepland voor %2$s. %3$d afleveringen van deze serie en andere series worden niet opgenomen vanwege conflicten. + Er is %1$d opname gepland voor %2$s. %3$d afleveringen van deze serie en andere series worden niet opgenomen vanwege conflicten. + + + Er zijn %1$d opnamen gepland voor %2$s. 1 aflevering van andere series wordt niet opgenomen vanwege conflicten. + Er is %1$d opname gepland voor %2$s. 1 aflevering van andere series wordt niet opgenomen vanwege conflicten. + + + Er zijn %1$d opnamen gepland voor %2$s. %3$d afleveringen van andere series worden niet opgenomen vanwege conflicten. + Er is %1$d opname gepland voor %2$s. %3$d afleveringen van andere series worden niet opgenomen vanwege conflicten. + "Opgenomen programma niet gevonden." "Gerelateerde opnamen" "(Geen programmabeschrijving)" @@ -336,6 +335,7 @@ "Serie-opname stoppen?" "Opgenomen afleveringen blijven beschikbaar in de DVR-bibliotheek." "Stoppen" + "Er worden nu geen afleveringen uitgezonden." "Er zijn geen afleveringen beschikbaar.\nZe worden opgenomen zodra ze beschikbaar zijn." (%1$d minuten) diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml index 1e86cb7a..0b8a5c16 100644 --- a/res/values-pl/strings.xml +++ b/res/values-pl/strings.xml @@ -20,9 +20,8 @@ "mono" "stereo" "Sterowanie odtwarzaniem" - "Ostatnie kanały" + "Kanały" "Opcje TV" - "Opcje PIP" "Elementy sterujące Play są niedostępne dla tego kanału" "Odtwórz lub wstrzymaj" "Przewiń do przodu" @@ -35,33 +34,15 @@ "Napisy" "Tryb wyświetl." "PIP" - "Włączony" - "Wyłączony" "Wiele kan. audio" "Więcej kanałów" "Ustawienia" - "Źródło" - "Przełącz" - "Włączone" - "Wyłączone" - "Dźwięk" - "Główne okno" - "Okno PIP" - "Układ" - "Prawy dolny róg" - "Prawy górny róg" - "Lewy górny róg" - "Lewy dolny róg" - "Obok siebie" - "Rozmiar" - "Duży" - "Mały" - "Źródło sygnału" "TV (antena/kabel)" "Brak informacji o programach" "Brak informacji" "Kanał zablokowany" - "Nieznany język" + "Nieznany język" + "Napisy: %1$d" "Napisy" "Wył." "Dostosuj format" @@ -139,6 +120,10 @@ "Nieprawidłowy PIN. Spróbuj ponownie" "Spróbuj ponownie. Niezgodny kod PIN" + "Wpisz kod pocztowy." + "Aplikacja Telewizja online będzie używać kodu pocztowego, by udostępniać kompletny przewodnik po programach telewizyjnych." + "Wpisz kod pocztowy" + "Nieprawidłowy kod pocztowy" "Ustawienia" "Dostosuj listę kanałów" "Wybierz kanały do przewodnika TV" @@ -147,6 +132,7 @@ "Kontrola rodzicielska" "Licencje open source" "Licencje open source" + "Prześlij opinię" "Wersja" "Aby oglądać ten kanał, naciśnij Prawo i wpisz kod PIN" "Aby oglądać ten program, naciśnij Prawo i wpisz kod PIN" @@ -189,8 +175,6 @@ "Naciśnij SELECT"", by otworzyć menu TV." "Nie znaleziono wejścia TV" "Nie można znaleźć wejścia TV" - "Tryb PIP nie jest obsługiwany" - "Brak sygnału wejściowego do wyświetlenia w trybie PIP" "Nieodpowiedni typ tunera. Uruchom aplikację Telewizja online na jego wejściu TV." "Strojenie nie powiodło się" "Nie znaleziono aplikacji do obsługi tego działania." @@ -279,8 +263,6 @@ "Zapisz" "Nagrania jednorazowe mają najwyższy priorytet" "Anuluj" - "Anuluj" - "Zapomnij" "Zatrzymaj" "Pokaż harmonogram nagrywania" "Tylko ten program" @@ -290,25 +272,29 @@ "Nagraj to w zamian" "Anuluj to nagrywanie" "Obejrzyj teraz" + "Usuń nagrania…" "Można nagrać" "Zaplanowano nagrywanie" "Konflikt nagrywania" "Nagrywam" "Nie udało się nagrać" "Odczytuję programy, by utworzyć harmonogram nagrywania" - "Odczytuję programy" - - + "Odczytuję programy" + "Zobacz ostatnie nagrania" + "Nagranie %1$s jest niepełne." + "Nagrania %1$s i %2$s są niepełne." + "Nagrania %1$s, %2$s i %3$s są niepełne." + "Nie udało się dokończyć nagrania %1$s z powodu braku miejsca." + "Nie udało się dokończyć nagrań %1$s i %2$s z powodu braku miejsca." + "Nie udało się dokończyć nagrań %1$s, %2$s i %3$s z powodu braku miejsca." "Nagrywarka DVR potrzebuje więcej miejsca" "Dzięki funkcji nagrywarki DVR możesz nagrywać programy, ale obecnie na urządzeniu jest za mało miejsca, by można było z niej korzystać. Podłącz dysk zewnętrzny o pojemności co najmniej %1$s GB i postępuj zgodnie z instrukcjami, by sformatować go jako pamięć urządzenia." + "Za mało miejsca" + "Nie można nagrać tego programu, bo brakuje miejsca. Usuń któreś z wcześniejszych nagrań." "Brak dostępu do pamięci" - "Brak dostępu do części pamięci wykorzystywanej przez DVR. Podłącz dysk zewnętrzny, którego używasz, zanim włączysz DVR ponownie. Jeśli nie masz już tego dysku zewnętrznego, możesz go zapomnieć." - "Zapomnieć pamięć nagrywarki?" - "Wszystkie zapisane treści i zaplanowane nagrania zostaną utracone." "Zatrzymać nagrywanie?" "Nagrana treść zostanie zapisana." - - + "Nagrywanie programu „%1$s” zostanie zatrzymane, bo jest w konflikcie z tym programem. Nagrane treści zostaną zachowane." "Nagrywanie zostało zaplanowane, ale wystąpiły konflikty" "Zaczęło się nagrywanie, ale występują konflikty" "Program %1$s zostanie nagrany." @@ -328,14 +314,37 @@ "Nagrywanie tego samego programu zostało już zaplanowane na %1$s." "Już nagrany" "Ten program został już nagrany. Jest dostępny w bibliotece nagrywarki DVR." - - - - - - - - + "Zaplanowano nagrywanie serialu" + + Zaplanowano %1$d nagrania serialu %2$s. + Zaplanowano %1$d nagrań serialu %2$s. + Zaplanowano %1$d nagrania serialu %2$s. + Zaplanowano %1$d nagranie serialu %2$s. + + + Zaplanowano %1$d nagrania serialu %2$s. Z powodu pokrywających się harmonogramów nie uda się nagrać %3$d odcinków. + Zaplanowano %1$d nagrań serialu %2$s. Z powodu pokrywających się harmonogramów nie uda się nagrać %3$d odcinków. + Zaplanowano %1$d nagrania serialu %2$s. Z powodu pokrywających się harmonogramów nie uda się nagrać %3$d odcinków. + Zaplanowano %1$d nagranie serialu %2$s. Z powodu pokrywających się harmonogramów nie uda się nagrać tego odcinka. + + + Zaplanowano %1$d nagrania serialu %2$s. Z powodu pokrywających się harmonogramów nie uda się nagrać %3$d odcinków tego i innych seriali. + Zaplanowano %1$d nagrań serialu %2$s. Z powodu pokrywających się harmonogramów nie uda się nagrać %3$d odcinków tego i innych seriali. + Zaplanowano %1$d nagrania serialu %2$s. Z powodu pokrywających się harmonogramów nie uda się nagrać %3$d odcinków tego i innych seriali. + Zaplanowano %1$d nagranie serialu %2$s. Z powodu pokrywających się harmonogramów nie uda się nagrać %3$d odcinków tego i innych seriali. + + + Zaplanowano %1$d nagrania serialu %2$s. Przez to nie uda się nagrać 1 odcinka innego serialu. + Zaplanowano %1$d nagrań serialu %2$s. Przez to nie uda się nagrać 1 odcinka innego serialu. + Zaplanowano %1$d nagrania serialu %2$s. Przez to nie uda się nagrać 1 odcinka innego serialu. + Zaplanowano %1$d nagranie serialu %2$s. Przez to nie uda się nagrać 1 odcinka innego serialu. + + + Zaplanowano %1$d nagrania serialu %2$s. Przez to nie uda się nagrać %3$d odcinków innych seriali. + Zaplanowano %1$d nagrań serialu %2$s. Przez to nie uda się nagrać %3$d odcinków innych seriali. + Zaplanowano %1$d nagrania serialu %2$s. Przez to nie uda się nagrać %3$d odcinków innych seriali. + Zaplanowano %1$d nagranie serialu %2$s. Przez to nie uda się nagrać %3$d odcinków innych seriali. + "Nie znaleziono nagranego programu." "Powiązane nagrania" "(Brak opisu programu)" @@ -362,6 +371,7 @@ "Zatrzymać nagrywanie cykliczne?" "Nagrane odcinki będą dostępne w bibliotece nagrywarki DVR." "Zatrzymaj" + "W tej chwili nie są nadawane żadne odcinki." "Brak dostępnych odcinków.\nZostaną one nagrane, gdy będą dostępne." (%1$d minuty) diff --git a/res/values-pt-rPT/strings.xml b/res/values-pt-rPT/strings.xml index 45fa4e20..480aac5a 100644 --- a/res/values-pt-rPT/strings.xml +++ b/res/values-pt-rPT/strings.xml @@ -20,9 +20,8 @@ "mono" "estéreo" "Controlos de reprodução" - "Canais recentes" + "Canais" "Opções de TV" - "Opções de PIP" "Controlos de reprodução indisponíveis para este canal" "Reproduzir ou colocar em pausa" "Avançar" @@ -35,33 +34,15 @@ "Legendas" "Modo de apres." "PIP" - "Ativado" - "Desativado" "Multiáudio" "Obter mais canais" "Definições" - "Fonte" - "Alternar" - "Ativado" - "Desativado" - "Som" - "Principal" - "Janela de PIP" - "Esquema" - "Canto inf. dir." - "Canto sup. dir." - "Canto sup. esq." - "Canto inf. esq." - "Lado a lado" - "Tamanho" - "Grande" - "Pequeno" - "Fonte de entrada" "TV (antena/cabo)" "Sem informação de programação" "Sem informações" "Canal bloqueado" - "Idioma desconhecido" + "Idioma desconhecido" + "Legendas %1$d" "Legendas" "Desativado" "Person. a formatação" @@ -135,6 +116,10 @@ "Esse PIN estava errado. Tente novamente." "Tente novamente, o PIN não corresponde" + "Introduza o seu código postal." + "A aplicação Canais em Direto utiliza o código postal para disponibilizar um guia de programação completo para os canais de TV." + "Introduza o seu código postal" + "Código postal inválido" "Definições" "Personalizar lista de canais" "Escolher canais para o guia de programação" @@ -143,6 +128,7 @@ "Controlo parental" "Licenças de código aberto" "Licenças de código aberto" + "Enviar comentários" "Versão" "Para ver este canal, prima Direito e introduza o PIN" "Para ver este programa, prima Direito e introduza o PIN" @@ -181,8 +167,6 @@ "Prima SELECT"" para aceder ao menu da TV." "Nenhuma entrada de TV encontrada" "Não é possível localizar a entrada de TV" - "Não é suportada a opção PIP" - "Nenhuma entrada disponível para apresentar com PIP" "Tipo de sintonizador inadequado. Inicie a aplicação Canais em direito para a entrada de TV do tipo de sintonizador." "Falha ao sintonizar" "Não foram encontradas aplicações para executar esta ação." @@ -259,8 +243,6 @@ "Guardar" "As gravações únicas têm a prioridade mais alta" "Cancelar" - "Cancelar" - "Esquecer" "Parar" "Ver horários de gravação" "Este programa único" @@ -270,25 +252,29 @@ "Gravar antes este" "Cancelar esta gravação" "Ver agora" + "Eliminar gravações" "Gravável" "Gravação agendada" "Conflito de gravação" "A gravar" "Falha na gravação" "A ler os programas para criar horários de gravação…" - "A ler os programas…" - - + "A ler os programas…" + "Ver gravações recentes" + "A gravação de %1$s está incompleta." + "As gravações de %1$s e %2$s estão incompletas." + "As gravações de %1$s, %2$s e %3$s estão incompletas." + "A gravação de %1$s não foi concluída devido a armazenamento insuficiente." + "As gravações de %1$s e %2$s não foram concluídas devido a armazenamento insuficiente." + "As gravações de %1$s, %2$s e %3$s não foram concluídas devido a armazenamento insuficiente." "O DVR necessita de mais armazenamento" "Pode gravar programas com o DVR. Contudo, não existe neste momento armazenamento suficiente no dispositivo para que o DVR funcione. Ligue uma unidade externa que tenha, pelo menos, %1$s GB e siga os passos para a formatar como armazenamento do dispositivo." + "Sem armazenamento suficiente" + "Este programa não será gravado porque não existe armazenamento suficiente. Experimente eliminar algumas gravações existentes." "Armazenamento em falta" - "Algum do armazenamento utilizado pelo DVR está em falta. Ligue a unidade externa que utilizou anteriormente para reativar o DVR. Em alternativa, pode optar por esquecer o armazenamento se este já não estiver disponível." - "Pretende esquecer o armazenamento?" - "Todos os seus conteúdos e agendamentos gravados serão perdidos." "Pretende parar a gravação?" "O conteúdo gravado será guardado." - - + "A gravação de %1$s será interrompida devido a conflitos com este programa. O conteúdo gravado será guardado." "Gravação agendada, mas com conflitos" "A gravação foi iniciada, mas tem conflitos" "O programa %1$s será gravado." @@ -306,14 +292,27 @@ "O mesmo programa já foi agendado para ser gravado às %1$s." "Já gravado" "Este programa já foi gravado. Está disponível na biblioteca do DVR." - - - - - - - - + "Gravação da série agendada" + + Foram agendadas %1$d gravações para %2$s. + Foi agendada %1$d gravação para %2$s. + + + Foram agendadas %1$d gravações para %2$s. %3$d destas não serão gravadas devido a conflitos. + Foi agendada %1$d gravação para %2$s. Esta não será gravada devido a conflitos. + + + Foram agendadas %1$d gravações para %2$s. %3$d episódios desta e de outras séries não serão gravados devido a conflitos. + Foi agendada %1$d gravação para %2$s. %3$d episódios desta e de outras séries não serão gravados devido a conflitos. + + + Foram agendadas %1$d gravações para %2$s. Não será gravado 1 episódio de outra série devido a conflitos. + Foi agendada %1$d gravação para %2$s. Não será gravado 1 episódio de outra série devido a conflitos. + + + Foram agendadas %1$d gravações para %2$s. Não serão gravados %3$d episódios de outras séries devido a conflitos. + Foi agendada %1$d gravação para %2$s. Não serão gravados %3$d episódios de outras séries devido a conflitos. + "Programa gravado não encontrado." "Gravações relacionadas" "(Sem descrição do programa)" @@ -336,6 +335,7 @@ "Pretende parar a gravação da série?" "Os episódios gravados ficam disponíveis na biblioteca do DVR." "Parar" + "Não estão a ser transmitidos episódios em direto." "Não existem episódios disponíveis.\nVão ser gravados assim que estiverem disponíveis." (%1$d minutos) diff --git a/res/values-pt/strings.xml b/res/values-pt/strings.xml index 9c22a58a..bc6f7803 100644 --- a/res/values-pt/strings.xml +++ b/res/values-pt/strings.xml @@ -20,9 +20,8 @@ "mono" "estéreo" "Controles de reprodução" - "Canais recentes" + "Canais" "Opções da TV" - "Opções de PIP" "Controles de reprodução indisponíveis para este canal" "Reproduzir ou pausar" "Avançar" @@ -35,33 +34,15 @@ "Closed captions" "Modo de exibiç." "PIP" - "Ativado" - "Desativado" "Múltip. áudios" "Adquirir mais canais" "Config." - "Origem" - "Trocar" - "Ativadas" - "Desativadas" - "Som" - "Principal" - "Janela de PIP" - "Layout" - "Inferior direito" - "Superior direito" - "Superior esquerdo" - "Inferior esquerdo" - "Lado a lado" - "Tamanho" - "Grande" - "Pequeno" - "Origem da entrada" "TV (antena/a cabo)" "Nenhuma informação sobre o programa" "Nenhuma informação" "Canal bloqueado" - "Idioma desconhecido" + "Idioma desconhecido" + "Closed captions %1$d" "Closed captions" "Desativado" "Person. formatação" @@ -135,6 +116,10 @@ "O PIN estava errado. Tente novamente." "Tente novamente, o PIN não corresponde" + "Digite seu CEP." + "O app Canais ao vivo usará o CEP para fornecer um guia completo da programação para os canais de TV." + "Digite seu CEP" + "CEP inválido" "Config." "Personalizar lista de canais" "Escolher canais para guia de programação" @@ -143,6 +128,7 @@ "Controle dos pais" "Licenças de código aberto" "Licenças de código aberto" + "Enviar feedback" "Versão" "Para assistir a este canal, pressione para a direita e digite o PIN" "Para assistir a este programa, pressione para a direita e digite o PIN" @@ -181,8 +167,6 @@ "Pressione \"SELECIONAR\""" para acessar o menu da TV." "Nenhuma entrada de TV encontrada" "Não foi possível encontrar a entrada de TV" - "PIP não é suportado" - "Não há entrada disponível que possa ser mostrada com PIP" "Tipo de sintonizador não adequado. Inicie o app \"Canais ao vivo\" para abrir o tipo de sintonizador para entrada de TV." "Falha na sintonia" "Nenhum app foi encontrado para executar esta ação." @@ -259,8 +243,6 @@ "Salvar" "Gravações únicas têm a maior prioridade" "Cancelar" - "Cancelar" - "Ignorar" "Parar" "Ver programação de gravação" "Este único programa" @@ -270,25 +252,29 @@ "Gravar este, e não o outro" "Cancelar esta gravação" "Assistir agora" + "Excluir gravações…" "Pode ser gravado" "Gravação programada" "Conflito de gravação" "Gravação" "Falha na gravação" "Lendo programas para criar programações de gravação" - "Lendo programas" - - + "Lendo programas" + "Ver gravações recentes" + "A gravação de %1$s está incompleta." + "As gravações de %1$s e %2$s estão incompletas." + "As gravações de %1$s, %2$s e %3$s estão incompletas." + "A gravação de %1$s não foi concluída devido à falta de espaço de armazenamento." + "As gravações de %1$s e %2$s não foram concluídas devido à falta de espaço de armazenamento." + "As gravações de %1$s, %2$s e %3$s não foram concluídas devido à falta de espaço de armazenamento." "O DVR precisa de mais armazenamento" "Você poderá gravar programas com DVR. No entanto, não há espaço de armazenamento suficiente no seu dispositivo no momento para que o DVR funcione. Conecte um drive externo que tenha %1$s GB ou mais e siga as etapas para formatá-lo como um armazenamento do dispositivo." + "Armazenamento insuficiente" + "Este programa não será gravado porque não há espaço de armazenamento suficiente. Tente excluir algumas gravações já existentes." "Armazenamento ausente" - "Alguns dos armazenamentos usados por DVR estão ausentes. Conecte o drive externo usado antes de reativar o DVR. Também é possível optar por esquecer o armazenamento se ele não estiver mais disponível." - "Esquecer armazenamento?" - "Todo o conteúdo gravado e programações serão perdidos." "Interromper gravação?" "O conteúdo gravado será salvo." - - + "A gravação de %1$s será interrompida porque ela tem um conflito com esse programa. O conteúdo gravado será salvo." "Gravação programada, mas há conflitos" "A gravação começou, mas há conflitos" "O programa %1$s será gravado." @@ -306,14 +292,27 @@ "A gravação do mesmo programa já foi programada para %1$s." "Já gravado" "Este programa já foi gravado. Ele está disponível na biblioteca de DVR." - - - - - - - - + "Gravação de série programada" + + Foi programada %1$d gravação de %2$s. + Foram programadas %1$d gravações de %2$s. + + + Foi programada %1$d gravação de %2$s. Ao todo, %3$d delas não serão gravadas devido a conflitos. + Foram programadas %1$d gravações de %2$s. Ao todo, %3$d delas não serão gravadas devido a conflitos. + + + Foi programada %1$d gravação de %2$s. Ao todo, %3$d episódios dessa e de outras séries não serão gravados devido a conflitos. + Foram programadas %1$d gravações de %2$s. Ao todo, %3$d episódios dessa e de outras séries não serão gravados devido a conflitos. + + + Foi programada %1$d gravação de %2$s. Ao todo, 1 episódio de outras séries não será gravado devido a conflitos. + Foram programadas %1$d gravações de %2$s. Ao todo, 1 episódio de outras séries não será gravado devido a conflitos. + + + Foi programada %1$d gravação de %2$s. Ao todo, %3$d episódios de outras séries não serão gravados devido a conflitos. + Foram programadas %1$d gravações de %2$s. Ao todo, %3$d episódios de outras séries não serão gravados devido a conflitos. + "Programa gravado não encontrado." "Gravações relacionadas" "Nenhuma descrição do programa" @@ -336,6 +335,7 @@ "Parar gravação da série?" "Os episódios gravados permanecerão disponíveis na biblioteca de DVR." "Parar" + "Nenhum episódio está sendo transmitido no momento." "Nenhum episódio disponível.\nOs episódios serão gravados quando estiverem disponíveis." (%1$d minuto) diff --git a/res/values-ro/strings.xml b/res/values-ro/strings.xml index adc3b359..ec979f1a 100644 --- a/res/values-ro/strings.xml +++ b/res/values-ro/strings.xml @@ -20,9 +20,8 @@ "mono" "stereo" "Comenzi de redare" - "Canale recente" + "Canale" "Opțiuni TV" - "Opțiuni PIP" "Comenzile de redare nu sunt disponibile pentru acest canal" "Redați sau întrerupeți redarea" "Derulați rapid înainte" @@ -35,33 +34,15 @@ "Subtitrări" "Mod de afișare" "PIP" - "Activat" - "Dezactivat" "Multi-audio" "Obțineți mai multe canale" "Setări" - "Sursă" - "Schimbați" - "Activat" - "Dezactivat" - "Sunet" - "Principală" - "Fereastră PIP" - "Aspect" - "Dreapta jos" - "Dreapta sus" - "Stânga sus" - "Stânga jos" - "Alăturat" - "Dimensiuni" - "Mare" - "Mic" - "Sursă de intrare" "TV (antenă/cablu)" "Nu există informații despre program" "Nicio informație" "Canal blocat" - "Limbă necunoscută" + "Limbă necunoscută" + "Subtitrări %1$d" "Subtitrări" "Dezactivat" "Personaliz. format." @@ -137,6 +118,10 @@ "Codul PIN a fost greșit. Încercați din nou." "Încercați din nou. Codul PIN nu se potrivește." + "Introduceți codul poștal" + "Aplicația Canale live va folosi codul poștal pentru a vă oferi un ghid de programe complet pentru canalele TV." + "Introduceți codul poștal" + "Cod poștal nevalid" "Setări" "Personalizați lista de canale" "Alegeți canale pentru ghidul de programe" @@ -145,6 +130,7 @@ "Control parental" "Licențe open source" "Licențe open source" + "Trimiteți feedback" "Versiune" "Pentru a viziona acest canal, apăsați la dreapta și introduceți codul PIN" "Pentru a viziona acest program, apăsați la dreapta și introduceți codul PIN" @@ -185,8 +171,6 @@ "Apăsați pe SELECTAȚI"" pentru a accesa meniul TV." "Nu s-a găsit nicio intrare TV" "Nu se poate găsi intrarea TV" - "Funcția PIP nu este acceptată" - "Nu există intrări disponibile pentru afișarea cu PIP" "Tipul tuner nu este corespunzător. Lansați aplicația Canale live pentru intrarea TV tip tuner." "Eroare la optimizare" "Nu s-a găsit o aplicație care să îndeplinească această acțiune." @@ -269,8 +253,6 @@ "Salvați" "Înregistrările unice au cea mai mare prioritate" "Anulați" - "Anulați" - "Eliminați" "Opriți" "Vedeți programul de înregistrare" "Numai acest program" @@ -280,25 +262,29 @@ "Înregistrați acest program" "Anulați această înregistrare" "Vedeți acum" + "Ștergeți înregistrări…" "Se poate înregistra" "Înregistrare programată" "Conflict privind înregistrarea" "Se înregistrează" "Nu s-a înregistrat" "Se citesc programele pentru crearea programărilor de înregistrare" - "Se citesc programele" - - + "Se citesc programele" + "Vedeți înregistrările recente" + "Înregistrarea pentru %1$s nu este finalizată." + "Înregistrările pentru %1$s și %2$s nu sunt finalizate." + "Înregistrările pentru %1$s, %2$s și %3$s nu sunt finalizate." + "Înregistrarea pentru %1$s nu s-a finalizat, din cauza spațiului de stocare insuficient." + "Înregistrările pentru %1$s și %2$s nu s-au finalizat, din cauza spațiului de stocare insuficient." + "Înregistrările pentru %1$s, %2$s și %3$s nu s-au finalizat, din cauza spațiului de stocare insuficient." "DVR are nevoie de mai mult spațiu de stocare" "Veți putea înregistra programe folosind DVR. Cu toate acestea, momentan, pe dispozitiv nu există suficient spațiu de stocare ca să funcționeze DVR-ul. Conectați o unitate externă de cel puțin %1$s GB și urmați pașii pentru a o formata ca stocare pe dispozitiv." + "Nu există suficient spațiu de stocare" + "Programul nu va fi înregistrat, deoarece nu există suficient spațiu de stocare. Ștergeți unele înregistrări existente." "Stocare lipsă" - "O parte din stocarea folosită de DVR lipsește. Pentru a reactiva DVR, conectați unitatea externă folosită anterior. Dacă stocarea externă nu mai este disponibilă, puteți să o eliminați." - "Eliminați stocarea?" - "Tot conținutul înregistrat și toate programările vor fi șterse." "Opriți înregistrarea?" "Conținutul înregistrat va fi salvat." - - + "Înregistrarea pentru %1$s va fi oprită, deoarece există conflicte cu acest program. Conținutul înregistrat va fi salvat." "Înregistrarea a fost programată, dar există conflicte" "Înregistrarea a început, dar există conflicte" "%1$s va fi înregistrat." @@ -317,14 +303,32 @@ "Același program a fost programat deja pentru înregistrare la %1$s." "Înregistrat deja" "Acest program a fost înregistrat deja. Este disponibil în biblioteca DVR." - - - - - - - - + "Înregistrarea serialului a fost programată" + + %1$d înregistrări au fost programate pentru %2$s. + %1$d de înregistrări au fost programate pentru %2$s. + %1$d înregistrare a fost programată pentru %2$s. + + + %1$d înregistrări au fost programate pentru %2$s. %3$d dintre acestea nu vor fi înregistrate din cauza unor conflicte. + %1$d de înregistrări au fost programate pentru %2$s. %3$d dintre acestea nu vor fi înregistrate din cauza unor conflicte. + %1$d înregistrare a fost programată pentru %2$s. Nu va fi înregistrată din cauza unor conflicte. + + + %1$d înregistrări au fost programate pentru %2$s. %3$d episoade din acest serial și din alte seriale nu vor fi înregistrate din cauza unor conflicte. + %1$d de înregistrări au fost programate pentru %2$s. %3$d episoade din acest serial și din alte seriale nu vor fi înregistrate din cauza unor conflicte. + %1$d înregistrare a fost programată pentru %2$s. %3$d episoade din acest serial și din alte seriale nu vor fi înregistrate din cauza unor conflicte. + + + %1$d înregistrări au fost programate pentru %2$s. Un episod din alte seriale nu va fi înregistrat din cauza unor conflicte. + %1$d de înregistrări au fost programate pentru %2$s. Un episod din alte seriale nu va fi înregistrat din cauza unor conflicte. + %1$d înregistrare a fost programată pentru %2$s. Un episod din alte seriale nu va fi înregistrat din cauza unor conflicte. + + + %1$d înregistrări au fost programate pentru %2$s. %3$d episoade din alte seriale nu vor fi înregistrate din cauza unor conflicte. + %1$d de înregistrări au fost programate pentru %2$s. %3$d episoade din alte seriale nu vor fi înregistrate din cauza unor conflicte. + %1$d înregistrare a fost programată pentru %2$s. %3$d episoade din alte seriale nu vor fi înregistrate din cauza unor conflicte. + "Programul înregistrat nu a fost găsit." "Înregistrări conexe" "(Nicio descriere de program)" @@ -349,6 +353,7 @@ "Opriți înregistrarea seriei?" "Episoadele înregistrate vor rămâne disponibile în biblioteca DVR." "Opriți" + "Acum nu este difuzat niciun episod." "Nu există episoade disponibile.\nAcestea vor fi înregistrate de îndată ce vor fi disponibile." (%1$d minute) diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml index 61bef1f6..0ac177d9 100644 --- a/res/values-ru/strings.xml +++ b/res/values-ru/strings.xml @@ -20,9 +20,8 @@ "Моно" "Стерео" "Управление" - "Недавние каналы" + "Каналы" "Настройки ТВ" - "Настройки PIP" "Команды управления недоступны для этого канала" "Воспроизведение/пауза" "Перемотать вперед" @@ -35,33 +34,15 @@ "Субтитры" "Режим" "Кадр в кадре" - "Вкл." - "Выкл." "Многоканальный" "Ещё каналы" "Настройки" - "Источник" - "Поменять" - "Вкл." - "Выкл." - "Звук" - "Главное окно" - "Кадр в кадре" - "Расположение" - "Справа внизу" - "Справа вверху" - "Слева вверху" - "Слева внизу" - "Рядом" - "Размер" - "Крупный" - "Мелкий" - "Источник" "ТВ (антенна/кабель)" "Нет информации о программах" "Неизвестно" "Заблокированный канал" - "Неизвестный язык" + "Неизвестный язык" + "Субтитры (%1$d)" "Субтитры" "Выкл." "Настройка субтитров" @@ -139,6 +120,10 @@ "Неверный PIN-код. Повторите попытку." "PIN-коды не совпадают. Повторите попытку." + "Введите почтовый индекс" + "Приложение \"Прямой эфир\" использует почтовый индекс, чтобы создавать телегид специально для вас." + "Введите почтовый индекс" + "Недопустимый почтовый индекс." "Настройки" "Настроить список" "Выбрать каналы для телегида" @@ -147,6 +132,7 @@ "Родительский контроль" "Лицензии открытого ПО" "Лицензии открытого ПО" + "Отправить отзыв" "Версия" "Чтобы смотреть этот канал, нажмите стрелку вправо и введите PIN-код." "Чтобы смотреть эту программу, нажмите стрелку вправо и введите PIN-код." @@ -189,8 +175,6 @@ "Нажмите кнопку \"ВЫБРАТЬ\""", чтобы открыть меню телевизора." "ТВ-вход не найден" "Не удается найти ТВ-вход" - "PIP не поддерживается" - "Нет источника для режима PIP" "Для ТВ-входа типа \"тюнер\" используйте приложение \"Прямой эфир\"" "Не удалось выполнить настройку" "Действие не поддерживается ни в одном приложении." @@ -279,8 +263,6 @@ "Сохранить" "Однократная запись имеет самый высокий приоритет" "Отмена" - "Отмена" - "Удалить" "Остановить" "Смотреть расписание записи" "Только эту серию" @@ -290,25 +272,29 @@ "Записать эту программу" "Отменить эту запись" "Смотреть" + "Удалить записи…" "Можно записать" "Таймер записи установлен" "Конфликт таймера записи" "Идет запись" "Ошибка записи видео" "Выполняется чтение программ. Будет создано расписание записи." - "Выполняется чтение программ…" - - + "Выполняется чтение программ…" + "Недавние записи" + "Не удалось завершить запись \"%1$s\"." + "Не удалось завершить записи \"%1$s\" и \"%2$s\"." + "Не удалось завершить записи \"%1$s\", \"%2$s\" и \"%3$s\"." + "Не удалось завершить запись \"%1$s\" из-за нехватки места." + "Не удалось завершить записи \"%1$s\" и \"%2$s\" из-за нехватки места." + "Не удалось завершить записи \"%1$s\", \"%2$s\" и \"%3$s\" из-за нехватки места." "Недостаточно места на устройстве" "Вы сможете записывать программы на DVR, однако в настоящее время на вашем устройстве недостаточно места. Подключите внешний накопитель объемом не менее %1$s ГБ и отформатируйте его как память устройства." + "Недостаточно места" + "Недостаточно места для сохранения данных. Попробуйте удалить несколько ненужных записей." "Хранилище отсутствует" - "Хранилище не найдено. Подсоедините внешний диск, прежде чем снова включить видеомагнитофон, либо удалите хранилище, если оно недоступно." - "Удалить хранилище?" - "Все созданные и запланированные записи будут стерты." "Остановить запись?" "Записанный контент будет сохранен." - - + "Запись сериала \"%1$s\" будет остановлена из-за конфликта в расписании. Записанный контент сохранится." "Возник конфликт в расписании записи" "Возник конфликт в расписании записи" "Программа \"%1$s\" будет записана." @@ -328,14 +314,37 @@ "Запись этой программы уже запланирована на %1$s." "Программа уже записана" "Эта программа сохранена в библиотеке видеорекордера." - - - - - - - - + "Запись запланирована" + + Запланирована %1$d запись сериала \"%2$s\". + Запланировано %1$d записи сериала \"%2$s\". + Запланировано %1$d записей сериала \"%2$s\". + Запланировано %1$d записи сериала \"%2$s\". + + + Запланирована %1$d запись сериала \"%2$s\". Несколько серий (%3$d) этого сериала не будут записаны из-за конфликта в расписании. + Запланировано %1$d записи сериала \"%2$s\". Несколько серий (%3$d) этого сериала не будут записаны из-за конфликта в расписании. + Запланировано %1$d записей сериала \"%2$s\". Несколько серий (%3$d) этого сериала не будут записаны из-за конфликта в расписании. + Запланировано %1$d записи сериала \"%2$s\". Несколько серий (%3$d) этого сериала не будут записаны из-за конфликта в расписании. + + + Запланирована %1$d запись сериала \"%2$s\". Несколько серий (%3$d) этого и других сериалов не будут записаны из-за конфликта в расписании. + Запланировано %1$d записи сериала \"%2$s\". Несколько серий (%3$d) этого и других сериалов не будут записаны из-за конфликта в расписании. + Запланировано %1$d записей сериала \"%2$s\". Несколько серий (%3$d) этого и других сериалов не будут записаны из-за конфликта в расписании. + Запланировано %1$d записи сериала \"%2$s\". Несколько серий (%3$d) этого и других сериалов не будут записаны из-за конфликта в расписании. + + + Запланирована %1$d запись сериала \"%2$s\". Одна серия другого сериала не будет записана из-за конфликта в расписании. + Запланировано %1$d записи сериала \"%2$s\". Одна серия другого сериала не будет записана из-за конфликта в расписании. + Запланировано %1$d записей сериала \"%2$s\". Одна серия другого сериала не будет записана из-за конфликта в расписании. + Запланировано %1$d записи сериала \"%2$s\". Одна серия другого сериала не будет записана из-за конфликта в расписании. + + + Запланирована %1$d запись сериала \"%2$s\". Несколько серий (%3$d) других сериалов не будут записаны из-за конфликта в расписании. + Запланировано %1$d записи сериала \"%2$s\". Несколько серий (%3$d) других сериалов не будут записаны из-за конфликта в расписании. + Запланировано %1$d записей сериала \"%2$s\". Несколько серий (%3$d) других сериалов не будут записаны из-за конфликта в расписании. + Запланировано %1$d записи сериала \"%2$s\". Несколько серий (%3$d) других сериалов не будут записаны из-за конфликта в расписании. + "Записанная программа не найдена." "Похожие записи" "(без описания)" @@ -362,6 +371,7 @@ "Остановить запись?" "Записанные серии будут сохранены в библиотеке видеорекордера." "Остановить" + "В эфире нет ни одной серии." "Серий пока нет.\nОни будут записаны, как только выйдут в эфир." (%1$d минута) diff --git a/res/values-si-rLK/strings.xml b/res/values-si-rLK/strings.xml index eab8c336..c21997a9 100644 --- a/res/values-si-rLK/strings.xml +++ b/res/values-si-rLK/strings.xml @@ -20,9 +20,8 @@ "මොනෝ" "ස්ටීරියෝ" "ධාවක පාලන" - "මෑත නාලිකා" + "නාලිකා" "රූපවාහිනී විකල්ප" - "PIP විකල්ප" "මෙම නාලිකාව සඳහා Play පාලන ලද නොහැකිය" "ධාවනය හෝ විරාමය කරන්න" "වේගයෙන් ඉදිරියට" @@ -35,33 +34,15 @@ "වසන ලද ශිර්ෂ" "දර්ශන ආකාරය" "PIP" - "සක්‍රියයි" - "අක්‍රිය කරන්න" "බහු-ශ්‍රව්‍ය" "තවත් නාලිකා ගන්න" "සැකසීම්" - "මුල්‍ය" - "මාරු කරන්න" - "සක්‍රියයි" - "අක්‍රිය කරන්න" - "ශබ්දය" - "මූලික" - "PIP කවුළුව" - "පිරිසැලසුම" - "පහළ දකුණ" - "ඉහළ දකුණ" - "ඉහළ වම" - "පහළ වම" - "පැතෙන් පැත්තට" - "ප්‍රමාණය" - "ලොකු" - "කුඩා" - "මූලය අදානය කරන්න" "TV (ඇන්ටනාව/කේබලය)" "වැඩසටහන් තොරතුරු නැත" "තොරතුරු නැත." "නාලිකාව අවහිර කරන ලදි" - "නොදන්නා භාෂාව" + "නොදන්නා භාෂාව" + "වැසූ සිරස්තල %1$d" "වැසූ සිරස්තල" "අක්‍රිය කරන්න" "ආකෘතිකරණය අභිරුචි කරන්න" @@ -135,6 +116,10 @@ "එම PIN එක වැරදිය. නැවත උත්සාහ කරන්න." "PIN එක ගැලපී නැත" + "ඔබේ ZIP කේතය ඇතුළු කරන්න." + "සජීව නාලිකා යෙදුම TV නාලිකා සඳහා සම්පූර්ණ වැඩසටහන් මාර්ගෝපදේශයක් සැපයීමට ZIP කේතය භාවිත කරනු ඇත." + "ඔබේ ZIP කේතය ඇතුළු කරන්න" + "වලංගු නොවන ZIP කේතයකි" "සැකසීම්" "නාලිකා ලැයිස්තුව අභිමත කරන්න" "ඔබගේ වැඩසටහන් මාර්ගෝපදේශය සඳහා නාලිකා තෝරන්න" @@ -143,6 +128,7 @@ "මාපිය පාලන" "විවෘත මූලාශ්‍ර බලපත්‍ර" "විවෘත මූලාශ්‍ර වරපත්" + "ප්‍රතිපෝෂණය යවන්න" "අනුවාදය" "මෙම නාලිකාව නැරඹිමට දකුණ ඔබා PIN එක ඇතුළු කරන්න" "මෙම වැඩසටහන නැරඹිමට දකුණ ඔබා PIN එක ඇතුළු කරන්න" @@ -181,8 +167,6 @@ "TV මෙනුවට පිවිසීමට ""SELECT ඔබන්න""." "TV ආදානය සොයාගැනීමට නොහැකි විය" "TV ආදානය සොයාගත නොහැක" - "PIP සහාය දක්වන්නේ නැත" - "PIP සමඟ පෙන්වූ විට අදානයක් එහි නොතිබේ" "සුසරක වර්ගය ගැලපෙන්නේ නැත; කරුණාකර සුසර කරන වර්ගයේ TV අදානය සඳහා සජීවී නාලිකා යෙදුම දමන්න." "සුසර කිරීම අසාර්ථක වුණි" "මෙම ක්‍රියාව හැසිරවීමට යෙදුමක් සොයාගත්තේ නැත" @@ -259,8 +243,6 @@ "සුරකින්න" "එක්-වරක පටිගත කිරීම්වලට වැඩිම ප්‍රමුඛතාව ඇත" "අවලංගු කර." - "අවලංගු කරන්න" - "අමතක කරන්න" "නතර කරන්න" "පටිගත කිරීමේ කාල සටහන බලන්න" "මෙම තනි වැඩසටහන" @@ -270,25 +252,29 @@ "ඒ වෙනුවට මෙය පටිගත කරන්න" "මෙම පටිගත කිරීම අවලංගු කරන්න" "දැන් නරඹන්න" + "පටිගත කිරීම් මකන්න..." "පටිගත කළ හැකි" "පටිගත කිරීම කාල සටහන්ගතයි" "පටිගත කිරීමේ ගැටුම" "පටිගත කරමින්" "පටිගත කිරීම අසාර්ථක විය" "පටිගත කිරීමේ කාලසටහන් සෑදීමට වැඩසටහන් කියවමින්" - "කියවීමේ වැඩසටහන්" - - + "කියවීමේ වැඩසටහන්" + "මෑත පටිගත කිරීම් බලන්න" + "%1$s හි පටිගත කිරීම අසම්පූර්ණයි." + "%1$s සහ %2$s හි පටිගත කිරීම් අසම්පූර්ණයි." + "%1$s, %2$s සහ %3$s හි පටිගත කිරීම් අසම්පූර්ණයි." + "ප්‍රමාණවත් නොවන ගබඩාව නිසා %1$s හි පටිගත කිරීම සම්පූර්ණ නොකරන ලදී." + "ප්‍රමාණවත් නොවන ගබඩාව නිසා %1$s සහ %2$s හි පටිගත කිරීම් සම්පූර්ණ නොකරන ලදී." + "ප්‍රමාණවත් නොවන ගබඩාව නිසා %1$s, %2$s සහ %3$s හි පටිගත කිරීම් සම්පූර්ණ නොකරන ලදී." "DVR සඳහා වැඩිපුර ගබඩාව අවශ්‍යයි" "ඔබට DVR සමගින් වැඩසටහන් පටිගත කිරීමට හැකි වනු ඇත. කෙසේ වෙතත් දැන් DVR ක්‍රියා කිරීම සඳහා ඔබේ උපාංගයේ ප්‍රමාණවත් තරම් ගබඩාව නැත. කරුණාකර %1$sGB හෝ ඊට වඩා විශාල බාහිර ධාවකයක් සම්බන්ධ කර එය උපාංග ගබඩාව ලෙස ෆෝමැට් කිරීමට පහත පියවර අනුගමනය කරන්න." + "ප්‍රමාණවත් තරම් ගබඩා ඉඩ නැත" + "ප්‍රමාණවත් තරම් ගබඩා ඉඩ නොමැති නිසා මෙම වැඩසටහන පටිගත කළ නොහැකි වනු ඇත. පවතින පටිගත කිරීම් සමහරක් මැකීම උත්සාහ කරන්න." "අස්ථානගත ගබඩාව" - "DVR මගින් භාවිත කළ ගබඩා සමහරක් අස්ථානගතය. කරුණාකර DVR නැවත-සබල කිරීමට ඔබ පෙරදී භාවිත කළ බාහිර drive සම්බන්ධ කරන්න. විකල්පව, එය තවදුරටත් ලබා ගත නොහැකි නම් ඔබට ගබඩාව අමතක කිරීමට තේරිය හැකිය." - "ගබඩාව අමතකද?" - "ඔබේ පටිගත කළ සියලු අන්තර්ගත සහ කාල සටහන් අහිමි වනු ඇත." "පටිගත කිරීම නවත්වන්නද?" "පටිගත කළ අන්තර්ගතය සුරැකෙනු ඇත." - - + "මෙම වැඩසටහන සමගින් වන ගැටුම් නිසා %1$s හි පටිගත කිරීම නවත්වන ලදී. පටිගත කරන ලද අන්තර්ගතය සුරකිනු ඇත." "පටිගත කිරීම කාල සටහන්ගත කර ඇති නමුත් ගැටුම් ඇත" "පටිගත කිරීම ආරම්භ කර ඇති නමුත් ගැටුම් ඇත" "%1$s පටිගත කරනු ඇත." @@ -306,14 +292,27 @@ "එම වැඩසටහනම %1$sට පටිගත කිරීමට කාල සටහන්ගත කර ඇත." "දැනටමත් පටිගත කර ඇත" "මෙම වැඩසටහන දැනටමත් පටිගත කර ඇත. එය DVR පුස්තකාලය තුළදී ලබා ගත හැකිය." - - - - - - - - + "මාලා පටිගත කිරීම කාලසටහන්ගත කරන ලදී" + + පටිගත කිරීම් %1$dක් %2$s සඳහා කාල සටහන්ගත කර ඇත. + පටිගත කිරීම් %1$dක් %2$s සඳහා කාල සටහන්ගත කර ඇත. + + + පටිගත කිරීම් %1$dක් %2$s සඳහා කාල සටහන්ගත කර ඇත. ඒවායින් %3$dක් ගැටුම් නිසා පටිගත නොකරනු ඇත. + පටිගත කිරීම් %1$dක් %2$s සඳහා කාල සටහන්ගත කර ඇත. ඒවායින් %3$dක් ගැටුම් නිසා පටිගත නොකරනු ඇත. + + + පටිගත කිරීම් %1$dක් %2$s සඳහා කාල සටහන්ගත කර ඇත. මෙම මාලාවෙහි සහ වෙනත් මාලාවල කථාංග %3$dක් ගැටුම් නිසා පටිගත නොකරනු ඇත. + පටිගත කිරීම් %1$dක් %2$s සඳහා කාල සටහන්ගත කර ඇත. මෙම මාලාවෙහි සහ වෙනත් මාලාවල කථාංග %3$dක් ගැටුම් නිසා පටිගත නොකරනු ඇත. + + + පටිගත කිරීම් %1$dක් %2$s සඳහා කාලසටහන්ගත කර ඇත. වෙනත් මාලාවල කථාංග 1ක් ගැටුම් නිසා පටිගත නොකරනු ඇත. + පටිගත කිරීම් %1$dක් %2$s සඳහා කාලසටහන්ගත කර ඇත. වෙනත් මාලාවල කථාංග 1ක් ගැටුම් නිසා පටිගත නොකරනු ඇත. + + + පටිගත කිරීම් %1$dක් %2$s සඳහා කාල සටහන්ගත කර ඇත. වෙනත් මාලාවේ කථාංග %3$dක් ගැටුම් නිසා පටිගත නොකරනු ඇත. + පටිගත කිරීම් %1$dක් %2$s සඳහා කාල සටහන්ගත කර ඇත. වෙනත් මාලාවේ කථාංග %3$dක් ගැටුම් නිසා පටිගත නොකරනු ඇත. + "පටිගත කළ වැඩසටහන හමු නොවීය." "අදාළ පටිගත කිරීම්" "(වැඩසටහන් විස්තරය නැත)" @@ -336,6 +335,7 @@ "මාලා පටිගත කිරීම නවත්වන්නද?" "පටිගත කළ කථාංග DVR පුස්තකාලය තුළ ලබා ගත හැකිව පවතිනු ඇත." "නවත්වන්න" + "දැන් ගුවනේ කථාංග නැත." "ලබා ගත හැකි කථාංග නැත.\nඒවා ලබා ගත හැකි වූ විට පටිගත කරනු ඇත." (මිනිත්තු %1$d) diff --git a/res/values-sk/strings.xml b/res/values-sk/strings.xml index 78d94c7f..ce3cbdb9 100644 --- a/res/values-sk/strings.xml +++ b/res/values-sk/strings.xml @@ -20,9 +20,8 @@ "mono" "stereo" "Ovládanie prehrávania" - "Nedávne kanály" + "Kanály" "Možnosti TV" - "Možnosti PIP" "Pre tento kanál nie sú k dispozícii ovládacie prvky prehrávania" "Prehrať alebo pozastaviť" "Pretočiť dopredu" @@ -35,33 +34,15 @@ "Skryté titulky" "Režim zobrazenia" "PIP" - "Zapnuté" - "Vypnuté" "Multi-audio" "Ďalšie kanály" "Nastavenia" - "Zdroj" - "Zameniť" - "Zapnuté" - "Vypnuté" - "Zvuk" - "Hlavné" - "Okno PIP" - "Rozloženie" - "Vpravo dole" - "Vpravo hore" - "Vľavo hore" - "Vľavo dole" - "Vedľa seba" - "Rozmery" - "Veľké" - "Malé" - "Zdroj vstupu" "TV (anténa/kábel)" "Žiadne informácie o programe" "Žiadne informácie" "Zablokovaný kanál" - "Neznámy jazyk" + "Neznámy jazyk" + "Skryté titulky: %1$d" "Skryté titulky" "Vypnuté" "Prispôsobiť formát" @@ -139,6 +120,10 @@ "Kód PIN bol zadaný chybne. Skúste to znova." "Kód PIN nesúhlasí. Skúste to znova." + "Zadajte svoje PSČ." + "Aplikácia Televízia online vám na základe PSČ poskytne kompletný program pre televízne kanály." + "Zadajte svoje PSČ" + "Neplatné PSČ" "Nastavenia" "Prispôsobiť zoznam kanálov" "Vybrať kanály pre televízny program" @@ -147,6 +132,7 @@ "Rodičovská kontrola" "Licencie open source" "Licencie open source" + "Odoslať spätnú väzbu" "Verzia" "Ak chcete sledovať tento kanál, stlačte šípku vpravo a zadajte kód PIN" "Ak chcete sledovať tento program, stlačte šípku doprava a zadajte kód PIN" @@ -189,8 +175,6 @@ "Stlačením tlačidla VYBRAŤ"" prejdete do TV ponuky." "Nenašiel sa žiadny TV vstup" "TV vstup sa nenašiel" - "Funkcia Obraz v obraze (PIP) nie je podporovaná" - "Neexistuje vstup, ktorý by mohla funkcia PIP zobraziť" "Typ tunera nie je vhodný. Pre TV vstup typu tunera spustite aplikáciu Aktívne kanály." "Ladenie zlyhalo" "Aplikácia potrebná na spracovanie tejto akcie sa nenašla." @@ -279,8 +263,6 @@ "Uložiť" "Jednorazové záznamenávania majú najvyššiu prioritu" "Zrušiť" - "Zrušiť" - "Odstrániť" "Zastaviť" "Zobraziť rozvrh nahrávania" "Iba tento program" @@ -290,25 +272,29 @@ "Zaznamenať radšej tento program" "Zrušiť tento záznam" "Pozrieť" + "Odstrániť nahratý obsah..." "Je možné nahrať" "Nahrávanie je naplánované" "Konflikt nahrávania" "Nahráva sa" "Nahrávanie zlyhalo" "Čítajú sa programy s cieľom vytvoriť plány zaznamenávania" - "Načítavajú sa programy" - - + "Načítavajú sa programy" + "Zobraziť nedávne nahrávky" + "Záznam programu %1$s nie je úplný." + "Záznamy programov %1$s%2$s nie sú úplné." + "Záznamy programov %1$s, %2$s%3$s nie sú úplné." + "Zaznamenávanie programu %1$s nebolo dokončené z dôvodu nedostatku miesta v úložisku." + "Zaznamenávanie programov %1$s%2$s nebolo dokončené z dôvodu nedostatku miesta v úložisku." + "Zaznamenávanie programov %1$s, %2$s%3$s nebolo dokončené z dôvodu nedostatku miesta v úložisku." "DVR vyžaduje viac miesta v úložisku" "Budete môcť zaznamenávať programy pomocou zariadenia DVR. Teraz však v úložisku vášho zariadenia nie je dostatok miesta na fungovanie zariadenia DVR. Pripojte externý disk s minimálnou kapacitou %1$s GB a podľa uvedených krokov ho naformátujte ako úložisko zariadenia." + "Nedostatok úložiska" + "Tento program nebude nahratý z dôvodu nedostatku úložiska. Skúste odstrániť časť nahratého obsahu." "Chýba úložisko" - "Určitá časť úložiska využitého zariadením DVR chýba. Pred opätovným povolením zariadenia DVR pripojte externý disk, ktorý ste predtým používali. Prípadne môžete úložisko odstrániť, ak už nie je ďalej k dispozícii." - "Odstrániť úložisko?" - "Všetok váš zaznamenaný obsah a plány budú stratené." "Zastaviť nahrávanie?" "Nahraný obsah sa uloží." - - + "Zaznamenávanie relácie %1$s bude zastavené, pretože koliduje s týmto programom. Zaznamenaný obsah sa uloží." "Nahrávanie je naplánované, ale obsahuje konflikty" "Záznam sa spustil, ale obsahuje konflikty" "Zaznamená sa program %1$s." @@ -328,14 +314,37 @@ "Zaznamenanie rovnakého programu už bolo naplánované na %1$s." "Už je zaznamenané" "Tento program je už zaznamenaný. Nájdete ho v knižnici zariadenia DVR." - - - - - - - - + "Zaznamenávanie relácie bolo naplánované" + + Pre seriál %2$s boli naplánované %1$d záznamy. + Pre seriál %2$s bolo naplánovaného %1$d záznamu. + Pre seriál %2$s bolo naplánovaných %1$d záznamov. + Pre seriál %2$s bol naplánovaný %1$d záznam. + + + Pre seriál %2$s boli naplánované %1$d záznamy. Z dôvodu konfliktov sa nezaznamenajú niektoré epizódy (počet: %3$d). + Pre seriál %2$s bolo naplánovaného %1$d záznamu. Z dôvodu konfliktov sa nezaznamenajú niektoré epizódy (počet: %3$d) + Pre seriál %2$s bolo naplánovaných %1$d záznamov. Z dôvodu konfliktov sa nezaznamenajú niektoré epizódy (počet: %3$d) + Pre seriál %2$s bol naplánovaný %1$d záznam. Z dôvodu konfliktov sa nezaznamená. + + + Pre seriál %2$s boli naplánované %1$d záznamy. Z dôvodu konfliktov sa nezaznamenajú niektoré epizódy z tohto seriálu a ďalších seriálov (počet: %3$d). + Pre seriál %2$s bolo naplánovaného %1$d záznamu. Z dôvodu konfliktov sa nezaznamenajú niektoré epizódy z tohto seriálu a ďalších seriálov (počet: %3$d). + Pre seriál %2$s bolo naplánovaných %1$d záznamov. Z dôvodu konfliktov sa nezaznamenajú niektoré epizódy z tohto seriálu a ďalších seriálov (počet: %3$d). + Pre seriál %2$s bol naplánovaný %1$d záznam. Z dôvodu konfliktov sa nezaznamenajú niektoré epizódy z tohto seriálu a ďalších seriálov (počet: %3$d). + + + Pre seriál %2$s boli naplánované %1$d záznamy.Z dôvodu konfliktov nebude zaznamenaná 1 epizóda inej relácie. + Pre seriál %2$s bolo naplánovaného %1$d záznamu.Z dôvodu konfliktov nebude zaznamenaná 1 epizóda inej relácie. + Pre seriál %2$s bolo naplánovaných %1$d záznamov.Z dôvodu konfliktov nebude zaznamenaná 1 epizóda inej relácie. + Pre seriál %2$s bol naplánovaný %1$d záznam.Z dôvodu konfliktov nebude zaznamenaná 1 epizóda inej relácie. + + + Pre seriál %2$s boli naplánované %1$d záznamy. Z dôvodu konfliktov sa nezaznamenajú epizódy z iného seriálu (počet: %3$d). + Pre seriál %2$s bolo naplánovaného %1$d záznamu. Z dôvodu konfliktov sa nezaznamenajú epizódy z iného seriálu (počet: %3$d). + Pre seriál %2$s bolo naplánovaných %1$d záznamov. Z dôvodu konfliktov sa nezaznamenajú epizódy z iného seriálu (počet: %3$d). + Pre seriál %2$s bol naplánovaný %1$d záznam. Z dôvodu konfliktov sa nezaznamenajú epizódy z iného seriálu (počet: %3$d). + "Zaznamenaný program sa nenašiel." "Súvisiace nahrávky" "(Žiadny popis programu)" @@ -362,6 +371,7 @@ "Zastaviť nahrávanie série?" "Nahrané epizódy zostanú k dispozícii v knižnici DVR." "Zastaviť" + "Momentálne nie sú vysielané žiadne epizódy." "Nie sú k dispozícii žiadne epizódy.\nZaznamenajú sa, keď budú dostupné." (%1$d minúty) diff --git a/res/values-sl/strings.xml b/res/values-sl/strings.xml index 6e79cacc..d36b0858 100644 --- a/res/values-sl/strings.xml +++ b/res/values-sl/strings.xml @@ -20,9 +20,8 @@ "mono" "stereo" "Kontrolniki predvajanja" - "Nedavni kanali" + "Kanali" "Možnosti za TV" - "Možnosti za PIP" "Kontrolniki za predvajanje niso na voljo za ta kanal" "Predvajanje ali zaustavitev" "Previjanje naprej" @@ -35,33 +34,15 @@ "Podnapisi" "Način prikaza" "PIP" - "Vklopljeno" - "Izklopljeno" "Multizvok" "Več kanalov" "Nastavitve" - "Vir" - "Zamenjaj" - "Vklopljeno" - "Izklopljeno" - "Zvok" - "Glavno" - "Okno PIP-a" - "Postavitev" - "Spodaj desno" - "Zgoraj desno" - "Zgoraj levo" - "Spodaj levo" - "Vzporedno" - "Velikost" - "Veliko" - "Majhno" - "Vir vhoda" "TV (antena/kabelska)" "Ni informacij o programu" "Ni informacij" "Blokiran kanal" - "Neznan jezik" + "Neznan jezik" + "Podnapisi %1$d" "Podnapisi" "Izklopljeno" "Oblikovanje po meri" @@ -139,6 +120,10 @@ "Koda PIN je bila napačna. Poskusite znova." "Poskusite znova. Koda PIN se ne ujema." + "Vnos poštne številke." + "Aplikacija Televizija v živo uporablja poštno številko za posredovanje popolnega programskega vodnika za televizijske kanale." + "Vnesite poštno številko" + "Neveljavna poštna številka" "Nastavitve" "Prilagajanje seznama kanalov" "Izbira kanalov za programski vodnik" @@ -147,6 +132,7 @@ "Starševski nadzor" "Odprtokodne licence" "Odprtokodne licence" + "Pošljite povratne informacije" "Različica" "Če želite gledati ta kanal, pritisnite v desno in vnesite kodo PIN" "Če želite gledati ta program, pritisnite v desno in vnesite kodo PIN" @@ -189,8 +175,6 @@ "Pritisnite »IZBIRA«"", če želite dostopati do menija TV-ja." "Ni TV-vhodov" "Ni mogoče najti TV-vhoda" - "Slika v sliki ni podprta" - "Ni vhoda, ki bi omogočal prikaz s sliko v sliki (PIP)" "Vrsta sprejemnika ni ustrezna. Zaženite aplikacijo Kanali v živo za uporabo TV-vhoda, ki deluje kot sprejemnik." "Nastavljanje kanalov ni uspelo" "Za to dejanje ni bilo mogoče najti nobene aplikacije." @@ -279,8 +263,6 @@ "Shrani" "Enkratna snemanja imajo najvišjo prednost" "Prekliči" - "Prekliči" - "Pozabi" "Ustavi" "Ogled razporeda snemanja" "Samo to oddajo" @@ -290,25 +272,29 @@ "Snemanje tega namesto drugega" "Preklic tega snemanja" "Ogled" + "Izbris posnetkov …" "Omogoča snemanje" "Čas snemanja nastavljen" "Posnetek v sporu" "Snemanje" "Snemanje ni uspelo" "Branje oddaj za ustvarjanje razporedov snemanja" - "Branje oddaj" - - + "Branje oddaj" + "Ogled nedavnih posnetkov" + "Posnetek vsebine %1$s je nepopoln." + "Posnetka vsebin %1$s in %2$s sta nepopolna." + "Posnetki vsebin %1$s, %2$s in %3$s so nepopolni." + "Snemanje vsebine %1$s se ni dokončalo zaradi pomanjkanja prostora za shranjevanje." + "Snemanje vsebin %1$s in %2$s se ni dokončalo zaradi pomanjkanja prostora za shranjevanje." + "Snemanje vsebin %1$s, %2$s in %3$s se ni dokončalo zaradi pomanjkanja prostora za shranjevanje." "Digitalni videorekorder potrebuje več shrambe" "Z digitalnim videorekorderjem boste lahko snemali oddaje, vendar v napravi ni dovolj shrambe, potrebne za njegovo delovanje. Priključite zunanji pogon velikosti %1$s GB ali več in upoštevajte navodila, da ga formatirate kot shrambo naprave." + "Ni dovolj prostora za shranjevanje" + "Ta program ne bo posnet, ker ni dovolj prostora za shranjevanje. Poskusite izbrisati nekatere obstoječe posnetke." "Manjkajoča shramba" - "Del shrambe, ki jo uporablja digitalni videorekorder, manjka. Povežite zunanji pogon, ki ste ga že uporabljali, če želite znova omogočiti digitalni videorekorder. Če shramba ni več na voljo, jo lahko tudi pozabite." - "Želite pozabiti shrambo?" - "Posneta vsebina in razporedi bodo izgubljeni." "Želite ustaviti snemanje?" "Posneta vsebina bo shranjena." - - + "Snemanje vsebine %1$s bo ustavljeno, ker je v sporu s tem programom. Posneta vsebina bo shranjena." "Snemanje je nastavljeno, ampak obstajajo spori" "Snemanje se je začelo, vendar so spori" "Oddaja %1$s bo posneta." @@ -328,14 +314,37 @@ "Snemanje iste oddaje je že nastavljeno za ob %1$s." "Že posneto" "Ta oddaja je že posneta. Na voljo je v knjižnici digitalnega videorekorderja." - - - - - - - - + "Snemanje serije je načrtovano" + + %1$d posnetek je načrtovan za serijo %2$s. + %1$d posnetka sta načrtovana za serijo %2$s. + %1$d posnetki so načrtovani za serijo %2$s. + %1$d posnetkov je načrtovanih za serijo %2$s. + + + %1$d posnetek je načrtovan za serijo %2$s. Zaradi sporov jih ne bo posnetih toliko: %3$d. + %1$d posnetka sta načrtovana za serijo %2$s. Zaradi sporov jih ne bo posnetih toliko: %3$d. + %1$d posnetki so načrtovani za serijo %2$s. Zaradi sporov jih ne bo posnetih toliko: %3$d. + %1$d posnetkov je načrtovanih za serijo %2$s. Zaradi sporov jih ne bo posnetih toliko: %3$d. + + + %1$d posnetek je načrtovan za serijo %2$s. Zaradi sporov ne bo posnetih toliko epizod te serije in drugih serij: %3$d. + %1$d posnetka sta načrtovana za serijo %2$s. Zaradi sporov ne bo posnetih toliko epizod te serije in drugih serij: %3$d. + %1$d posnetki so načrtovani za serijo %2$s. Zaradi sporov ne bo posnetih toliko epizod te serije in drugih serij: %3$d. + %1$d posnetkov je načrtovanih za serijo %2$s. Zaradi sporov ne bo posnetih toliko epizod te serije in drugih serij: %3$d. + + + %1$d posnetek je načrtovan za serijo %2$s. Zaradi sporov ne bo posneta 1 epizoda druge serije. + %1$d posnetka sta načrtovana za serijo %2$s. Zaradi sporov ne bo posneta 1 epizoda druge serije. + %1$d posnetki so načrtovani za serijo %2$s. Zaradi sporov ne bo posneta 1 epizoda druge serije. + %1$d posnetkov je načrtovanih za serijo %2$s. Zaradi sporov ne bo posneta 1 epizoda druge serije. + + + %1$d posnetek je načrtovan za serijo %2$s. Zaradi sporov ne bo posnetih toliko epizod drugih serij: %3$d. + %1$d posnetka sta načrtovana za serijo %2$s. Zaradi sporov ne bo posnetih toliko epizod drugih serij: %3$d. + %1$d posnetki so načrtovani za serijo %2$s. Zaradi sporov ne bo posnetih toliko epizod drugih serij: %3$d. + %1$d posnetkov je načrtovanih za serijo %2$s. Zaradi sporov ne bo posnetih toliko epizod drugih serij: %3$d. + "Posnetega programa ni bilo mogoče najti." "Sorodni posnetki" "(ni opisa programa)" @@ -362,6 +371,7 @@ "Ustavitev snemanja serije?" "Posnete epizode bodo na voljo v knjižnici digitalnega videorekorderja." "Ustavi" + "Trenutno se ne predvaja nobena epizoda." "Na voljo ni nobena epizoda.\nPosnete bodo, ko bodo na voljo." (%1$d minuta) diff --git a/res/values-sr/strings.xml b/res/values-sr/strings.xml index 0b4c2f3c..618f9e86 100644 --- a/res/values-sr/strings.xml +++ b/res/values-sr/strings.xml @@ -20,9 +20,8 @@ "моно" "стерео" "Play контроле" - "Недавни канали" + "Канали" "ТВ опције" - "Опц. сл. у сл." "Контроле за пуштање нису доступне за овај канал" "Пусти или паузирај" "Премотај унапред" @@ -35,33 +34,15 @@ "Титлови" "Режим приказа" "Слика у слици" - "Укључено" - "Искључено" "Вишеструк аудио" "Набави још канала" "Подешавања" - "Извор" - "Замени" - "Укључено" - "Искључено" - "Звук" - "Главни" - "Прозор слике у слици" - "Распоред" - "Доњи десни угао" - "Горњи десни угао" - "Горњи леви угао" - "Доњи леви угао" - "Упоредо" - "Величина" - "Велика" - "Мала" - "Извор улаза" "ТВ (антенска/кабловска)" "Нема информација о програму" "Нема информација" "Блокирани канал" - "Непознат језик" + "Непознат језик" + "Опционални титл: %1$d" "Опционални титлови" "Искључи" "Прилагоди формат" @@ -137,6 +118,10 @@ "Тај PIN је погрешан. Пробајте поново." "Пробајте поново, PIN се не подудара" + "Унесите поштански број" + "Апликација Канали уживо ће користити овај поштански број да би пружила комплетан водич за програме за ТВ канале." + "Унесите поштански број" + "Неважећи поштански број" "Подешавања" "Прилагоди листу канала" "Изаберите канале за водич за програме" @@ -145,6 +130,7 @@ "Родитељски надзор" "Лиценце отвореног кода" "Лиценце отвореног кода" + "Пошаљи повратне информације" "Верзија" "Да бисте гледали овај канал, притисните дугме Десно и унесите PIN" "Да бисте гледали овај програм, притисните дугме Десно и унесите PIN" @@ -185,8 +171,6 @@ "Притисните ИЗАБЕРИ"" да бисте приступили TV менију." "Нису пронађени ТВ улази" "Не можемо да пронађемо ТВ улаз" - "Слика у слици није подржана" - "Нема улаза који може да се прикаже као слика у слици" "Тип тјунера не одговара. Покрените апликацију Канали уживо за тип ТВ улаза са тјунером." "Подешавање није успело" "Није пронађена ниједна апликација која би могла да обави ову радњу." @@ -269,8 +253,6 @@ "Сачувај" "Једнократни снимци имају највиши приоритет" "Откажи" - "Откажи" - "Заборави" "Заустави" "Прикажи распоред за снимање" "Само ова епизода" @@ -280,25 +262,29 @@ "Сними овај програм уместо њега" "Откажи ово снимање" "Пусти одмах" + "Избришите снимке…" "Подржава снимање" "Снимање је заказано" "Неусаглашеност при снимању" "Снимање" "Снимање није успело" "Читамо програме да бисмо направили распореде" - "Читамо програме" - - + "Читамо програме" + "Прикажи недавне снимке" + "Снимање програма %1$s није довршено." + "Снимање програма %1$s и %2$s није довршено." + "Снимање програма %1$s, %2$s и %3$s није довршено." + "Нисмо довршили снимање програма %1$s због недовољног меморијског простора." + "Снимање програма %1$s и %2$s није довршено због недовољног меморијског простора." + "Снимање програма %1$s, %2$s и %3$s није довршено због недовољног меморијског простора." "DVR-у треба више меморијског простора" "Моћи ћете да снимате програме помоћу DVR-а. Међутим, тренутно на уређају нема довољно меморијског простора да би DVR функционисао. Повежите спољни диск који има %1$s GB или више и пратите кораке да бисте га форматирали као меморијски простор уређаја." + "Нема довољно меморијског простора" + "Овај програм неће бити снимљен јер нема довољно меморијског простора. Пробајте да избришете неколико постојећих снимака." "Меморијски простор недостаје" - "Недостаје део меморијског простора који DVR користи. Повежите спољни диск који сте раније користили да бисте поново омогућили DVR. Уместо тога можете да заборавите меморијски простор ако више није доступан." - "Желите ли да заборавите меморијски простор?" - "Сав снимљени садржај и распореди ће бити изгубљени." "Зауставити снимање?" "Снимљени садржај ће се сачувати." - - + "Снимање програма %1$s ће се зауставити јер је дошло до неусаглашености са овим програмом. Снимљени садржај ће бити сачуван." "Снимање је заказано, али постоје неусаглашености" "Снимање је почело али има неусаглашености" "Програм %1$s ће бити снимљен." @@ -317,14 +303,32 @@ "Снимање истог програма је већ заказано за %1$s." "Већ је снимљено" "Овај програм је већ снимљен. Доступан је у DVR филмотеци." - - - - - - - - + "Снимање серије је заказано" + + %1$d снимање је заказано за серијал %2$s. + %1$d снимања су заказана за серијал %2$s. + %1$d снимања је заказано за серијал %2$s. + + + %1$d снимање је заказано за серијал %2$s. Због преклапања неће бити снимљено: %3$d. + %1$d снимања су заказана за серијал %2$s. Због преклапања неће бити снимљено: %3$d. + %1$d снимања је заказано за серијал %2$s. Због преклапања неће бити снимљено: %3$d. + + + %1$d снимање је заказано за серијал %2$s. %3$d епизода(е) овог серијала и других серијала неће бити снимљено због преклапања. + %1$d снимања су заказана за серијал %2$s. %3$d епизода(е) овог серијала и других серијала неће бити снимљено због преклапања. + %1$d снимања је заказано за серијал %2$s. %3$d епизода(е) овог серијала и других серијала неће бити снимљено због преклапања. + + + %1$d снимање је заказано за серијал %2$s. 1 епизода другог серијала неће бити снимљена због преклапања. + %1$d снимања су заказана за серијал %2$s. 1 епизода другог серијала неће бити снимљена због преклапања. + %1$d снимања је заказано за серијал %2$s. 1 епизода другог серијала неће бити снимљена због преклапања. + + + %1$d снимање је заказано за серијал %2$s. %3$d епизоде(а) других серијала неће бити снимљено због преклапања. + %1$d снимања су заказана за серијал %2$s. %3$d епизоде(а) других серијала неће бити снимљено због преклапања. + %1$d снимања је заказано за серијал %2$s. %3$d епизоде(а) других серијала неће бити снимљено због преклапања. + "Снимљени програм није пронађен." "Сродни снимци" "(Нема описа програма)" @@ -349,6 +353,7 @@ "Зауставити снимање серије?" "Снимљене епизоде ће остати доступне у DVR библиотеци." "Заустави" + "Тренутно се не приказује ниједна епизода." "Нема доступних епизода.\nБиће снимљене када постану доступне." (%1$d минут) diff --git a/res/values-sv/strings.xml b/res/values-sv/strings.xml index 212fd957..8f554150 100644 --- a/res/values-sv/strings.xml +++ b/res/values-sv/strings.xml @@ -20,9 +20,8 @@ "mono" "stereo" "Uppspelningskontroller" - "Senaste kanaler" + "Kanaler" "Tv-alternativ" - "PIP-alternativ" "Uppspelningskontrollerna är inte tillgängliga för den här kanalen" "Spela upp eller pausa" "Snabbspola framåt" @@ -35,33 +34,15 @@ "Textning" "Visningsläge" "PIP" - "På" - "Av" "Flera ljudkäll." "Få fler kanaler" "Inställningar" - "Källa" - "Byt" - "På" - "Av" - "Ljud" - "Primär" - "PIP-fönster" - "Layout" - "Nere till h." - "Uppe till h." - "Uppe till v." - "Nere till v." - "Sida vid sida" - "Storlek" - "Stor" - "Liten" - "Ingångskälla" "Tv (antenn/kabel)" "Ingen programinformation" "Ingen information" "Blockerad kanal" - "Okänt språk" + "Okänt språk" + "Textning %1$d" "Textning" "Av" "Anpassa formatering" @@ -135,6 +116,10 @@ "Det var fel pinkod. Försök igen." "Försök igen. Pinkoden stämmer inte" + "Ange ditt postnummer." + "Appen Livekanaler använder postnumret till att ta fram en komplett programguide för TV-kanalerna." + "Ange ditt postnummer" + "Ogiltigt postnummer" "Inställningar" "Anpassa kanallista" "Välj kanaler för programguiden" @@ -143,6 +128,7 @@ "Innehållsfilter" "Licenser för öppen källkod" "Öppen källkod" + "Skicka feedback" "Version" "Tryck till höger och ange pinkoden om du vill titta på den här kanalen" "Tryck till höger och ange pinkoden om du vill titta på det här programmet" @@ -181,8 +167,6 @@ "Tryck på VÄLJ"" för att öppna TV-menyn." "Ingen tv-ingång hittades" "Det går inte att hitta tv-ingången" - "PIP stöds inte" - "Det finns ingen tillgänglig ingång som kan visas med PIP" "Olämplig insignal. Starta appen Livekanaler om du vill kunna ta emot insignaler av tv-kortstyp." "Kanaljusteringen misslyckades" "Ingen app som kan hantera åtgärden hittades" @@ -191,11 +175,11 @@ "Knappen BACK (bakåt) gäller en ansluten enhet. Avsluta genom att trycka på knappen HOME (start)." "Livekanaler behöver behörighet att läsa TV-tablåer." "Konfigurera dina källor" - "Livekanaler kombinerar det bästa från traditionella TV-kanaler med strömmande kanaler från appar. \n\nKom igång genom att konfigurera de kanalkällor som redan är installerade eller kolla in Google Play Butik för fler appar som erbjuder livekanaler." + "Livekanaler kombinerar det bästa från traditionella TV-kanaler med streamade kanaler från appar. \n\nKom igång genom att konfigurera de kanalkällor som redan är installerade eller kolla in Google Play Butik för fler appar som erbjuder livekanaler." "Inspelningar och scheman" "10 minuter" "30 minuter" - "En timme" + "1 timme" "3 timmar" "Senaste" "Planerat" @@ -259,8 +243,6 @@ "Spara" "Engångsinspelningar har högsta prioritet" "Avbryt" - "Avbryt" - "Glöm" "Stoppa" "Visa inspelningsschema" "Bara det här programmet" @@ -270,25 +252,29 @@ "Spela in detta i stället" "Avbryt den här inspelningen" "Titta nu" + "Radera inspelningar …" "Kan spelas in" "Inspelningen har schemalagts" "Inspelningskonflikt" "Inspelning" "Inspelningen misslyckades" "Läser in program för att inspelningsscheman" - "Läser in program" - - + "Läser in program" + "Visa de senaste inspelningarna" + "Inspelningen av %1$s är ofullständig." + "Inspelningen av %1$s och %2$s är ofullständig." + "Inspelningen av %1$s, %2$s och %3$s är ofullständig." + "Det gick inte att slutföra inspelningen av %1$s eftersom det inte finns tillräckligt med lagringsutrymme." + "Det gick inte att slutföra inspelningen av %1$s och %2$s eftersom det inte finns tillräckligt med lagringsutrymme." + "Det gick inte att slutföra inspelningen av %1$s, %2$s och %3$s eftersom det inte finns tillräckligt med lagringsutrymme." "Mer lagringsutrymme krävs för hårddiskinspelning" "Du kan använda hårddiskinspelning för att spela in program, men just nu finns det inte tillräckligt mycket ledigt lagringsutrymme på enheten för hårddiskinspelning. Anslut en extern enhet på minst %1$s GB och följ anvisningarna för att formatera den som enhetslagring." + "Otillräckligt lagringsutrymme" + "Det här programmet spelas inte in eftersom lagringsutrymmet inte räcker. Du kan göra plats genom att radera gamla inspelningar." "Lagringsutrymmet är inte tillgängligt" - "En del av lagringsutrymmet som används av DVR saknas. Anslut den externa hårddisken du använde innan DVR återaktiverades. Du kan också att välja att glömma bort lagringsutrymmet om det inte längre är tillgängligt." - "Ska lagringsutrymmet glömmas bort?" - "Allt rekommenderat innehåll och alla inspelningsscheman försvinner." "Vill du sluta spela in?" "Det inspelade innehållet sparas." - - + "Inspelningen av %1$s avbryts eftersom den krockar med det här programmet. Det inspelade innehållet sparas." "Inspelningen har schemalagts men innehåller konflikter" "Inspelningen har startat men innehåller konflikter" "%1$s spelas in." @@ -306,14 +292,27 @@ "Samma program har schemalagts för inspelning kl. %1$s." "Redan inspelat" "Programmet har redan spelats in. Det finns i DVR-biblioteket." - - - - - - - - + "Serieinspelningen har schemalagts" + + %1$d inspelningar har schemalagts för %2$s. + %1$d inspelning har schemalagts för %2$s. + + + %1$d inspelningar har schemalagts för %2$s. %3$d av dem spelas inte in på grund av programkrockar. + %1$d inspelning har schemalagts för %2$s. Det spelas inte in på grund av programkrockar. + + + %1$d inspelningar har schemalagts för %2$s. %3$d avsnitt av den här serien och andra serier spelas inte in på grund av programkrockar. + %1$d inspelning har schemalagts för %2$s. %3$d avsnitt av den här serien och andra serier spelas inte in på grund av programkrockar. + + + %1$d inspelningar har schemalagts för %2$s. 1 avsnitt av andra serier spelas inte in på grund av programkrockar. + %1$d inspelning har schemalagts för %2$s. 1 avsnitt av andra serier spelas inte in på grund av programkrockar. + + + %1$d inspelningar har schemalagts för %2$s. %3$d avsnitt av andra serier spelas inte in på grund av programkrockar. + %1$d inspelning har schemalagts för %2$s. %3$d avsnitt av andra serier spelas inte in på grund av programkrockar. + "Det gick inte att hitta det inspelade programmet." "Relaterade inspelningar" "(Programbeskrivning saknas)" @@ -336,6 +335,7 @@ "Vill du stoppa serieinspelning?" "Inspelade avsnitt finns kvar i DVR-biblioteket" "Stoppa" + "Inga avsnitt sänds just nu." "Det finns inga tillgängliga avsnitt.\nDe spelas in när de är tillgängliga." (%1$d minuter) diff --git a/res/values-sw/strings.xml b/res/values-sw/strings.xml index 00038ff4..92f4d200 100644 --- a/res/values-sw/strings.xml +++ b/res/values-sw/strings.xml @@ -20,9 +20,8 @@ "mono" "stereo" "Vidhibiti vya kucheza" - "Vituo vya hivi karibuni" + "Vituo" "Chaguo za Runinga" - "Chaguo za PIP" "Vidhibiti vya kucheza havipatikani kwa kituo hiki" "Cheza au usitishe" "Peleka mbele kwa kasi" @@ -35,33 +34,15 @@ "Manukuu" "Hali ya onyesho" "PIP" - "Imewashwa" - "Imezimwa" "Sauti nyingi" "Pata vituo zaidi" "Mipangilio" - "Chanzo" - "Badili" - "Imewashwa" - "Imezimwa" - "Sauti" - "Kuu" - "Dirisha la PIP" - "Muundo" - "Chini kulia" - "Juu kulia" - "Juu kushoto" - "Chini kushoto" - "Upande kwa upande" - "Ukubwa" - "Kubwa" - "Ndogo" - "Chanzo cha data" "Runinga (antena/kebo)" "Hakuna maelezo ya programu" "Hakuna maelezo" "Kituo kilichozuiwa" - "Lugha Isiyojulikana" + "Lugha isiyojulikana" + "Manukuu %1$d" "Manukuu" "Imezimwa" "Geuza muundo ukufae" @@ -135,6 +116,10 @@ "PIN uliyoweka si sahihi. Jaribu tena." "Jaribu tena, PIN hailingani" + "Andika Msimbo wa Eneo lako" + "Programu ya Televisheni Mtandaoni itatumia Msimbo wa eneo lako ili kutoa orodha kamili ya vipindi vya vituo vya televisheni." + "Weka Msimbo wa Eneo lako" + "Msimbo wa Eneo si Sahihi" "Mipangilio" "Geuza orodha ya vituo utakavyo" "Chagua vituo kwa ajili ya orodha yako ya vipindi" @@ -143,6 +128,7 @@ "Udhibiti wa wazazi" "Leseni za programu huria" "Leseni za programu huria" + "Tuma maoni" "Toleo" "Ili uangalie kituo hiki, bonyeza Kulia na uweke PIN" "Ili uangalie kipindi hiki, bonyeza Kulia na uweke PIN" @@ -181,8 +167,6 @@ "Bonyeza CHAGUA "" ili ufikie menyu ya televisheni." "Hakuna vifaa vya kuingiza maudhui ya runinga vinavyopatikana" "Haiwezi kupata vifaa vya kuingiza maudhui kwenye runinga" - "PIP haiwezi kutumika" - "Hakuna maudhui yanayoweza kuonyeshwa pamoja na PIP" "Aina ya kirekebishaji haifai. Fungua programu ya Vituo vya Moja kwa Moja ya vifaa vya kuingiza maudhui ya runinga." "Haikuweza kurekebisha" "Hakuna programu iliyopatikana inayoweza kushughulikia kitendo hiki." @@ -259,8 +243,6 @@ "Hifadhi" "Rekodi za mara moja hupewa kipaumbele" "Ghairi" - "Ghairi" - "Sahau" "Komesha" "Angalia ratiba ya kurekodi" "Mpango huu mmoja" @@ -270,25 +252,29 @@ "Rekodi hii badala yake" "Ghairi rekodi hii" "Angalia sasa" + "Futa rekodi..." "Inaweza kurekodiwa" "Kurekodi kumeratibiwa" "Hitilafu ya kurekodi" "Inarekodi" "Imeshindwa kurekodi" "Inasoma maelezo ya programu ili kuunda ratiba" - "Inasoma programu" - - + "Inasoma ratiba" + "Angalia rekodi za hivi majuzi" + "Haikumaliza kurekodi %1$s." + "Haikumaliza kurekodi %1$s na %2$s." + "Haikumaliza kurekodi %1$s, %2$s na %3$s." + "Haikumaliza kurekodi %1$s kwa sababu nafasi ya hifadhi haikutosha." + "Haikumaliza kurekodi %1$s na %2$s kwa sababu nafasi ya hifadhi haikutosha." + "Haikumaliza kurekodi %1$s, %2$s na %3$s kwa sababu nafasi ya hifadhi haikutosha." "DVR inahitaji nafasi zaidi ya hifadhi" "Utaweza kurekodi vipindi kwa kutumia DVR. Hata hivyo, kwa sasa hakuna hifadhi ya kutosha kwenye kifaa chako ili kuwezesha DVR kufanya kazi. Tafadhali unganisha hifadhi ya nje isiyopungua GB %1$s na ufuate hatua za kuiumbiza kuwa hifadhi ya kifaa." + "Hifadhi haitoshi" + "Kipindi hiki hakitarekodiwa kwa sababu hakuna hifadhi ya kutosha. Jaribu kufuta baadhi ya vipindi vilivyopo." "Hifadhi haipo" - "Baadhi ya hifadhi inayotumiwa na DVR haipo. Tafadhali unganisha hifadhi ya nje uliyotumia awali ili uwashe upya DVR yako. Vinginevyo, unaweza kuchagua kusahau hifadhi ikiwa haipatikani tena." - "Ungependa kusahau hifadhi?" - "Hatua hii itaondoa maudhui na ratiba zote ulizohifadhi." "Ungependa kuacha kurekodi?" "Itahifadhi maudhui uliyorekodi." - - + "Shughuli ya kurekodi kipindi cha %1$s itasitishwa kwa sababu inakinzana na kipindi hiki. Maudhui yaliyorekodiwa yatahifadhiwa." "Rekodi zimeratibiwa lakini zinakinzana" "Inaendelea kurekodi japo kuna ukinzani" "%1$s itarekodiwa." @@ -306,14 +292,27 @@ "Tayari umeratibu kurekodi kipindi hiki saa %1$s." "Tayari kimerekodiwa" "Tayari umerekodi kipindi hiki. Kinapatikana katika maktaba ya DVR." - - - - - - - - + "Shughuli ya kurekodi mfululizo imeratibiwa" + + Imeratibu kurekodi vipindi %1$d vya %2$s. + Imeratibu kurekodi kipindi %1$d cha %2$s. + + + Imeratibu kurekodi vipindi %1$d vya %2$s. Haitarekodi vipindi %3$d kutokana na ukinzani. + Imeratibu kurekodi kipindi %1$d cha %2$s. Haitarekodi kipindi kutokana na ukinzani. + + + Imeratibu kurekodi vipindi %1$d vya %2$s. Haitarekodi vipindi %3$d vya mfululizo huu na mifululizo mingine kutokana na ukinzani. + Imeratibu kurekodi kipindi %1$d cha %2$s. Haitarekodi vipindi %3$d vya mfululizo huu na mifululizo mingine kutokana na ukinzani. + + + Imeratibu kurekodi vipindi %1$d vya %2$s. Haitarekodi kipindi 1 cha mifululizo mingine kutokana na ukinzani. + Imeratibu kurekodi kipindi %1$d cha %2$s. Haitarekodi kipindi 1 cha mifululizo mingine kutokana na ukinzani. + + + Imeratibu kurekodi vipindi %1$d vya %2$s. Haitarekodi vipindi %3$d vya mifululizo mingine kutokana na ukinzani. + Imeratibu kurekodi kipindi %1$d cha %2$s. Haitarekodi vipindi %3$d vya mifululizo mingine kutokana na ukinzani. + "Haikupata programu iliyorekodiwa." "Rekodi zinazohusiana" "(Hakuna maelezo ya programu)" @@ -336,6 +335,7 @@ "Ungependa kukomesha kurekodi mfululizo?" "Vipindi vilivyorekodiwa vitaendelea kupatikana katika maktaba ya DVR." "Komesha" + "Hakuna vipindi vinavyopeperushwa kwa sasa." "Hakuna vipindi vinavyopatikana.\nVitarekodiwa pindi vitakapopatikana." (Dakika %1$d) diff --git a/res/values-ta-rIN/strings.xml b/res/values-ta-rIN/strings.xml index 7d13e3cf..79e9d974 100644 --- a/res/values-ta-rIN/strings.xml +++ b/res/values-ta-rIN/strings.xml @@ -20,9 +20,8 @@ "மோனோ" "ஸ்டீரியோ" "இயக்கக் கட்டுப்பாடுகள்" - "சமீபத்திய சேனல்கள்" + "சேனல்கள்" "டிவி விருப்பங்கள்" - "PIP விருப்பங்கள்" "இந்தச் சேனலுக்கு இயக்கக் கட்டுப்பாடுகள் இல்லை" "இயக்கு அல்லது இடைநிறுத்து" "வேகமாக முன் நகர்த்து" @@ -35,33 +34,15 @@ "விரிவான வசனங்கள்" "காட்சிப் பயன்முறை" "PIP" - "இயக்கத்தில்" - "முடக்கத்தில்" "மல்டி-ஆடியோ" "மேலும் சேனல்கள்" "அமைப்புகள்" - "மூலம்" - "மாற்று" - "இயக்கத்தில்" - "முடக்கத்தில்" - "ஒலி" - "முதன்மை" - "PIP சாளரம்" - "தளவமைப்பு" - "கீழ் வலதுபுறம்" - "மேல் வலதுபுறம்" - "மேல் இடதுபுறம்" - "கீழ் இடதுபுறம்" - "அருகருகே" - "அளவு" - "பெரியது" - "சிறியது" - "உள்ளீட்டு மூலம்" "டிவி (ஆண்டெனா/கேபிள்)" "நிகழ்ச்சி தகவல் இல்லை" "தகவல் இல்லை" "தடுக்கப்பட்ட சேனல்" - "தெரியாத மொழி" + "தெரியாத மொழி" + "வசனங்கள் %1$d" "விரிவான வசனங்கள்" "முடக்கு" "வடிவமைப்பைத் தனிப்பயனாக்கவும்" @@ -135,6 +116,10 @@ "PIN தவறானது. மீண்டும் முயலவும்." "மீண்டும் முயலவும், PIN பொருந்தவில்லை" + "அஞ்சல் குறியீட்டை உள்ளிடவும்." + "நேரலைச் சேனல்கள் பயன்பாடானது, டிவி சேனல்களின் முழுமையான நிகழ்ச்சி வழிகாட்டியை வழங்குவதற்கு அஞ்சல் குறியீட்டைப் பயன்படுத்தும்." + "அஞ்சல் குறியீட்டை உள்ளிடவும்" + "தவறான அஞ்சல் குறியீடு" "அமைப்புகள்" "சேனல் பட்டியலைத் தனிப்பயனாக்கு" "நிகழ்ச்சி வழிகாட்டியில் சேர்ப்பதற்கான சேனல்களைத் தேர்வுசெய்க" @@ -143,6 +128,7 @@ "பெற்றோர் கட்டுப்பாடுகள்" "ஓப்பன் சோர்ஸ் உரிமங்கள்" "ஓப்பன் சோர்ஸ் உரிமங்கள்" + "கருத்து அனுப்பு" "பதிப்பு" "இந்தச் சேனலைப் பார்க்க, வலது பக்கம் அழுத்தி, உங்கள் PINஐ உள்ளிடவும்" "இந்த நிகழ்ச்சியைப் பார்க்க, வலது பக்கம் அழுத்தி PINஐ உள்ளிடவும்" @@ -181,8 +167,6 @@ "TV மெனுவை அணுக, ""SELECTஐ அழுத்தவும்""." "டிவி உள்ளீடு இல்லை" "டிவி உள்ளீடு இல்லை" - "PIP ஆதரிக்கப்படவில்லை" - "PIP உடன் காண்பிக்கத்தக்க உள்ளீடு எதுவுமில்லை" "ட்யூனர் வகை பொருந்தவில்லை. ட்யூனர் வகை டிவி உள்ளீட்டிற்கு நேரலைச் சேனல்கள் பயன்பாட்டைத் துவங்கவும்." "ட்யூன் செய்ய முடியவில்லை" "இந்தச் செயலைச் செய்வதற்கான பயன்பாடு எதுவுமில்லை." @@ -259,8 +243,6 @@ "சேமி" "ஒரே முறை ரெக்கார்டு செய்யக்கூடியவற்றுக்கு மிக அதிக முன்னுரிமை வழங்கு" "ரத்துசெய்" - "ரத்துசெய்" - "நீக்கு" "நிறுத்து" "ரெக்கார்டிங் ஷெட்யூலைக் காட்டு" "இந்த நிகழ்ச்சியை மட்டும்" @@ -270,25 +252,29 @@ "பதிலாக, இதை ரெக்கார்டு செய்" "இந்த ரெக்கார்டிங்கை ரத்துசெய்" "இப்போது காட்டு" + "ரெக்கார்டிங்குகளை நீக்கு…" "ரெக்கார்டு செய்யக்கூடியது" "ரெக்கார்டிங் திட்டமிடப்பட்டது" "ரெக்கார்டிங்கில் முரண்பாடு" "ரெக்கார்ட் செய்யப்படுகிறது" "ரெக்கார்டு செய்ய முடியவில்லை" "ரெக்கார்டிங் ஷெட்யூல்களை உருவாக்க, நிகழ்ச்சிகளைப் படிக்கிறது" - "நிகழ்ச்சிகளைப் படிக்கிறது" - - + "நிகழ்ச்சிகளைப் படிக்கிறது" + "சமீபத்திய ரெக்கார்டிங்குகளைக் காட்டு" + "%1$sஐ ரெக்கார்டு செய்வது முடிவடையவில்லை." + "%1$s மற்றும் %2$sஐ ரெக்கார்டு செய்வது முடிவடையவில்லை." + "%1$s, %2$s, %3$s ஆகியவற்றை ரெக்கார்டு செய்வது முடிவடையவில்லை." + "போதுமான சேமிப்பிடம் இல்லாததால், %1$sஐ ரெக்கார்டு செய்வது முடிவடையவில்லை." + "போதுமான சேமிப்பிடம் இல்லாததால், %1$s மற்றும் %2$sஐ ரெக்கார்டு செய்வது முடிவடையவில்லை." + "போதுமான சேமிப்பிடம் இல்லாததால், %1$s, %2$s, %3$s ஆகியவற்றை ரெக்கார்டு செய்வது முடிவடையவில்லை." "DVRக்கு அதிகச் சேமிப்பிடம் தேவை" "நீங்கள் DVR மூலம் நிகழ்ச்சிகளைப் பதிவுசெய்ய முடியும். எனினும், DVR சரியாக வேலை செய்வதற்குத் தேவைப்படும் போதுமான சேமிப்பகம் இப்போது சாதனத்தில் இல்லை. %1$sஜி.பை. அல்லது அதற்கும் அதிகமான அளவில் உள்ள வெளிப்புற இயக்ககத்தை இணைக்கவும். பின் அதைச் சாதனச் சேமிப்பகமாகப் பயன்படுத்த, இந்தப் படிகளைப் பின்பற்றவும்." + "போதுமான சேமிப்பிடம் இல்லை" + "போதுமான சேமிப்பிடம் இல்லாததால், இந்த நிகழ்ச்சி ரெக்கார்டு செய்யப்படாது. ஏற்கனவே உள்ள சில ரெக்கார்டிங்குகளை நீக்கவும்." "சேமிப்பகம் இல்லை" - "DVR பயன்படுத்தும் சேமிப்பகம் இல்லை. DVRஐ மீண்டும் இயக்கும் முன், நீங்கள் பயன்படுத்தும் வெளிப்புற டிரைவை இணைக்கவும். மாற்றுவழியாக, சேமிப்பகத்தை இனி பயன்படுத்த மாட்டீர்கள் எனில், அதை நீக்கும்படியும் தேர்ந்தெடுக்கலாம்." - "சேமிப்பகத்தை நீக்கவா?" - "பதிவுசெய்த உள்ளடக்கத்தையும் திட்ட அட்டவணைகளையும் இழப்பீர்கள்." "ரெக்கார்டு செய்வதை நிறுத்தவா?" "ரெக்கார்டு செய்த உள்ளடக்கம் சேமிக்கப்படும்." - - + "%1$sஐ ரெக்கார்டு செய்வது இந்த நிகழ்ச்சியுடன் முரண்படுவதால், ரெக்கார்டிங் நிறுத்தப்படும். ரெக்கார்டு செய்த உள்ளடக்கம் சேமிக்கப்படும்." "ரெக்கார்டிங் திட்டமிடப்பட்டது, ஆனால் முரண்பாடுகள் உள்ளன" "ரெக்கார்டு செய்வது தொடங்கப்பட்டது, ஆனால் முரண்பாடுகள் உள்ளன" "%1$s ரெக்கார்டு செய்யப்படும்." @@ -306,14 +292,27 @@ "%1$sக்கு ரெக்கார்டு செய்வதற்காக இந்த நிகழ்ச்சி ஏற்கனவே திட்டமிடப்பட்டுள்ளது." "ஏற்கனவே ரெக்கார்டு செய்யப்பட்டது" "இந்த நிகழ்ச்சி ஏற்கனவே ரெக்கார்டு செய்யப்பட்டது. மேலும் DVR நூலகத்தில் கிடைக்கும்." - - - - - - - - + "தொடர் ரெக்கார்டிங் திட்டமிடப்பட்டது" + + %2$s தொடருக்கு %1$d ரெக்கார்டிங்குகள் திட்டமிடப்பட்டுள்ளன. + %2$s தொடருக்கு %1$d ரெக்கார்டிங் திட்டமிடப்பட்டுள்ளது. + + + %2$s தொடருக்கு %1$d ரெக்கார்டிங்குகள் திட்டமிடப்பட்டுள்ளன. முரண்பாடுகளின் காரணமாக, இந்தத் தொடரின் %3$d எபிசோடுகள் ரெக்கார்டு செய்யப்படாது. + %2$s தொடருக்கு %1$d ரெக்கார்டிங் திட்டமிடப்பட்டுள்ளது. முரண்பாடுகளின் காரணமாக, இது ரெக்கார்டு செய்யப்படாது. + + + %2$s தொடருக்கு %1$d ரெக்கார்டிங்குகள் திட்டமிடப்பட்டுள்ளன. முரண்பாடுகளின் காரணமாக, இந்தத் தொடர் மற்றும் பிற தொடரின் %3$d எபிசோடுகள் ரெக்கார்டு செய்யப்படாது. + %2$s தொடருக்கு %1$d ரெக்கார்டிங் திட்டமிடப்பட்டுள்ளது. முரண்பாடுகளின் காரணமாக, இந்தத் தொடர் மற்றும் பிற தொடரின் %3$d எபிசோடுகள் ரெக்கார்டு செய்யப்படாது. + + + %2$s தொடருக்கு %1$d ரெக்கார்டிங்குகள் திட்டமிடப்பட்டுள்ளன. முரண்பாடுகளின் காரணமாக, பிற தொடரின் ஒரு எபிசோடு ரெக்கார்டு செய்யப்படாது. + %2$s தொடருக்கு %1$d ரெக்கார்டிங் திட்டமிடப்பட்டுள்ளது. முரண்பாடுகளின் காரணமாக, பிற தொடரின் ஒரு எபிசோடு ரெக்கார்டு செய்யப்படாது. + + + %2$s தொடருக்கு %1$d ரெக்கார்டிங்குகள் திட்டமிடப்பட்டுள்ளன. முரண்பாடுகளின் காரணமாக, பிற தொடரின் %3$d எபிசோடுகள் ரெக்கார்டு செய்யப்படாது. + %2$s தொடருக்கு %1$d ரெக்கார்டிங் திட்டமிடப்பட்டுள்ளது. முரண்பாடுகளின் காரணமாக, பிற தொடரின் %3$d எபிசோடுகள் ரெக்கார்டு செய்யப்படாது. + "ரெக்கார்டு செய்த நிகழ்ச்சி இல்லை." "தொடர்புடைய ரெக்கார்டிங்குகள்" "(நிகழ்ச்சி விளக்கம் இல்லை)" @@ -336,6 +335,7 @@ "தொடர் ரெக்கார்டிங்கை நிறுத்தவா?" "ரெக்கார்டு செய்யப்பட எபிசோடுகள் தொடர்ந்து DVR நூலகத்தில் இருக்கும்." "நிறுத்து" + "இப்போது எந்த எபிசோடுகளும் நேரலையில் இல்லை." "எபிசோடுகள் இல்லை.\nஅவை கிடைக்கும் போது ரெக்கார்டு செய்யப்படும்." (%1$d நிமிடங்கள்) diff --git a/res/values-te-rIN/strings.xml b/res/values-te-rIN/strings.xml index 274be2ce..f58294d1 100644 --- a/res/values-te-rIN/strings.xml +++ b/res/values-te-rIN/strings.xml @@ -20,9 +20,8 @@ "మోనో" "స్టీరియో" "ప్లే నియంత్రణలు" - "ఇటీవలి ఛానెళ్లు" + "ఛానెల్‌లు" "టీవీ ఎంపికలు" - "PIP ఎంపికలు" "ఈ ఛానెల్ యొక్క ప్లే నియంత్రణలు అందుబాటులో లేవు" "ప్లే చేస్తుంది లేదా పాజ్ చేస్తుంది" "ఫాస్ట్ ఫార్వార్డ్ చేస్తుంది" @@ -35,33 +34,15 @@ "సంవృత శీర్షికలు" "ప్రదర్శన మోడ్" "PIP" - "ఆన్‌లో ఉంది" - "ఆఫ్‌లో ఉంది" "బహుళ-ఆడియో" "మరిన్ని ఛానెల్‌లను పొందండి" "సెట్టింగ్‌లు" - "మూలం" - "మార్చు" - "ఆన్‌లో ఉంది" - "ఆఫ్‌లో ఉంది" - "ధ్వని" - "ప్రధానమైనది" - "PIP విండో" - "లేఅవుట్" - "దిగువ కుడివైపు" - "ఎగువ కుడివైపు" - "ఎగువ ఎడమవైపు" - "దిగువ ఎడమవైపు" - "పక్కపక్కన" - "పరిమాణం" - "పెద్దది" - "చిన్నది" - "ఇన్‌పుట్ మూలం" "టీవీ (యాంటెన్నా/కేబుల్)" "కార్యక్రమ సమాచారం లేదు" "సమాచారం లేదు" "బ్లాక్ చేసిన ఛానెల్" - "భాష తెలియదు" + "భాష తెలియదు" + "ఉపశీర్షికలు %1$d" "సంవృత శీర్షికలు" "ఆఫ్ చేయి" "ఆకృతీకరణను అనుకూలీకరించు" @@ -135,6 +116,10 @@ "ఆ పిన్ తప్పు. మళ్లీ ప్రయత్నించండి." "మళ్లీ ప్రయత్నించండి, పిన్ సరిపోలలేదు" + "మీ జిప్ కోడ్‌ను నమోదు చేయండి." + "ప్రత్యక్ష ప్రసార ఛానెల్‌ల అనువర్తనం టీవీ ఛానెల్‌లకి సంబంధించిన పూర్తి ప్రోగ్రామ్ గైడ్‌ను అందించడానికి జిప్ కోడ్‌ను ఉపయోగిస్తుంది." + "మీ జిప్ కోడ్‌ను నమోదు చేయండి" + "జిప్ కోడ్ చెల్లదు" "సెట్టింగ్‌లు" "ఛానెల్‌ జాబితా అనుకూలీకరించండి" "మీ ప్రోగ్రామ్ గైడ్ కోసం ఛానెల్‌లను ఎంచుకోండి" @@ -143,6 +128,7 @@ "తల్లిదండ్రుల నియంత్రణలు" "ఓపెన్ సోర్స్ లైసెన్స్‌లు" "ఓపెన్ సోర్స్ లైసెన్స్‌లు" + "అభిప్రాయాన్ని పంపు" "సంస్కరణ" "ఈ ఛానెల్‌ను చూడటానికి, కుడివైపు బటన్ నొక్కి, మీ పిన్‌ని నమోదు చేయండి" "ఈ ప్రోగ్రామ్‌ని చూడటానికి, కుడివైపు బటన్ నొక్కి, మీ పిన్‌ని నమోదు చేయండి" @@ -181,8 +167,6 @@ "టీవీ మెనుని ప్రాప్యత చేయడానికి ""ఎంచుకోండి నొక్కండి""." "టీవీ ఇన్‌పుట్ కనుగొనబడలేదు" "టీవీ ఇన్‌పుట్‌ను కనుగొనడం సాధ్యపడదు" - "PIPకి మద్దతు లేదు" - "PIPతో చూపబడే ఇన్‌పుట్ ఏదీ అందుబాటులో లేదు" "ట్యూనర్ రకం తగినది కాదు. దయచేసి ట్యూనర్ రకం టీవీ ఇన్‌పుట్ కోసం లైవ్ ఛానెల్‌లు అనువర్తనాన్ని ప్రారంభించండి." "ట్యూన్ విఫలమైంది" "ఈ చర్యను నిర్వహించడానికి అనువర్తనం ఏదీ కనుగొనబడలేదు." @@ -259,8 +243,6 @@ "సేవ్ చేయి" "ఒక పర్యాయ రికార్డింగ్‌లు అత్యధిక ప్రాధాన్యతను కలిగి ఉంటాయి" "రద్దు చేయి" - "రద్దు చేయి" - "విస్మరించు" "ఆపివేయి" "రికార్డింగ్ షెడ్యూల్ చూడండి" "ఈ ఒక్క కార్యక్రమం" @@ -270,25 +252,29 @@ "బదులుగా దీన్ని రికార్డ్ చేయి" "ఈ రికార్డింగ్‌ను రద్దు చేయి" "ఇప్పుడే చూడండి" + "రికార్డింగ్‌లను తొలగించు…" "రికార్డ్ చేయవచ్చు" "రికార్డింగ్ షెడ్యూల్ చేయబడింది" "రికార్డింగ్ వైరుధ్యం" "రికార్డ్ అవుతోంది" "రికార్డింగ్ విఫలమైంది" "రికార్డింగ్ షెడ్యూళ్లను రూపొందించడానికి కార్యక్రమాలను చదువుతోంది" - "కార్యక్రమాలను చదువుతోంది" - - + "కార్యక్రమాలను చదువుతోంది" + "ఇటీవలి రికార్డింగ్‌లను వీక్షించండి" + "%1$s రికార్డింగ్ అసంపూర్ణంగా ఉంది." + "%1$s మరియు %2$s రికార్డింగ్‌లు అసంపూర్ణంగా ఉన్నాయి." + "%1$s, %2$s మరియు %3$s రికార్డింగ్‌లు అసంపూర్ణంగా ఉన్నాయి." + "తగినంత నిల్వ లేని కారణంగా, %1$s రికార్డింగ్ పూర్తి కాలేదు." + "తగినంత నిల్వ లేని కారణంగా, %1$s మరియు %2$s రికార్డింగ్‌లు పూర్తి కాలేదు." + "తగినంత నిల్వ లేని కారణంగా, %1$s, %2$s మరియు %3$s రికార్డింగ్‌లు పూర్తి కాలేదు." "DVRకు మరింత నిల్వ అవసరం" "మీరు DVRతో కార్యక్రమాలను రికార్డ్ చేయగలుగుతారు. అయితే, ప్రస్తుతం DVR పని చేయడానికి మీ పరికరంలో తగినంత నిల్వ ఖాళీ లేదు. దయచేసి %1$sGB లేదా అంతకంటే ఎక్కువ ఖాళీ స్థలం గల బయటి డ్రైవ్‌ను కనెక్ట్ చేసి, ఆపై దాన్ని పరికర నిల్వగా ఫార్మాట్ చేయడానికి సూచనలను అనుసరించండి." + "తగినంత నిల్వ లేదు" + "తగినంత నిల్వ లేనందున ఈ కార్యక్రమం రికార్డ్ చేయబడదు. ఇప్పటికే ఉన్న కొన్ని రికార్డింగ్‌లను తొలగించడానికి ప్రయత్నించండి." "నిల్వ కనిపించడం లేదు" - "DVR ద్వారా ఉపయోగించబడిన కొంత నిల్వ కనిపించడం లేదు. దయచేసి DVRని పునఃప్రారంభించడానికి మీరు ఇంతకుముందు ఉపయోగించిన బయటి డిస్క్‌ను కనెక్ట్ చేయండి. లేదంటే, అది అందుబాటులో లేని పక్షంలో మీరు ఆ నిల్వను విస్మరించమని ఎంచుకోవచ్చు." - "నిల్వను విస్మరించాలా?" - "మీ మొత్తం రికార్డ్ చేసిన కంటెంట్‌ను మరియు షెడ్యూల్‌లను కోల్పోతారు." "రికార్డింగ్ ఆపివేయాలా?" "రికార్డ్ అయిన కంటెంట్ సేవ్ చేయబడుతుంది." - - + "%1$s యొక్క రికార్డింగ్‌కి ఈ కార్యక్రమంతో వైరుధ్యం తలెత్తినందున అది ఆపివేయబడుతుంది. రికార్డ్ చేసిన కంటెంట్ సేవ్ చేయబడుతుంది." "రికార్డింగ్ షెడ్యూల్ చేయబడింది కానీ మిగిలిన వాటితో వైరుధ్యాలను కలిగి ఉంది" "రికార్డింగ్ ప్రారంభమైంది, కానీ వైరుధ్యాలను కలిగి ఉంది" "%1$s రికార్డ్ చేయబడుతుంది." @@ -306,14 +292,27 @@ "ఇదే కార్యక్రమం ఇప్పటికే %1$sకి రికార్డ్ చేయడానికి షెడ్యూల్ చేయబడింది." "ఇప్పటికే రికార్డ్ అయింది" "ఈ ప్రోగ్రామ్ ఇప్పటికే రికార్డ్ అయింది. ఇది DVR లైబ్రరీలో అందుబాటులో ఉంది." - - - - - - - - + "సిరీస్ రికార్డింగ్ షెడ్యూల్ చేయబడింది" + + %2$sకి %1$d రికార్డింగ్‌లు షెడ్యూల్ చేయబడ్డాయి. + %2$sకి %1$d రికార్డింగ్ షెడ్యూల్ చేయబడింది. + + + %2$sకి %1$d రికార్డింగ్‌లు షెడ్యూల్ చేయబడ్డాయి. వైరుధ్యాల కారణంగా వాటిలో %3$d రికార్డ్ చేయబడవు. + %2$sకి %1$d రికార్డింగ్ షెడ్యూల్ చేయబడింది. వైరుధ్యాల కారణంగా ఇది రికార్డ్ చేయబడదు. + + + %2$sకి %1$d రికార్డింగ్‌లు షెడ్యూల్ చేయబడ్డాయి. వైరుధ్యాల కారణంగా ఈ సిరీస్ మరియు మరో సిరీస్‌లోని %3$d ఎపిసోడ్‌లు రికార్డ్ చేయబడవు. + %2$sకి %1$d రికార్డింగ్ షెడ్యూల్ చేయబడింది. వైరుధ్యాల కారణంగా ఈ సిరీస్ మరియు మరో సిరీస్‌లోని %3$d ఎపిసోడ్‌లు రికార్డ్ చేయబడవు. + + + %2$sకి %1$d రికార్డింగ్‌లు షెడ్యూల్ చేయబడ్డాయి. వైరుధ్యాల కారణంగా మరో సిరీస్‌లోని 1 ఎపిసోడ్ రికార్డ్ చేయబడదు. + %2$sకి %1$d రికార్డింగ్ షెడ్యూల్ చేయబడింది. వైరుధ్యాల కారణంగా మరో సిరీస్‌లోని 1 ఎపిసోడ్ రికార్డ్ చేయబడదు. + + + %2$sకి %1$d రికార్డింగ్‌లు షెడ్యూల్ చేయబడ్డాయి. వైరుధ్యాల కారణంగా మరో సిరీస్‌లోని %3$d ఎపిసోడ్‌లు రికార్డ్ చేయబడవు. + %2$sకి %1$d రికార్డింగ్ షెడ్యూల్ చేయబడింది. వైరుధ్యాల కారణంగా మరో సిరీస్‌లోని %3$d ఎపిసోడ్‌లు రికార్డ్ చేయబడవు. + "రికార్డ్ చేసిన కార్యక్రమం కనుగొనబడలేదు." "సంబంధిత రికార్డింగ్‌లు" "(కార్యక్రమం వివరణ లేదు)" @@ -336,6 +335,7 @@ "సిరీస్ రికార్డింగ్‌ను ఆపివేయాలా?" "రికార్డ్ అయిన ఎపిసోడ్‌లు DVR లైబ్రరీలో అలాగే అందుబాటులో ఉంటాయి." "ఆపివేయి" + "ప్రస్తుతం ఎపిసోడ్‌లు ఏవీ ప్రసారంలో లేవు." "ఎపిసోడ్‌లు ఏవీ అందుబాటులో లేవు.\nఇవి అందుబాటులోకి వచ్చిన వెంటనే రికార్డ్ చేయబడతాయి." (%1$d నిమిషాలు) diff --git a/res/values-th/strings.xml b/res/values-th/strings.xml index 8424e9bb..86f4a317 100644 --- a/res/values-th/strings.xml +++ b/res/values-th/strings.xml @@ -20,9 +20,8 @@ "โมโน" "สเตอริโอ" "การควบคุมการเล่น" - "ช่องล่าสุด" + "ช่อง" "ตัวเลือกทีวี" - "ตัวเลือกของ PIP" "ไม่มีการควบคุมการเล่นสำหรับช่องนี้" "เล่นหรือหยุดชั่วคราว" "กรอไปข้างหน้า" @@ -35,33 +34,15 @@ "คำบรรยาย" "โหมดการแสดงผล" "PIP" - "เปิด" - "ปิด" "หลายเสียง" "รับชมช่องต่างๆ มากขึ้น" "การตั้งค่า" - "แหล่งที่มา" - "สลับ" - "เปิด" - "ปิด" - "เสียง" - "หลัก" - "หน้าต่าง PIP" - "การจัดวาง" - "ขวาล่าง" - "ขวาบน" - "ซ้ายบน" - "ซ้ายล่าง" - "แสดงคู่กัน" - "ขนาด" - "ใหญ่" - "เล็ก" - "แหล่งที่มาอินพุต" "TV (สายอากาศ/เคเบิล)" "ไม่มีข้อมูลโปรแกรม" "ไม่มีข้อมูล" "ช่องที่ถูกบล็อก" - "ภาษาที่ไม่รู้จัก" + "ภาษาที่ไม่รู้จัก" + "คำอธิบายภาพ %1$d" "คำบรรยาย" "ปิด" "กำหนดค่าการจัดรูปแบบ" @@ -135,6 +116,10 @@ "PIN ไม่ถูกต้อง ลองอีกครั้ง" "ลองอีกครั้ง PIN ไม่ตรงกัน" + "ป้อนรหัสไปรษณีย์ของคุณ" + "แอป Live TV จะใช้รหัสไปรษณีย์สำหรับการจัดส่งคู่มือรายการทีวีช่องต่างๆ ฉบับเต็ม" + "ป้อนรหัสไปรษณีย์ของคุณ" + "รหัสไปรษณีย์ไม่ถูกต้อง" "การตั้งค่า" "กำหนดค่ารายการช่อง" "เลือกช่องสำหรับคู่มือรายการทีวีของคุณ" @@ -143,6 +128,7 @@ "การควบคุมโดยผู้ปกครอง" "ใบอนุญาตโอเพนซอร์ส" "ใบอนุญาตโอเพนซอร์ส" + "ส่งความคิดเห็น" "เวอร์ชัน" "หากต้องการดูช่องนี้ ให้กดขวาและป้อน PIN" "หากต้องการดูโปรแกรมนี้ ให้กดขวาและป้อน PIN" @@ -181,8 +167,6 @@ "กด \"เลือก\""" เพื่อเข้าถึงเมนู TV" "ไม่พบอินพุต TV" "ไม่พบอินพุต TV" - "ไม่สนับสนุน PIP" - "ไม่มีอินพุตที่พร้อมใช้งานซึ่งสามารถแสดงด้วย PIP" "ประเภทตัวรับสัญญาณไม่เหมาะสม โปรดเปิดแอป Live TV สำหรับอินพุต TV ที่เป็นประเภทตัวรับสัญญาณ" "การรับสัญญาณล้มเหลว" "ไม่พบแอปสำหรับการทำงานนี้" @@ -259,8 +243,6 @@ "บันทึก" "การบันทึกครั้งเดียวมีความสำคัญสูงสุด" "ยกเลิก" - "ยกเลิก" - "ไม่จำ" "หยุด" "ดูกำหนดการบันทึก" "โปรแกรมนี้เท่านั้น" @@ -270,25 +252,29 @@ "บันทึกรายการนี้แทน" "ยกเลิกการบันทึกนี้" "ดูตอนนี้" + "ลบรายการที่บันทึกไว้…" "สามารถบันทึกได้" "กำหนดเวลาบันทึกแล้ว" "ตารางบันทึกชนกัน" "กำลังบันทึก" "การบันทึกล้มเหลว" "กำลังอ่านรายการเพื่อสร้างกำหนดเวลาการบันทึก" - "กำลังอ่านรายการ" - - + "กำลังอ่านรายการ" + "ดูการบันทึกล่าสุด" + "การบันทึก %1$s ไม่สมบูรณ์" + "การบันทึก %1$s และ %2$s ไม่สมบูรณ์" + "การบันทึก %1$s, %2$s และ %3$s ไม่สมบูรณ์" + "การบันทึก %1$s ไม่สมบูรณ์ เนื่องจากพื้นที่เก็บข้อมูลไม่เพียงพอ" + "การบันทึก %1$s และ %2$s ไม่สมบูรณ์ เนื่องจากพื้นที่เก็บข้อมูลไม่เพียงพอ" + "การบันทึก %1$s, %2$s และ %3$s ไม่สมบูรณ์ เนื่องจากพื้นที่เก็บข้อมูลไม่เพียงพอ" "DVR ต้องการพื้นที่เก็บข้อมูลเพิ่มเติม" "คุณจะสามารถบันทึกรายการด้วย DVR ได้ อย่างไรก็ตาม ตอนนี้อุปกรณ์ของคุณมีพื้นที่เก็บข้อมูลไม่เพียงพอสำหรับให้ DVR ทำงาน โปรดเชื่อมต่อไดรฟ์ภายนอกที่มีขนาด %1$s GB ขึ้นไป และทำตามขั้นตอนเพื่อฟอร์แมตไดรฟ์ให้เป็นพื้นที่เก็บข้อมูลของอุปกรณ์" + "พื้นที่เก็บข้อมูลไม่เพียงพอ" + "ไม่สามารถบันทึกรายการนี้ได้เนื่องจากพื้นที่เก็บข้อมูลไม่เพียงพอ โปรดลองลบบางรายการที่บันทึกไว้" "พื้นที่เก็บข้อมูลหายไป" - "พื้นที่เก็บข้อมูลบางส่วนที่ DVR ใช้หายไป โปรดเชื่อมต่อไดรฟ์ภายนอกที่คุณใช้ก่อนเปิดใช้ DVR อีกครั้ง หรือคุณอาจเลือกไม่จำพื้นที่เก็บข้อมูลหากพื้นที่เก็บข้อมูลไม่พร้อมใช้งานอีกต่อไป" - "ไม่จำพื้นที่เก็บข้อมูลใช่ไหม" - "เนื้อหาและกำหนดการทั้งหมดที่คุณบันทึกไว้จะหายไป" "หยุดบันทึกใช่ไหม" "ระบบจะเก็บเนื้อหาที่บันทึกไว้" - - + "การบันทึก %1$s จะหยุดลงเนื่องจากตารางบันทึกชนกับรายการนี้ เนื้อหาที่บันทึกแล้วจะได้รับการเก็บไว้" "กำหนดการบันทึกรายการชนกัน" "เริ่มบันทึกรายการแล้ว แต่กำหนดการชนกัน" "ระบบจะบันทึก %1$s" @@ -306,14 +292,27 @@ "มีกำหนดบันทึกรายการเดียวกันนี้แล้วเวลา %1$s" "บันทึกไว้แล้ว" "บันทึกรายการนี้ไว้แล้ว สามารถดูได้ที่ห้องสมุด DVR" - - - - - - - - + "กำหนดเวลาบันทึกซีรีส์แล้ว" + + กำหนดเวลาบันทึกแล้ว %1$d รายการสำหรับ %2$s + กำหนดเวลาบันทึกแล้ว %1$d รายการสำหรับ %2$s + + + กำหนดเวลาบันทึกแล้ว %1$d รายการสำหรับ %2$s %3$d รายการจะไม่มีการบันทึกเนื่องจากตารางบันทึกชนกัน + กำหนดเวลาบันทึกแล้ว %1$d รายการสำหรับ %2$s จะไม่มีการบันทึกเนื่องจากเนื่องจากตารางบันทึกชนกัน + + + กำหนดเวลาบันทึกแล้ว %1$d รายการสำหรับ %2$s ซีรีส์นี้และซีรีส์อื่น %3$d ตอนจะไม่มีการบันทึกเนื่องจากตารางบันทึกชนกัน + กำหนดเวลาบันทึกแล้ว %1$d รายการสำหรับ %2$s ซีรีส์นี้และซีรีส์อื่น %3$d ตอนจะไม่มีการบันทึกเนื่องจากตารางบันทึกชนกัน + + + กำหนดเวลาบันทึกแล้ว %1$d รายการสำหรับ %2$s ซีรีส์อื่น 1 ตอนจะไม่มีการบันทึกเนื่องจากตารางบันทึกชนกัน + กำหนดเวลาบันทึกแล้ว %1$d รายการสำหรับ %2$s ซีรีส์อื่น 1 ตอนจะไม่มีการบันทึกเนื่องจากตารางบันทึกชนกัน + + + กำหนดเวลาบันทึกแล้ว %1$d รายการสำหรับ %2$s ซีรีส์อื่น %3$d ตอนจะไม่มีการบันทึกเนื่องจากตารางบันทึกชนกัน + กำหนดเวลาบันทึกแล้ว %1$d รายการสำหรับ %2$s ซีรีส์อื่น %3$d ตอนจะไม่มีการบันทึกเนื่องจากตารางบันทึกชนกัน + "ไม่พบโปรแกรมที่บันทึกไว้" "การบันทึกที่เกี่ยวข้อง" "(ไม่มีรายละเอียดรายการ)" @@ -336,6 +335,7 @@ "หยุดบันทึกซีรีส์ไหม" "ตอนที่บันทึกไว้จะยังคงอยู่ในไลบรารี DVR" "หยุด" + "ไม่มีตอนใดออกอากาศในตอนนี้" "ไม่มีตอนที่พร้อมรับชม\nระบบจะเริ่มบันทึกเมื่อมีตอนที่พร้อมรับชม" (%1$d นาที) diff --git a/res/values-tl/strings.xml b/res/values-tl/strings.xml index a925aee6..bd944ba8 100644 --- a/res/values-tl/strings.xml +++ b/res/values-tl/strings.xml @@ -20,9 +20,8 @@ "mono" "stereo" "Mga kontrol sa Play" - "Kamakailang channel" + "Mga Channel" "Opsyon sa TV" - "Mga opsyon sa PIP" "Hindi available para sa channel na ito ang mga kontrol ng laro" "I-play o i-pause" "I-fast forward" @@ -35,33 +34,15 @@ "Closed captions" "Display mode" "PIP" - "Naka-on" - "Naka-off" "Multi-audio" "Higit pa channel" "Mga Setting" - "Pinagmulan" - "Pagpalitin" - "Naka-on" - "Naka-off" - "Tunog" - "Pangunahin" - "PIP window" - "Layout" - "Kanang ibaba" - "Kanang itaas" - "Kaliwang itaas" - "Kaliwang ibaba" - "Magkatabi" - "Laki" - "Malaki" - "Maliit" - "Pinagmulan ng input" "TV (antenna/cable)" "Walang impormasyon ng programa" "Walang impormasyon" "Naka-block na channel" - "Hindi kilalang wika" + "Hindi kilalang wika" + "Mga closed caption %1$d" "Mga closed caption" "Naka-off" "Customize formatting" @@ -135,6 +116,10 @@ "Mali ang PIN na iyon. Subukang muli." "Subukang muli, hindi tumutugma ang PIN" + "Ilagay ang iyong ZIP Code." + "Gagamitin ng app na Mga Live Channel ang ZIP Code upang magbigay ng kumpletong gabay sa programa para sa mga channel sa TV." + "Ilagay ang iyong ZIP Code" + "Di-wasto ang ZIP Code" "Mga Setting" "I-customize lista ng channel" "Pumili ng mga channel para sa gabay sa programa" @@ -143,6 +128,7 @@ "Mga kontrol ng magulang" "Mga open source na lisensya" "Mga lisensyang open source" + "Magpadala ng feedback" "Bersyon" "Upang mapanood ang channel na ito, pindutin ang Kanan at ilagay ang iyong PIN" "Upang mapanood ang programang ito, pindutin ang Kanan at ilagay ang iyong PIN" @@ -181,8 +167,6 @@ "Pindutin ang SELECT"" upang i-access ang menu ng TV." "Walang nahanap na TV input" "Hindi mahanap ang TV input" - "Hindi sinusuportahan ang PIP" - "Walang available na input na maipapakita sa PIP" "Hindi naaangkop ang uri ng tuner. Pakilunsad ang app na Mga Live na Channel para sa uri ng tuner na input ng TV." "Hindi na-tune" "Walang nakitang app na gagawa sa aksyong ito." @@ -259,8 +243,6 @@ "I-save" "Ang mga isang beses na pagre-record ang may pinakamataas na priyoridad" "Kanselahin" - "Kanselahin" - "Kalimutan" "Ihinto" "Tingnan, iskedyul ng recording" "Ang isang programang ito" @@ -270,25 +252,29 @@ "Ito na lang ang i-record" "Kanselahin ang pag-record na ito" "Panoorin ngayon" + "I-delete ang mga recording…" "Mare-record" "Nakaiskedyul ang recording" "May conflict sa pagre-record" "Nagre-record" "Hindi na-record" "Nagbabasa ng mga program upang makagawa ng mga iskedyul ng pagre-record" - "Binabasa ang mga programa" - - + "Binabasa ang mga programa" + "Tingnan ang mga kamakailang recording" + "Hindi natapos ang pag-record sa %1$s." + "Hindi natapos ang mga pag-record sa %1$s at %2$s." + "Hindi natapos ang mga pag-record sa %1$s, %2$s at %3$s." + "Hindi natapos ang pag-record sa %1$s dahil sa hindi sapat na storage." + "Hindi natapos ang mga pag-record sa %1$s at %2$s dahil sa hindi sapat na storage." + "Hindi natapos ang mga pag-record sa %1$s, %2$s at %3$s dahil sa hindi sapat na storage." "Kailangan ng DVR ng higit pang storage" "Makakapag-record ka ng mga program gamit ang DVR. Gayunpaman, walang sapat na storage sa iyong device ngayon upang gumana ang DVR. Mangyaring magkonekta ng external drive na %1$sGB o mas malaki at sundin ang mga hakbang upang i-format ito bilang storage ng device." + "Hindi sapat ang storage" + "Hindi mare-record ang program na ito dahil walang sapat na storage. Subukang mag-delete ng ilang dati nang recording." "Nawawala ang storage" - "May nawawalang bahagi ng storage na ginagamit ng DVR. Pakikonekta ang external na drive na ginamit mo dati upang muling i-enable ang DVR. O kaya, maaari mo ring piliing kalimutan ang storage kung hindi na ito available." - "Kalimutan ang storage?" - "Mawawala ang lahat ng na-record mong content at iskedyul." "Ihinto ang pagre-record?" "Mase-save ang na-record na content." - - + "Ihihinto na ang pag-record ng %1$s dahil kasabay ito ng palabas na ito. Ise-save ang na-record na content." "Naiskedyul na ang pagre-record ngunit may mga hindi pagkakatugma" "Nagsimula na ang pagre-record ngunit may mga hindi pagkakatugma" "Mare-record ang %1$s." @@ -306,14 +292,27 @@ "Naiskedyul na ang parehong programa na ma-record sa %1$s." "Na-record na" "Na-record na ang programang ito. Available ito sa DVR library." - - - - - - - - + "Naiskedyul na ang pag-record ng series" + + %1$d recording ang naiskedyul para sa %2$s. + %1$d na recording ang naiskedyul para sa %2$s. + + + %1$d recording ang naiskedyul para sa %2$s. Hindi mare-record ang %3$d sa mga ito dahil sa may mga nakaiskedyul na. + %1$d na recording ang naiskedyul para sa %2$s. Hindi mare-record ang %3$d sa mga ito dahil sa may mga nakaiskedyul na. + + + %1$d recording ang naiskedyul para sa %2$s. %3$d episode ng series na ito at ng iba pang series ang hindi mare-record dahil may mga nakaiskedyul na. + %1$d na recording ang naiskedyul para sa %2$s. %3$d na episode ng series na ito at ng iba pang series ang hindi mare-record dahil may mga nakaiskedyul na. + + + %1$d recording ang naiskedyul para sa %2$s. 1 episode ng iba pang series ang hindi mare-record dahil sa may mga nakaiskedyul na. + %1$d na recording ang naiskedyul para sa %2$s. 1 episode ng iba pang series ang hindi mare-record dahil sa may mga nakaiskedyul na. + + + %1$d recording ang naiskedyul para sa %2$s. %3$d episode ng iba pang series ang hindi mare-record dahil sa may mga nakaiskedyul na. + %1$d na recording ang naiskedyul para sa %2$s. %3$d na episode ng iba pang series ang hindi mare-record dahil sa may mga nakaiskedyul na. + "Hindi nakita ang na-record na programa." "Mga nauugnay na recording" "(Walang paglalarawan ng program)" @@ -336,6 +335,7 @@ "Gusto mo bang ihinto ang pagre-record ng mga serye?" "Mananatiling available sa library ng DVR ang mga na-record na episode." "Ihinto" + "Walang ipinapalabas na episode ngayon." "Walang available na mga episode.\nMare-record ang mga ito kapag available na ang mga ito." (%1$d minuto) diff --git a/res/values-tr/strings.xml b/res/values-tr/strings.xml index 8162fbfa..df1e3c70 100644 --- a/res/values-tr/strings.xml +++ b/res/values-tr/strings.xml @@ -20,9 +20,8 @@ "mono" "stereo" "Oynatma denetimleri" - "Son kanallar" + "Kanallar" "TV seçenekleri" - "PIP seçenekleri" "Bu kanal için oynatma denetimleri kullanılamıyor" "Oynat veya duraklat" "İleri sar" @@ -35,33 +34,15 @@ "Altyazılar" "Görüntü modu" "PIP" - "Açık" - "Kapalı" "Çoklu ses" "Daha çok kanal al" "Ayarlar" - "Kaynak" - "Değiştir" - "Açık" - "Kapalı" - "Ses" - "Ana" - "PIP penceresi" - "Düzen" - "Sağ alt" - "Sağ üst" - "Sol üst" - "Sol alt" - "Yan yana" - "Boyut" - "Büyük" - "Küçük" - "Giriş kaynağı" "TV (anten/kablo)" "Hiçbir program bilgisi yok" "Bilgi yok" "Engellenen kanal" - "Bilinmeyen dil" + "Bilinmeyen dil" + "Altyazılar %1$d" "Altyazılar" "Kapalı" "Biçimi özelleştir" @@ -135,6 +116,10 @@ "Girdiğiniz PIN hatalıydı. Tekrar deneyin." "Tekrar deneyin, PIN eşleşmiyor" + "Posta Kodunuzu girin." + "Canlı Kanallar uygulaması, TV kanalları için eksiksiz bir program rehberi sunmak üzere Posta Kodu\'nu kullanacaktır." + "Posta Kodunuzu girin" + "Geçersiz Posta Kodu" "Ayarlar" "Kanal listesini özelleştir" "Program rehberiniz için kanalları seçin" @@ -143,6 +128,7 @@ "Ebeveyn denetimleri" "Açık kaynak lisansları" "Açık kaynak lisansları" + "Geri bildirim gönder" "Sürüm" "Bu kanalı izlemek için Sağ tuşuna basın ve PIN\'inizi girin" "Bu programı izlemek için Sağ tuşuna basın ve PIN\'inizi girin" @@ -181,8 +167,6 @@ "TV menüsüne erişmek için ""SEÇ\'e basın""." "TV girişi bulunamadı" "TV girişi bulunamadı" - "PIP desteklenmiyor" - "PIP ile göstermeye uygun giriş yok" "Kanal tarayıcı türü uygun değil. TV girişi kanal tarayıcı türü için lütfen Canlı Yayın Kanalları uygulamasını başlatın." "Tarama işlemi başarısız oldu" "Bu işlemi gerçekleştirecek uygulama bulunamadı." @@ -259,8 +243,6 @@ "Kaydet" "Bir kerelik kayıtlar en yüksek önceliğe sahip" "İptal" - "İptal" - "Unut" "Durdur" "Kayıt programını görüntüle" "Yalnızca bu program" @@ -270,25 +252,29 @@ "Onun yerine bunu kaydet" "Bu kaydı iptal et" "Şimdi izle" + "Kayıtları sil…" "Kaydedilebilir" "Kayıt programlandı" "Kayıt çakışması" "Kaydediliyor" "Kaydedilemedi" "Kayıt planlarını oluşturmak için programlar okunuyor" - "Program bilgisi okunuyor" - - + "Program bilgisi okunuyor" + "Son kayıtları görüntüleyin" + "%1$s kaydı tam değil." + "%1$s ve %2$s kayıtları tam değil." + "%1$s, %2$s ve %3$s kayıtları tam değil." + "Kullanılabilir depolama alanı yeterli olmadığından %1$s kaydı tamamlanamadı." + "Kullanılabilir depolama alanı yeterli olmadığından %1$s ve %2$s kayıtları tamamlanamadı." + "Kullanılabilir depolama alanı yeterli olmadığından %1$s, %2$s ve %3$s kayıtları tamamlanamadı." "DVR için daha fazla depolama alanı gerekiyor" "Programları DVR ile kaydedebileceksiniz. Ancak şu anda cihazınızda DVR\'nin çalışması için yeterli miktarda boş depolama alanı yok. Lütfen %1$s GB veya daha büyük kapasiteye sahip harici sürücü bağlayın ve bu sürücüyü cihaz depolama alanı olarak biçimlendirmek için adımları uygulayın." + "Yeterli depolama alanı yok" + "Yeterli depolama alanı olmadığı için bu program kaydedilmeyecek. Mevcut kayıtlardan bazılarını silmeyi deneyin." "Kayıp depolama birimi" - "DVR tarafından kullanılan bazı depolama alanları kayıp. DVR\'yi yeniden etkinleştirmeden önce lütfen kullandığınız harici sürücüyü bağlayın. Ayrıca bu depolama alanı artık kullanılmıyorsa unutulmasını da seçebilirsiniz." - "Depolama alanı unutulsun mu?" - "Kaydedilen tüm içeriğiniz ve programlarınız kaybolacaktır." "Kayıt durudurulsun mu?" "Kaydedilen içerik saklanacak." - - + "%1$s kaydı, bu programla çakıştığından durdurulacak. Kaydedilen içerik saklanacak." "Kayıt programlandı, ancak çakışmalar var" "Kayıt işlemi başladı, ancak çakışmalar var" "%1$s kaydedilecek." @@ -306,14 +292,27 @@ "Aynı program şu tarihte ve saatte kaydedilecek şekilde zaten programlandı: %1$s." "Zaten kaydedildi" "Bu program zaten kaydedildi. DVR kitaplığında bulabilirsiniz." - - - - - - - - + "Dizi kaydı programlandı" + + %2$s için %1$d kayıt programlandı. + %2$s için %1$d kayıt programlandı. + + + %2$s için %1$d kayıt programlandı. Çakışma nedeniyle bu kayıtların %3$d tanesi yapılmayacak. + %2$s için %1$d kayıt programlandı. Çakışma nedeniyle bu kayıt yapılmayacak. + + + %2$s için %1$d kayıt programlandı. Çakışma nedeniyle bu dizinin ve diğer dizilerin %3$d bölümü kaydedilmeyecek. + %2$s için %1$d kayıt programlandı. Çakışma nedeniyle bu dizinin ve diğer dizilerin %3$d bölümü kaydedilmeyecek. + + + %2$s için %1$d kayıt programlandı. Çakışma nedeniyle diğer dizinin 1 bölümü kaydedilmeyecek. + %2$s için %1$d kayıt programlandı. Çakışma nedeniyle diğer dizinin 1 bölümü kaydedilmeyecek. + + + %2$s için %1$d kayıt programlandı. Çakışma nedeniyle diğer dizinin %3$d bölümü kaydedilmeyecek. + %2$s için %1$d kayıt programlandı. Çakışma nedeniyle diğer dizinin %3$d bölümü kaydedilmeyecek. + "Kaydedilen program bulunamadı." "İlgili kayıtlar" "(Program açıklaması yok)" @@ -336,6 +335,7 @@ "Dizinin kaydı durdurulsun mu?" "Kaydedilen bölümler DVR kitaplığında kalmaya devam edecektir." "Durdur" + "Şu anda yayınlanan bir bölüm yok." "Kaydedilebilecek bir bölüm yok.\nBölümler kullanıma sunulduğunda kaydedilecektir." (%1$d dakika) diff --git a/res/values-uk/strings.xml b/res/values-uk/strings.xml index b9d91a7a..e644fd02 100644 --- a/res/values-uk/strings.xml +++ b/res/values-uk/strings.xml @@ -20,9 +20,8 @@ "моно" "стерео" "Керування відтворенням" - "Останні канали" + "Канали" "Опції ТБ" - "Опції PIP" "Елементи керування відтворенням, яких немає в цьому каналі" "Відтворити або призупинити" "Перемотати вперед" @@ -35,33 +34,15 @@ "Субтитри" "Режим показу" "PIP" - "Увімкнено" - "Вимкнено" "Кілька аудіо" "Більше каналів" "Налаштування" - "Джерело" - "Заміна" - "Увімкнено" - "Вимкнено" - "Звук" - "Основне вікно" - "Вікно PIP" - "Схема" - "Унизу праворуч" - "Угорі праворуч" - "Угорі ліворуч" - "Унизу ліворуч" - "Поруч" - "Розмір" - "Великий розмір" - "Малий розмір" - "Джерело сигналу" "ТБ (ефірне або кабельне)" "Немає інформації про програму" "Немає інформації." "Заблокований канал" - "Невідома мова" + "Невідома мова" + "Субтитри (%1$d)" "Субтитри" "Вимкнути" "Налаштувати формат" @@ -139,6 +120,10 @@ "Цей PIN-код неправильний. Повторіть спробу." "PIN-коди не збігаються. Повторіть спробу" + "Введіть поштовий індекс." + "Додаток Телеканали показує повну програму телепередач на основі поштового індексу." + "Введіть поштовий індекс" + "Недійсний поштовий індекс" "Налаштування" "Налаштувати список каналів" "Вибрати канали для програми телепередач" @@ -147,6 +132,7 @@ "Батьківський контроль" "Ліцензії відкритого коду" "Ліцензії ПЗ з відкритим кодом" + "Надіслати відгук" "Версія" "Щоб дивитися цей канал, натисніть стрілку праворуч і введіть PIN-код" "Щоб дивитися цю телепередачу, натисніть стрілку праворуч і введіть PIN-код" @@ -189,8 +175,6 @@ "Натисніть \"ВИБРАТИ\""", щоб відкрити меню телевізора." "Не знайдено джерел вхідного телесигналу" "Не вдається знайти джерело вхідного телесигналу" - "PIP не підтримується" - "Немає джерел вхідного сигналу для PIP" "Тюнер не підтримується. Щоб використовувати тюнер, запустіть додаток Live TV." "Не вдалося налаштувати" "Не знайдено додатка для цієї дії." @@ -279,8 +263,6 @@ "Зберегти" "Одноразові записи мають найвищий пріоритет" "Скасувати" - "Скасувати" - "Забути" "Припинити" "Переглянути розклад запису" "Лише ця передача" @@ -290,25 +272,29 @@ "Натомість записати цю передачу" "Скасувати цей запис" "Дивитися" + "Видалити записи…" "Можна записати" "Запис заплановано" "Конфлікт запису" "Запис" "Не записано" "Читаються назви передач для створення розкладів" - "Читання даних програм" - - + "Читання інформації про передачі" + "Переглянути нещодавні записи" + "Запис передачі \"%1$s\" не завершено." + "Запис передач \"%1$s\" і \"%2$s\" не завершено." + "Запис передач \"%1$s\", \"%2$s\" і \"%3$s\" не завершено." + "Запис передачі \"%1$s\" не завершено. Замало місця." + "Запис передач \"%1$s\" і \"%2$s\" не завершено. Замало місця." + "Запис передач \"%1$s\", \"%2$s\" і \"%3$s\" не завершено. Замало місця." "Пристрою DVR потрібно більше пам’яті" "За допомогою пристрою DVR можна записувати програми, однак на ньому недостатньо пам’яті. Підключіть зовнішній диск ємністю %1$s Гб або більше та дотримуйтеся вказівок, щоб відформатувати його як пам’ять пристрою." + "Замало пам’яті" + "Замало пам’яті. Цю передачу не буде записано. Спробуйте видалити деякі наявні записи." "Немає пам’яті" - "Немає деякої пам’яті, яку використовує DVR. Щоб знову ввімкнути DVR, під’єднайте зовнішній диск, який ви використовували раніше. Можна також забути пам’ять, якщо вона недоступна." - "Забути пам’ять?" - "Увесь записаний вміст і розклади буде втрачено." "Припинити запис?" "Записаний вміст буде збережено." - - + "Запис серіалу \"%1$s\" буде зупинено через конфлікти з ним. Записаний вміст буде збережено." "Запис заплановано, однак є конфлікти" "Запис почався, однак є конфлікти" "Програму \"%1$s\" буде записано." @@ -328,14 +314,37 @@ "Цю передачу вже заплановано записати о %1$s" "Уже записано" "Цю передачу вже записано. Вона доступна в бібліотеці DVR." - - - - - - - - + "Заплановано запис серіалу" + + Заплановано %1$d запис серіалу \"%2$s\". + Заплановано %1$d записи серіалу \"%2$s\". + Заплановано %1$d записів серіалу \"%2$s\". + Заплановано %1$d запису серіалу \"%2$s\". + + + Заплановано %1$d запис серіалу \"%2$s\". Через конфлікти не буде записано %3$d із них. + Заплановано %1$d записи серіалу \"%2$s\". Через конфлікти не буде записано %3$d з них. + Заплановано %1$d записів серіалу \"%2$s\". Через конфлікти не буде записано %3$d із них. + Заплановано %1$d запису серіалу \"%2$s\". Через конфлікти не буде записано %3$d із них. + + + Заплановано %1$d запис серіалу \"%2$s\". Через конфлікти не буде записано стільки серій цього й іншого серіалів: %3$d. + Заплановано %1$d записи серіалу \"%2$s\". Через конфлікти не буде записано стільки серій цього й іншого серіалів: %3$d. + Заплановано %1$d записів серіалу \"%2$s\". Через конфлікти не буде записано стільки серій цього й іншого серіалів: %3$d. + Заплановано %1$d запису серіалу \"%2$s\". Через конфлікти не буде записано стільки серій цього й іншого серіалів: %3$d. + + + Заплановано %1$d запис серіалу \"%2$s\". Через конфлікти не буде записано 1 серію іншого серіалу. + Заплановано %1$d записи серіалу \"%2$s\". Через конфлікти не буде записано 1 серію іншого серіалу. + Заплановано %1$d записів серіалу \"%2$s\". Через конфлікти не буде записано 1 серію іншого серіалу. + Заплановано %1$d запису серіалу \"%2$s\". Через конфлікти не буде записано 1 серію іншого серіалу. + + + Заплановано %1$d запис серіалу \"%2$s\". Через конфлікти не буде записано стільки серій іншого серіалу: %3$d. + Заплановано %1$d записи серіалу \"%2$s\". Через конфлікти не буде записано стільки серій іншого серіалу: %3$d. + Заплановано %1$d записів серіалу \"%2$s\". Через конфлікти не буде записано стільки серій іншого серіалу: %3$d. + Заплановано %1$d запису серіалу \"%2$s\". Через конфлікти не буде записано стільки серій іншого серіалу: %3$d. + "Не вдалося знайти записані програми." "Пов’язані записи" "(Немає опису програми)" @@ -362,6 +371,7 @@ "Припинити запис серій?" "Записані серії можна переглянути в бібліотеці DVR." "Припинити" + "Зараз серії не транслюються." "Немає серій.\nСерії буде записано, щойно вони з’являться." (%1$d хвилина) diff --git a/res/values-ur-rPK/strings.xml b/res/values-ur-rPK/strings.xml index 47fcb2ad..e4674e9f 100644 --- a/res/values-ur-rPK/strings.xml +++ b/res/values-ur-rPK/strings.xml @@ -20,9 +20,8 @@ "مونو" "اسٹیریو" "پلے کنٹرولز" - "حالیہ چینلز" + "چینلز" "‏TV کے اختیارات" - "‏PIP کے اختیارات" "چلانے کے کنٹرولز اس چینل کیلئے غیر دستیاب ہیں" "چلائیں یا موقوف کریں" "تیزی سے فارورڈ کریں" @@ -35,33 +34,15 @@ "سب ٹائٹلز" "ڈسپلے وضع" "PIP" - "آن" - "آف" "کثیر آڈیو" "مزید چینلز حاصل کریں" "ترتیبات" - "ماخذ" - "تبادلہ کریں" - "آن" - "آف" - "آواز" - "مرکزی" - "‏PIP ونڈو" - "لے آؤٹ" - "نیچے دائیں" - "اوپری دائیں" - "اوپری بائیں" - "نیچے بائیں" - "سمت بہ سمت" - "سائز" - "بڑا" - "چھوٹا" - "ان پٹ ماخذ" "‏TV (اینٹینا/کیبل)" "پروگرام کی معلومات نہیں" "کوئی معلومات نہیں ہے" "مسدود چینل" - "نامعلوم زبان" + "نامعلوم زبان" + "‏سب ٹائٹلز ‎%1$d" "سب ٹائٹلز" "آف" "فارمیٹنگ کسٹمائز کریں" @@ -135,6 +116,10 @@ "‏وہ PIN غلط تھا۔ دوبارہ کوشش کریں۔" "‏دوبارہ کوشش کریں، PIN مماثل نہیں ہے" + "اپنا زپ کوڈ درج کریں۔" + "‏لائیو چینلز ایپ TV چینلز کیلئے ایک مکمل پروگرام گائیڈ فراہم کرنے کیلئے زپ کوڈ استعمال کرے گی۔" + "اپنا زپ کوڈ درج کریں" + "غلط زپ کوڈ" "ترتیبات" "چینل فہرست حسب ضرورت بنائیں" "اپنی پروگرام گائیڈ کیلئے چینلز منتخب کریں" @@ -143,6 +128,7 @@ "پیرنٹل کنٹرولز" "اوپن سورس لائسنسز" "اوپن سورس لائسنسز" + "تاثرات بھیجیں" "ورژن" "‏یہ چینل دیکھنے کیلئے Right دبائیں اور اپنا PIN درج کریں" "‏یہ پروگرام دیکھنے کیلئے Right دبائیں اور اپنا PIN درج کریں" @@ -181,8 +167,6 @@ "‏TV مینو تک رسائی حاصل کرنے کیلئے ""منتخب کریں کو دبائیں""۔" "‏کوئی TV ان پٹ نہیں ملا" "‏TV ان پٹ تلاش نہیں کیا جا سکتا" - "‏PIP تعاون یافتہ نہیں ہے" - "‏ایسا کوئی ان پٹ دستیاب نہیں ہے جسے PIP کے ساتھ دکھایا جا سکے" "‏ٹیونر کی قسم مناسب نہیں ہے۔ براہ کرم ٹیونر کی قسم والے TV ان پٹ کیلئے لائیو چینلز ایپ شروع کریں۔" "ٹیون کرنا ناکام ہوگیا" "اس کارروائی کو نمٹانے کیلئے کوئی ایپ نہیں ملی۔" @@ -259,8 +243,6 @@ "محفوظ کریں" "یک وقتی ریکارڈنگز کو سب سے اعلی ترجیح حاصل ہوتی ہے" "منسوخ کریں" - "منسوخ کریں" - "بھول جائیں" "روکیں" "ریکارڈنگ کا شیڈول ملاحظہ کریں" "یہ واحد پروگرام" @@ -270,25 +252,29 @@ "اس کی بجائے یہ ریکارڈ کریں" "اس ریکارڈنگ کو منسوخ کریں" "ابھی دیکھیں" + "ریکارڈنگز حذف کریں…" "قابل ریکارڈ" "ریکارڈنگ کا شیڈول" "ریکارڈنگ شیڈول میں تصادم" "ریکارڈ ہو رہا ہے" "ریکارڈنگ ناکام ہو گئی" "ریکارڈنگ کے شیڈولز بنانے کیلئے پروگرام پڑھے جا رہے ہیں" - "پروگرامز پڑھے جا رہے ہیں" - - + "پروگرامز پڑھے جا رہے ہیں" + "حالیہ ریکارڈنگز ملاحظہ کریں" + "%1$s کی ریکارڈنگ نامکمل ہے۔" + "%1$s اور %2$s کی ریکارڈنگز نامکمل ہیں۔" + "%1$s، %2$s اور %3$s کی ریکارڈنگز نامکمل ہیں۔" + "ناکافی اسٹوریج کی وجہ سے %1$s کی ریکارڈنگ مکمل نہیں ہوئی۔" + "ناکافی اسٹوریج کی وجہ سے %1$s اور %2$s کی ریکارڈنگز مکمل نہیں ہوئیں۔" + "ناکافی اسٹوریج کی وجہ سے %1$s، %2$s اور %3$s کی ریکارڈنگز مکمل نہیں ہوئیں۔" "‏DVR کو مزید اسٹوریج درکار ہے" "‏آپ DVR کے ساتھ پروگرامز ریکارڈ کر سکیں گے۔ تاہم آپ کے آلہ پر DVR کے کام کرنے کیلئے ابھی کافی اسٹوریج نہیں ہے۔ براہ کرم ایک بیرونی ڈرائیو منسلک کریں جو GB %1$s یا اس سے بڑی ہو اور اسے آلہ کی اسٹوریج کے بطور فارمیٹ کرنے کیلئے مراحل کی پیروی کریں۔" + "اسٹوریج کافی نہیں ہے" + "یہ پروگرام ریکارڈ نہیں ہوگا کیونکہ اسٹوریج کافی نہیں ہے۔ کچھ موجودہ ریکارڈنگز حذف کرنے کی کوشش کریں۔" "اسٹوریج غائب ہے" - "‏DVR کی استعمال کردہ کچھ اسٹوریج غائب ہے۔ براہ کرم وہ بیرونی ڈرائیو جو آپ نے پہلے DVR کو دوبارہ فعال کرنے کیلئے استعمال کی تھی، منسلک کریں۔ متبادل طور پر، اگر یہ مزید دستیاب نہ ہو تو آپ اسٹوریج کو بھولنے کا انتخاب کر سکتے ہیں۔" - "اسٹوریج کو بھول جائیں؟" - "آپ کا تمام ریکارڈ کردہ مواد اور شیڈولز ضائع ہو جائیں گے۔" "ریکارڈنگ روکیں؟" "ریکارڈ شدہ مواد محفوظ ہو جائے گا۔" - - + "%1$s کی ریکارڈنگ روک دی جائے گی کیونکہ یہ اس پروگرام کے ساتھ متضاد ہے۔ ریکارڈ کردہ مواد محفوظ ہو جائے گا۔" "ریکارڈنگ کا شیڈول لیکن تضادات" "ریکارڈنگ شروع ہو گئی ہے لیکن اس میں تضادات ہیں" "%1$s ریکارڈ ہوجائے گا۔" @@ -306,14 +292,27 @@ "اسی پروگرام کا پہلے ہی %1$s بجے ریکارڈ ہونے کیلئے شیڈول بنا ہوا ہے۔" "پہلے سے ریکارڈ شدہ" "‏یہ پروگرام پہلے سے ہی ریکارڈ شدہ ہے۔ یہ DVR لائبریری میں دستیاب ہے۔" - - - - - - - - + "سیریز کی ریکارڈنگ کا شیڈول بن گیا ہے" + + %2$s کیلئے %1$d ریکارڈنگز کا شیڈول بن گیا ہے۔ + %2$s کیلئے %1$d ریکارڈنگ کا شیڈول بن گیا ہے۔ + + + %2$s کیلئے %1$d ریکارڈنگز کا شیڈول بن گیا ہے۔ تضادات کی وجہ سے ان میں سے %3$d ریکارڈ نہیں ہوں گی۔ + %2$s کیلئے %1$d ریکارڈنگ کا شیڈول بن گیا ہے۔ تضادات کی وجہ سے اس کی ریکارڈنگ نہیں ہو گی۔ + + + %2$s کیلئے %1$d ریکارڈنگز کا شیڈول بن گیا ہے۔ تضادات کی وجہ سے اس سیریز اور دیگر سیریز کی %3$d اقساط ریکارڈ نہیں ہوں گی۔ + %2$s کیلئے %1$d ریکارڈنگ کا شیڈول بن گیا ہے۔ تضادات کی وجہ سے اس سیریز اور دیگر سیریز کی %3$d اقساط ریکارڈ نہیں ہوں گی۔ + + + %2$s کیلئے %1$d ریکارڈنگز کا شیڈول بن گیا ہے۔ تضادات کی وجہ سے دیگر سیریز کی 1 قسط ریکارڈ نہیں ہو گی۔ + %2$s کیلئے %1$d ریکارڈنگ کا شیڈول بن گیا ہے۔ تضادات کی وجہ سے دیگر سیریز کی 1 قسط ریکارڈ نہیں ہو گی۔ + + + %2$s کیلئے %1$d ریکارڈنگز کا شیڈول بن گیا ہے۔ تضادات کی وجہ سے دیگر سیریز کی %3$d اقساط ریکارڈ نہیں ہوں گی۔ + %2$s کیلئے %1$d ریکارڈنگ کا شیڈول بن گیا ہے۔ تضادات کی وجہ سے دیگر سیریز کی %3$d اقساط ریکارڈ نہیں ہوں گی۔ + "ریکارڈ شدہ پروگرام نہیں ملا۔" "متعلقہ ریکارڈنگز" "(پروگرام کی کوئی تفصیل نہیں)" @@ -336,6 +335,7 @@ "سیریز ریکارڈنگ روکیں؟" "‏ریکارڈ شدہ ایپی سوڈز DVR لائبریری میں دستیاب رہیں گے۔" "روکیں" + "ابھی کوئی ایپی سوڈ آن ائیر نہیں ہے۔" "کوئی ایپی سوڈز دستیاب نہیں۔\nایک بار دستیاب ہوجائے تو ان کو ریکارڈ کیا جائے گا۔" ‏(%1$d منٹ) diff --git a/res/values-uz-rUZ/strings.xml b/res/values-uz-rUZ/strings.xml index 55a0324f..d2c390b6 100644 --- a/res/values-uz-rUZ/strings.xml +++ b/res/values-uz-rUZ/strings.xml @@ -20,9 +20,8 @@ "mono" "stereo" "Boshqaruv" - "So‘nggi kanallar" + "Kanallar" "TV sozlamalari" - "PIP sozlamalari" "Ushbu kanal uchun ijro etish boshqaruvlari mavjud emas" "Ijro yoki pauza" "Oldinga tezkor o‘tkazish" @@ -35,33 +34,15 @@ "Taglavhalar" "Ekran rejimi" "Kadr ustida kadr" - "Yoniq" - "O‘chiq" "Ko‘p kanalli" "Boshqa kanallar" "Sozlamalar" - "Manba" - "Almashtirish" - "Yoniq" - "O‘chiq" - "Ovoz" - "Asosiy oyna" - "PIP oynasi" - "Joylashuvi" - "Pastda o‘ngda" - "Tepada o‘ngda" - "Tepada chapda" - "Pastda chapda" - "Yonma-yon" - "O‘lchami" - "Katta" - "Kichik" - "Kirish manbasi" "TV (antenna/kabel)" "Dastur haqida ma’l. yo‘q" "Ma’lumot yo‘q" "Bloklangan kanal" - "Noma’lum til" + "Noma’lum til" + "Taglavhalar %1$d" "Taglavhalar" "O‘chiq" "Formatlarni sozlash" @@ -97,7 +78,7 @@ "Yoniq" "O‘chiq" "Kanallar bloklandi" - "Barchasini bloklash" + "Hammasini bloklash" "Blokdan chiqarish" "Berkitilgan kanallar" "Yosh cheklovlari" @@ -135,6 +116,10 @@ "PIN-kod noto‘g‘ri, qaytadan urining." "Qaytadan urinib ko‘ring, PIN-kod noto‘g‘ri" + "Pochta indeksini kiriting." + "Jonli efir ilovasi TV kanallari uchun to‘liq teledasturlarni taqdim etish uchun pochta indeksidan foydalanadi." + "Pochta indeksini kiriting" + "Pochta indeksi xato" "Sozlamalar" "Ro‘yxatni sozlash" "Teledastur uchun kanallarni tanlash" @@ -143,6 +128,7 @@ "Ota-ona nazorati" "Ochiq kodli DT litsenziyalari" "Ochiq kodli DT litsenziyalari" + "Fikr-mulohaza yuborish" "Versiyasi" "Bu kanalni ko‘rish uchun o‘ngga qaragan chiziqni bosing va PIN kodni kiriting" "Bu dasturni ko‘rish uchun o‘ngga qaragan chiziqni bosing va PIN kodni kiriting" @@ -181,8 +167,6 @@ "TV menyusiga kirish uchun ""TANLASH tugmasini bosing""." "Hech qanday TV-kirish topilmadi" "TV-kirishni topib bo‘lmadi" - "PIP qo‘llab-quvvatlanmaydi" - "PIP bilan ko‘rsatish uchun hech qanday manba yo‘q" "Tyuner turi mos kelmaydi. Agar TV-kirishdan foydalanayotgan bo‘lsangiz, “Jonli efir” ilovasini ishga tushiring." "Sozlashni amalga oshirib bo‘lmadi." "Bu amalni bajara oladigan ilova topilmadi." @@ -259,8 +243,6 @@ "Saqlash" "Bir martalik yozuvlarning muhimlilik darajasi yuqori" "Bekor q-sh" - "Bekor qilish" - "O‘chirib tashlash" "To‘xtatish" "Yozib olish jadvalini ko‘rish" "Faqat bu dastur" @@ -270,25 +252,29 @@ "Bu dasturni yozib olish" "Bu yozib olishni bekor qilish" "Tomosha qilish" + "Yozuvlarni o‘chirish…" "Yozib olish mumkin" "Yozib olish rejalashtirildi" "Yozib olish taymerida ziddiyat" "Yozib olish" "Yozib olib bo‘lmadi" "Yozib olish jadvallarini yaratish uchun dasturlar o‘qib chiqilmoqda" - "Dasturlar o‘qib chiqilmoqda" - - + "Dasturlar o‘qib chiqilmoqda" + "Oxirgi yozuvlarni ko‘rish" + "“%1$s” to‘liq yozib olinmadi." + "“%1$s” va “%2$s” to‘liq yozib olinmadi." + "“%1$s”, “%2$s” va “%3$s” to‘liq yozib olinmadi." + "Xotirada joy kamligi tufayli “%1$s” to‘liq yozib olinmadi." + "Xotirada joy kamligi tufayli “%1$s” va “%2$s” to‘liq yozib olinmadi." + "Xotirada joy kamligi tufayli “%1$s”, “%2$s” va “%3$s” to‘liq yozib olinmadi." "DVR uchun ko‘proq joy kerak" "Dasturlarni DVR orqali yozib olish mumkin. Lekin hozir DVR ishlashi uchun qurilmangizda yetarli joy qolmadi. %1$s GB va undan katta hajmli tashqi xotira qurilmasini ulang va qurilma xotirasi sifatida formatlash uchun ko‘rsatmalarga amal qiling." + "Xotirada joy yetarli emas" + "Joy yetarli bo‘lmagani uchun bu dasturni yozib bo‘lmaydi. Eski yozuvlarni bir nechtasini o‘chirib tashlab ko‘ring." "Xotira mavjud emas" - "Xotira topilmadi. Videomagnitofonni qayta yoqishdan oldin tashqi xotirani ulang yoki undan foydalanib bo‘lmasa, xotirani o‘chirib tashlang." - "Xotira o‘chirib tashlansinmi?" - "Barcha yozib olingan kontentlar va jadvallar o‘chib ketadi." "Yozib olish to‘xtatilsinmi?" "Yozib olingan kontent saqlab qo‘yiladi." - - + "Bu dastur bilan ixtilof borligi uchun “%1$s” dasturini yozib olish to‘xtatiladi. Yozib olingan qismi saqlab olinadi." "Yozib olish rejalashtirildi, lekin ixtiloflar bor" "Yozib olish jadvalida ixtiloflar bor" "“%1$s” dasturi yozib olinadi." @@ -306,14 +292,27 @@ "Bu dasturni allaqachon %1$s da yozib olish rejalashtirilgan." "Dastur allaqachon yozib olingan" "Bu dastur allaqachon yozib olingan. U DVR kutubxonasida saqlanadi." - - - - - - - - + "Seriallarni yozib olish rejalashtirildi" + + %2$s uchun %1$d ta yozuv rejalashtirilgan. + %2$s uchun %1$d ta yozuv rejalashtirilgan. + + + %2$s uchun %1$d ta yozuv rejalashtirilgan. Ixtiloflar borligi uchun ulardan %3$d tasi yozib olinmaydi. + %2$s uchun %1$d ta yozuv rejalashtirilgan. Ixtiloflar borligi uchun u yozib olinmaydi. + + + %2$s uchun %1$d ta yozuv rejalashtirilgan. Ixtiloflar borligi uchun bu serialning %3$d ta qismi va boshqa seriallar yozib olinmaydi. + %2$s uchun %1$d ta yozuv rejalashtirilgan. Ixtiloflar borligi uchun bu serialning %3$d ta qismi va boshqa seriallar yozib olinmaydi. + + + %2$s uchun %1$d ta yozuv rejalashtirilgan. Ixtiloflar borligi uchun boshqa seriallarning 1 ta qismi yozib olinmaydi. + %2$s uchun %1$d ta yozuv rejalashtirilgan. Ixtiloflar borligi uchun boshqa seriallarning 1 ta qismi yozib olinmaydi. + + + %2$s uchun %1$d ta yozuv rejalashtirilgan. Ixtiloflar borligi uchun boshqa seriallarning %3$d ta qismi yozib olinmaydi. + %2$s uchun %1$d ta yozuv rejalashtirilgan. Ixtiloflar borligi uchun boshqa seriallarning %3$d ta qismi yozib olinmaydi. + "Yozib olingan dasturni topib bo‘lmadi." "Aloqador yozuvlar" "(Dastur haqida ma’lumot yo‘q)" @@ -336,6 +335,7 @@ "Yozib olish to‘xtatilsinmi?" "Yozib olingan qismlar DVR kutubxonasida saqlanadi." "To‘xtatish" + "Hozir hech qaysi serial qismi efirga uzatilmayapti." "Hali serial qismi chiqmagan.\nEfirga chiqishi bilan yozib olinadi." (%1$d daqiqa) diff --git a/res/values-v23/strings.xml b/res/values-v23/strings.xml deleted file mode 100644 index 4809682a..00000000 --- a/res/values-v23/strings.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - Channels - diff --git a/res/values-vi/strings.xml b/res/values-vi/strings.xml index a8c4ab09..9ed3df48 100644 --- a/res/values-vi/strings.xml +++ b/res/values-vi/strings.xml @@ -20,9 +20,8 @@ "đơn âm" "âm thanh nổi" "Điều khiển phát" - "Kênh gần đây" + "Kênh" "Tùy chọn TV" - "Tùy chọn PIP" "Điều khiển phát không có sẵn cho kênh này" "Phát hoặc tạm dừng" "Tua đi" @@ -35,33 +34,15 @@ "Phụ đề" "Chế độ hiển thị" "PIP" - "Bật" - "Tắt" "Âm thanh đa kênh" "Tải thêm kênh" "Cài đặt" - "Nguồn" - "Hoán đổi" - "Bật" - "Tắt" - "Âm thanh" - "Cửa sổ chính" - "Cửa sổ PIP" - "Bố cục" - "Phía dưới cùng bên phải" - "Phía trên cùng bên phải" - "Phía trên cùng bên trái" - "Phía dưới cùng bên trái" - "Cạnh nhau" - "Kích thước" - "Lớn" - "Nhỏ" - "Nguồn đầu vào" "TV (ăng-ten/cáp)" "Không có thông tin chương trình" "Không có thông tin" "Kênh bị chặn" - "Tiếng không xác định" + "Ngôn ngữ không xác định" + "Phụ đề chi tiết %1$d" "Phụ đề" "Tắt" "Tùy chỉnh định dạng" @@ -135,6 +116,10 @@ "Mã PIN đó không đúng. Hãy thử lại." "Hãy thử lại, mã PIN không khớp" + "Nhập mã ZIP của bạn." + "Ứng dụng Kênh trực tiếp sẽ sử dụng mã ZIP để cung cấp hướng dẫn chương trình đầy đủ cho các kênh truyền hình." + "Nhập mã ZIP của bạn" + "Mã ZIP không hợp lệ" "Cài đặt" "Tùy chỉnh danh sách kênh" "Chọn kênh cho hướng dẫn chương trình của bạn" @@ -143,6 +128,7 @@ "Kiểm soát của phụ huynh" "Giấy phép nguồn mở" "Giấy phép nguồn mở" + "Gửi phản hồi" "Phiên bản" "Để xem kênh này, hãy nhấn vào Quyền và nhập mã PIN của bạn" "Để xem chương trình này, hãy nhấn vào Quyền và nhập mã PIN của bạn" @@ -181,8 +167,6 @@ "Nhấn CHỌN"" để truy cập menu TV." "Không tìm thấy đầu vào TV nào" "Không tìm thấy đầu vào TV" - "PIP không được hỗ trợ" - "Không có đầu vào có thể hiển thị với PIP" "Loại bộ dò ko phù hợp. Hãy chạy ứng dụng Kênh trực tiếp cho đầu vào TV loại bộ dò." "Không dò được" "Không tìm thấy ứng dụng nào để xử lý tác vụ này." @@ -259,8 +243,6 @@ "Lưu" "Ghi một lần có mức độ ưu tiên cao nhất" "Hủy" - "Hủy" - "Quên" "Dừng" "Xem lịch ghi" "Chương trình duy nhất này" @@ -270,25 +252,29 @@ "Ghi chương trình này để thay thế" "Hủy lịch ghi này" "Xem ngay bây giờ" + "Xóa bản ghi…" "Có thể ghi được" "Đã lên lịch ghi" "Lịch ghi xung đột" "Đang ghi" "Ghi không thành công" "Đang đọc các chương trình để tạo lịch ghi" - "Đang đọc chương trình" - - + "Đang đọc chương trình" + "Xem bản ghi gần đây" + "Quá trình ghi %1$s không hoàn thành." + "Quá trình ghi %1$s%2$s không hoàn thành." + "Quá trình ghi %1$s, %2$s%3$s không hoàn thành." + "Quá trình ghi %1$s đã không hoàn thành do không đủ bộ nhớ." + "Quá trình ghi %1$s%2$s đã không hoàn thành do không đủ bộ nhớ." + "Quá trình ghi %1$s, %2$s%3$s đã không hoàn thành do không đủ bộ nhớ." "DVR cần thêm bộ nhớ" "Bạn sẽ có thể ghi các chương trình với DVR. Tuy nhiên không có đủ bộ nhớ trên thiết bị của bạn bây giờ để DVR hoạt động. Vui lòng kết nối ổ đĩa ngoài %1$sGB hoặc lớn hơn và làm theo các bước để định dạng ổ đĩa ngoài làm thiết bị lưu trữ." + "Không đủ bộ nhớ" + "Chương trình này sẽ không được ghi vì không có đủ bộ nhớ. Hãy thử xóa một số bản ghi hiện có." "Thiếu bộ nhớ" - "Một số bộ nhớ được DVR sử dụng bị thiếu. Vui lòng kết nối các ổ đĩa ngoài bạn đã sử dụng trước đó để bật lại DVR. Ngoài ra bạn còn có thể chọn để quên bộ nhớ nếu bộ nhớ không còn nữa." - "Quên bộ nhớ?" - "Tất cả nội dung đã ghi và lịch ghi của bạn sẽ bị mất." "Dừng ghi?" "Nội dung đã ghi sẽ được lưu." - - + "Quá trình ghi %1$s sẽ bị dừng lại vì mâu thuẫn với chương trình này. Nội dung đã ghi sẽ được lưu." "Đã lên lịch ghi nhưng có xung đột" "Đã bắt đầu ghi nhưng có xung đột" "%1$s sẽ được ghi." @@ -306,14 +292,27 @@ "Chương trình tương tự đã được lên lịch để ghi lúc %1$s." "Đã được ghi" "Chương trình này đã được ghi. Chương trình có sẵn trong thư viện DVR." - - - - - - - - + "Đã lên lịch ghi loạt phim" + + %1$d bản ghi đã được lên lịch cho %2$s. + %1$d bản ghi đã được lên lịch cho %2$s. + + + %1$d bản ghi đã được lên lịch cho %2$s. %3$d trong số chúng sẽ không được ghi do xung đột. + %1$d bản ghi đã được lên lịch cho %2$s. Bản ghi sẽ không được ghi do xung đột. + + + %1$d bản ghi đã được lên lịch cho %2$s. %3$d tập của loạt phim này và các loạt phim khác sẽ không được ghi do xung đột. + %1$d bản ghi đã được lên lịch cho %2$s. %3$d tập của loạt phim này và các loạt phim khác sẽ không được ghi do xung đột. + + + %1$d bản ghi đã được lên lịch cho %2$s. 1 tập của loạt phim khác sẽ không được ghi do xung đột. + %1$d bản ghi đã được lên lịch cho %2$s. 1 tập của loạt phim khác sẽ không được ghi do xung đột. + + + %1$d bản ghi đã được lên lịch cho %2$s. %3$d tập của loạt phim khác sẽ không được ghi do xung đột. + %1$d bản ghi đã được lên lịch cho %2$s. %3$d tập của loạt phim khác sẽ không được ghi do xung đột. + "Không tìm thấy chương trình nào được ghi." "Bản ghi liên quan" "(Không có mô tả chương trình)" @@ -336,6 +335,7 @@ "Dừng ghi loạt phim?" "Các tập đã ghi sẽ vẫn có sẵn trong thư viện DVR." "Dừng" + "Không có tập nào đang chiếu bây giờ." "Không có sẵn tập nào.\nChúng sẽ được ghi khi có sẵn." (%1$d phút) diff --git a/res/values-zh-rCN/strings.xml b/res/values-zh-rCN/strings.xml index e3d8ea08..752419d6 100644 --- a/res/values-zh-rCN/strings.xml +++ b/res/values-zh-rCN/strings.xml @@ -20,9 +20,8 @@ "单声道" "立体声" "播放控件" - "最近观看的频道" + "频道" "电视选项" - "PIP 选项" "此频道无法使用播放控件" "播放或暂停" "快进" @@ -35,33 +34,15 @@ "字幕" "显示模式" "PIP" - "开启" - "关闭" "多音频" "获取更多频道" "设置" - "来源" - "切换" - "开启" - "关闭" - "声音" - "主窗口" - "PIP 窗口" - "布局" - "右下方" - "右上方" - "左上方" - "左下方" - "并排" - "尺寸" - "大" - "小" - "输入源" "电视(天线/有线)" "没有节目信息" "无信息" "已屏蔽的频道" - "未知语言" + "未知语言" + "字幕(%1$d)" "字幕" "关闭" "自定义格式" @@ -135,6 +116,10 @@ "PIN码不正确,请重试。" "PIN码不匹配,请重试" + "请输入您的邮政编码。" + "直播频道应用将使用邮政编码提供完整的电视频道收视指南。" + "请输入您的邮政编码" + "邮政编码无效" "设置" "自定义频道列表" "为您的收视指南选择频道" @@ -143,6 +128,7 @@ "家长控制" "开放源代码许可" "开放源代码许可" + "发送反馈" "版本" "要观看此频道,请按“向右”按钮,然后输入您的PIN码" "要观看此节目,请按“向右”按钮,然后输入您的PIN码" @@ -181,8 +167,6 @@ "按“选择”""可访问电视菜单。" "未检测到电视输入设备" "找不到该电视输入设备" - "不支持 PIP" - "没有可通过 PIP 方式显示的输入" "不支持调谐器类型。请启动支持调谐器类型电视输入端口的“直播频道”应用。" "调谐失败" "未找到可处理此操作的应用。" @@ -259,8 +243,6 @@ "保存" "一次性录制内容具有最高优先级" "取消" - "取消" - "移除" "停止" "查看录制时间表" "只录这一集节目" @@ -270,25 +252,29 @@ "改录这个节目" "取消这项录制安排" "立即观看" + "删除录制的节目…" "可录制" "已排定录制时间" "录制冲突" "正在录制" "录制失败" "正在读取节目以创建录制时间安排表" - "正在读取节目" - - + "正在读取节目" + "查看最近的录制内容" + "%1$s的录制没有完成。" + "%1$s%2$s的录制没有完成。" + "%1$s%2$s%3$s的录制没有完成。" + "由于存储空间不足,%1$s的录制没有完成。" + "由于存储空间不足,%1$s%2$s的录制没有完成。" + "由于存储空间不足,%1$s%2$s%3$s的录制没有完成。" "DVR 需要更多存储空间" "您将可以使用 DVR 录制节目,但目前您设备上的存储空间不足,因此无法使用 DVR。请连接存储空间不小于 %1$sGB 的外部驱动器,然后按照相关步骤将其格式化为设备的存储空间。" + "存储空间不足" + "由于存储空间不足,系统将不会录制此节目。请尝试删除部分已录制的节目。" "无法访问存储空间" - "无法访问 DVR 使用的部分存储空间。请连接您先前使用的外部驱动器,以重新启用 DVR。如果存储空间已无法再使用,您也可以选择忽略该存储空间。" - "要移除此存储空间吗?" - "您的所有录制内容和录制安排计划都将丢失。" "要停止录制吗?" "系统将保存已录制的内容。" - - + "系统将停止录制《%1$s》,因为它与此节目存在冲突。系统将保存已录制的内容。" "已排定录制时间,但录制时间存在冲突" "已开始录制,但存在冲突" "系统将会录制《%1$s》。" @@ -306,14 +292,27 @@ "已安排在以下时间录制同一节目:%1$s。" "已录制" "此节目已完成录制并保存在 DVR 媒体库中。" - - - - - - - - + "已排定剧集录制时间" + + 已为《%2$s》排定 %1$d 项录制计划。 + 已为《%2$s》排定 %1$d 项录制计划。 + + + 已为《%2$s》排定 %1$d 项录制计划。由于存在冲突,系统将无法录制其中 %3$d 集节目。 + 已为《%2$s》排定 %1$d 项录制计划。由于存在冲突,系统将无法录制这集节目。 + + + 已为《%2$s》排定 %1$d 项录制计划。由于存在冲突,系统将无法录制此剧集和其他剧集的 %3$d 集节目。 + 已为《%2$s》排定 %1$d 项录制计划。由于存在冲突,系统将无法录制此剧集和其他剧集的 %3$d 集节目。 + + + 已为《%2$s》排定 %1$d 项录制计划。由于存在冲突,系统将无法录制其他剧集的 1 集节目。 + 已为《%2$s》排定 %1$d 项录制计划。由于存在冲突,系统将无法录制其他剧集的 1 集节目。 + + + 已为《%2$s》排定 %1$d 项录制计划。由于存在冲突,系统将无法录制其他剧集的 %3$d 集节目。 + 已为《%2$s》排定 %1$d 项录制计划。由于存在冲突,系统将无法录制其他剧集的 %3$d 集节目。 + "未找到录制的节目。" "相关录制内容" "(没有节目说明)" @@ -336,6 +335,7 @@ "要停止创建剧集录制时间表吗?" "已录制的剧集仍会保存到 DVR 媒体库中。" "停止" + "目前没有任何正在播出的剧集。" "没有已录制的剧集。\n一旦有剧集可供录制,系统将立即开始录制。" (%1$d 分钟) diff --git a/res/values-zh-rHK/strings.xml b/res/values-zh-rHK/strings.xml index 5fe984d5..8748b65d 100644 --- a/res/values-zh-rHK/strings.xml +++ b/res/values-zh-rHK/strings.xml @@ -20,9 +20,8 @@ "單聲道" "立體聲" "播放控制" - "最近觀看的頻道" + "頻道" "電視選項" - "PIP 選項" "播放控制功能不適用於此頻道" "播放或暫停" "向前快轉" @@ -35,33 +34,15 @@ "隱藏式字幕" "顯示模式" "子母畫面" - "開啟" - "關閉" "多聲道" "取得更多頻道" "設定" - "來源" - "切換" - "開啟" - "關閉" - "音效" - "主視窗" - "子母畫面視窗" - "版面配置" - "右下方" - "右上方" - "左上方" - "左下方" - "並排" - "大小" - "大" - "小" - "輸入源" "電視 (天線/連接線)" "沒有節目資訊" "無資訊" "已封鎖的頻道" - "不明語言" + "不明語言" + "隱藏式字幕 (%1$d)" "隱藏式字幕" "關閉" "自訂格式" @@ -135,6 +116,10 @@ "該 PIN 錯誤,請再試一次。" "PIN 碼不符,請再試一次" + "請輸入郵遞區號。" + "「直播頻道」應用程式會利用郵遞區號,提供電視頻道的完整電視節目指南。" + "請輸入郵遞區號" + "郵遞區號無效" "設定" "自訂頻道清單" "選擇頻道以建立電視節目指南" @@ -143,6 +128,7 @@ "家長監控設定" "開放原始碼授權" "開放原始碼授權" + "傳送意見" "版本" "按向右鍵並輸入您的 PIN,以觀看這個頻道" "按向右鍵並輸入您的 PIN,以觀看這個節目" @@ -181,8 +167,6 @@ "按 [選擇]"" 前往 [電視選單]。" "找不到電視輸入" "找不到電視輸入" - "PIP 不受支援" - "沒有可供 PIP 顯示的輸入" "調諧器類型不適用;請啟動調諧器類型電視輸入專用的「直播頻道」應用程式。" "調校失敗" "找不到可以處理這項操作的應用程式。" @@ -259,8 +243,6 @@ "儲存" "單次錄影享有最高優先級別" "取消" - "取消" - "刪除" "停止" "查看錄影時間表" "只錄影這一集" @@ -270,25 +252,29 @@ "改為錄影此節目" "取消此錄影" "立即觀看" + "刪除錄影節目…" "可錄影" "已排定錄影時間" "錄影時間有衝突" "正在錄影" "錄影失敗" "正在讀取節目以建立錄影時間表" - "正在讀取節目" - - + "正在讀取節目" + "查看最近的錄影" + "《%1$s》未完成錄影。" + "《%1$s》和《%2$s》未完成錄影。" + "《%1$s》、《%2$s》和《%3$s》未完成錄影。" + "由於儲存空間不足,因此《%1$s》未完成錄影。" + "由於儲存空間不足,因此《%1$s》和《%2$s》未完成錄影。" + "由於儲存空間不足,因此《%1$s》、《%2$s》和《%3$s》未完成錄影。" "DVR 需要更多儲存空間" "您可以使用 DVR 錄影節目,但裝置目前的儲存空間不足,因此無法使用 DVR。請連接 %1$sGB 或以上的外置硬碟,然後按步驟格式化,以用作裝置儲存空間。" + "儲存空間不足" + "由於儲存空間不足,因此無法錄影此節目。請嘗試刪除部分現有的錄影節目。" "無法存取儲存空間" - "系統存取部分 DVR 使用的儲存空間。請連接您先前使用的外置磁碟,然後重新啟用 DVR。如果儲存空間已無法使用,您亦可選擇刪除儲存空間。" - "要刪除儲存空間嗎?" - "所有已錄影的內容和時間表將會遺失。" "要停止錄影嗎?" "系統將儲存已錄影的內容。" - - + "由於《%1$s》的錄影時間與此節目有衝突,因此系統將停止錄影並儲存已錄影的內容。" "已預定錄影時間,但與錄影時間有衝突" "已開始錄影,但與其他預定錄影時間有衝突" "系統將錄影《%1$s》。" @@ -306,14 +292,27 @@ "已預定在 %1$s錄影相同的節目。" "已錄影" "系統已錄影此節目並儲存在 DVR 媒體庫中。" - - - - - - - - + "已預定劇集錄影時間" + + 已為《%2$s》預定 %1$d 個錄影時間。 + 已為《%2$s》預定 %1$d 個錄影時間。 + + + 已為《%2$s》預定 %1$d 個錄影時間。由於發生衝突,因此系統將不會錄影其中 %3$d 集節目。 + 已為《%2$s》預定 %1$d 個錄影時間。由於發生衝突,因此系統將不會錄影該節目。 + + + 已為《%2$s》預定 %1$d 個錄影時間。由於發生衝突,因此系統將不會錄影此劇集和其他劇集的 %3$d 集節目。 + 已為《%2$s》預定 %1$d 個錄影時間。由於發生衝突,因此系統將不會錄影此劇集和其他劇集的 %3$d 集節目。 + + + 已為《%2$s》預定 %1$d 個錄影時間。由於發生衝突,因此系統將不會錄影其他劇集的 1 集節目。 + 已為《%2$s》預定 %1$d 個錄影時間。由於發生衝突,因此系統將不會錄影其他劇集的 1 集節目。 + + + 已為《%2$s》預定 %1$d 個錄影時間。由於發生衝突,因此系統將不會錄影其他劇集的 %3$d 集節目。 + 已為《%2$s》預定 %1$d 個錄影時間。由於發生衝突,因此系統將不會錄影其他劇集的 %3$d 集節目。 + "找不到已錄影的節目。" "相關錄影" "(沒有節目說明)" @@ -336,6 +335,7 @@ "要停止錄影劇集嗎?" "已錄影的劇集仍將保留在 DVR 媒體庫中。" "停止" + "目前沒有任何正在播出的劇集。" "沒有可供錄影的劇集。\n系統會在劇集播出時立即錄影。" (%1$d 分鐘) diff --git a/res/values-zh-rTW/strings.xml b/res/values-zh-rTW/strings.xml index e33998ba..460f442b 100644 --- a/res/values-zh-rTW/strings.xml +++ b/res/values-zh-rTW/strings.xml @@ -20,9 +20,8 @@ "單聲道" "立體聲" "播放控制介面" - "最近觀看的頻道" + "頻道" "電視選項" - "子母畫面選項" "這個頻道無法使用播放控制介面" "播放或暫停" "快轉" @@ -35,33 +34,15 @@ "字幕" "顯示模式" "子母畫面" - "開啟" - "關閉" "多聲道" "取得更多頻道" "設定" - "來源" - "切換" - "開啟" - "關閉" - "音效" - "主視窗" - "子母畫面視窗" - "版面配置" - "右下角" - "右上角" - "左上角" - "左下角" - "並排" - "尺寸" - "大" - "小" - "輸入來源" "電視 (有線/無線)" "沒有節目資訊" "無資訊" "封鎖的頻道" - "不明語言" + "不明語言" + "隱藏式輔助字幕 (%1$d)" "字幕" "關閉" "自訂格式設定" @@ -135,6 +116,10 @@ "該 PIN 錯誤,請再試一次。" "PIN 碼不符,請再試一次" + "請輸入你的郵遞區號。" + "直播頻道應用程式會利用郵遞區號提供完整的電視頻道節目指南。" + "請輸入你的郵遞區號" + "無效的郵遞區號" "設定" "自訂頻道清單" "為您的節目指南選擇頻道" @@ -143,6 +128,7 @@ "家長監護" "開放原始碼授權" "開放原始碼授權" + "提供意見" "版本" "如要觀看這個頻道,請按向右鍵並輸入您的 PIN" "如要觀看這個節目,請按向右鍵並輸入您的 PIN" @@ -181,8 +167,6 @@ "按 [選取]"" 即可使用 [電視選單]。" "找不到電視輸入裝置" "找不到電視輸入裝置" - "不支援子母畫面" - "沒有可透過子母畫面顯示的輸入來源" "協調器類型不適合;請啟動協調器類型電視輸入裝置專用的 Live TV 應用程式。" "協調失敗" "找不到可以處理這個動作的應用程式。" @@ -259,8 +243,6 @@ "儲存" "只排定錄製一次的項目優先順序最高" "取消" - "取消" - "移除" "停止" "查看錄影時間表" "只錄這一集" @@ -270,25 +252,29 @@ "改錄這個節目" "取消這項錄影預約" "立即觀看" + "刪除錄影檔案…" "可錄影" "已排定錄影時間" "錄影衝突" "錄製中" "錄製失敗" "正在讀取節目以建立錄影時間表" - "正在讀取節目" - - + "正在讀取節目資訊" + "查看最近的錄製項目" + "《%1$s》未錄製完成。" + "《%1$s》和《%2$s》未錄製完成。" + "《%1$s》、《%2$s》和《%3$s》未錄製完成。" + "儲存空間不足,因此《%1$s》未錄製完成。" + "儲存空間不足,因此《%1$s》和《%2$s》未錄製完成。" + "儲存空間不足,因此《%1$s》、《%2$s》和《%3$s》未錄製完成。" "DVR 需要更多儲存空間" "你可以利用 DVR 錄製節目,但目前裝置儲存空間不足,因此無法使用 DVR。請連接 %1$sGB 以上的外接式磁碟,然後按照相關步驟將該磁碟格式化為裝置儲存空間。" + "儲存空間不足" + "儲存空間不足,因此無法錄製這個節目。請嘗試刪除一些現有的錄影檔案。" "無法存取儲存空間" - "無法存取部分 DVR 使用的儲存空間。請連接你先前使用的外接式磁碟以重新啟用 DVR。如果儲存空間已無法使用,你也可以選擇移除儲存空間。" - "要移除儲存空間嗎?" - "所有錄製內容和錄影時間表都不會保存下來。" "要停止錄影嗎?" "系統將儲存已錄製的內容。" - - + "由於「%1$s」與這個節目發生衝突,因此系統將停止錄製。已錄製的內容會保留下來。" "已排定錄影時間,但錄影時間發生衝突" "已開始錄影,但發生衝突" "系統將會錄製《%1$s》。" @@ -306,14 +292,27 @@ "同一個節目已預約在 %1$s 錄影。" "已錄影" "這個節目已完成錄影並儲存在 DVR 媒體庫中。" - - - - - - - - + "已排定影集錄製時間" + + 已為《%2$s》排定 %1$d 個錄影時間。 + 已為《%2$s》排定 %1$d 個錄影時間。 + + + 已為《%2$s》排定 %1$d 個錄影時間。由於發生時間衝突,因此系統將不會錄製其中 %3$d 集節目。 + 已為《%2$s》排定 %1$d 個錄影時間。由於發生時間衝突,因此系統將不會錄製這集節目。 + + + 已為《%2$s》排定 %1$d 個錄影時間。由於發生時間衝突,因此系統將不會錄製這個影集和其他影集的 %3$d 集節目。 + 已為《%2$s》排定 %1$d 個錄影時間。由於發生時間衝突,因此系統將不會錄製這個影集和其他影集的 %3$d 集節目。 + + + 已為《%2$s》排定 %1$d 個錄影時間。由於發生時間衝突,因此系統將不會錄製其他影集的 1 集節目。 + 已為《%2$s》排定 %1$d 個錄影時間。由於發生時間衝突,因此系統將不會錄製其他影集的 1 集節目。 + + + 已為《%2$s》排定 %1$d 個錄影時間。由於發生時間衝突,因此系統將不會錄製其他影集的 %3$d 集節目。 + 已為《%2$s》排定 %1$d 個錄影時間。由於發生時間衝突,因此系統將不會錄製其他影集的 %3$d 集節目。 + "找不到錄製的節目。" "相關錄影" "(無節目說明)" @@ -336,6 +335,7 @@ "要停止建立影集錄製時間表嗎?" "錄製完畢的集數會保存在 DVR 媒體庫中。" "停止" + "目前未播出任何劇集。" "目前沒有可供觀看的集數。\n系統會在節目播出時立即錄影。" (%1$d 分鐘) diff --git a/res/values-zu/strings.xml b/res/values-zu/strings.xml index ff1cfb3d..7edc27e4 100644 --- a/res/values-zu/strings.xml +++ b/res/values-zu/strings.xml @@ -20,9 +20,8 @@ "i-mono" "i-stereo" "Izilawuli zokudlala" - "Iziteshi zakamuva" + "Iziteshi" "Izinketho ze-TV" - "Izinketho ze-PIP" "Izilawuli zokudlala azitholakali kulesi siteshi" "Dlala noma umise isikhashana" "Ukudlulisa ngokushesha" @@ -35,33 +34,15 @@ "Amazwibela avaliwe" "Imodi yesibonisi" "I-PIP" - "Vuliwe" - "Valiwe" "Umsindo omningi" "Thola iziteshi eziningi" "Izilungiselelo" - "Umthombo" - "Shintsha" - "Vuliwe" - "Valiwe" - "Umsindo" - "Isisekelo" - "Iwindil le-PIP" - "Isakhiwo" - "Phansi ngakwesokudla" - "Phezulu ngakwesokudla" - "Phezulu ngakwesokunxele" - "Phansi ngakwesokunxele" - "ingxenye nenxenye" - "Usayizi" - "Kukhulu" - "Kuncane" - "Umthombo wokufaka" "I-TV (I-antenna/Intambo)" "Alukho ulwazi lohlelo" "Alukho ulwazi" "Isiteshi esivinjiwe" - "Ulimi olungaziwa" + "Ulimi olungaziwa" + "Imibhalo engezansi evaliwe engu-%1$d" "Amazwibela avaliwe" "Valiwe" "Yenza ngokwezifiso ukufometha" @@ -135,6 +116,10 @@ "Leyo phini ayilungile. Zama futhi." "Zama futhi, iphinikhodi ayifani" + "Faka ikhodi yakho ye-ZIP." + "Uhlelo lokusebenza lweziteshi ezibukhoma luzosebenzisa ikhodi ye-ZIP ukuze lunikeze umhlahlandlela wohlelo ophelele weziteshi ze-TV." + "Faka ikhodi yakho ye-ZIP" + "Ikhodi ye-ZIP engavumelekile" "Izilungiselelo" "Yenza ngezifiso uhlu lweziteshi" "Khetha iziteshi zomhlahlandlela wohlelo lwakho" @@ -143,6 +128,7 @@ "Ukulawula kwabazali" "Amalayisense womthombo ovulekile" "Amalayisense womthombo ovulekile" + "Thumela impendulo" "Inguqulo" "Ukuze ubuke lesi siteshi, cindezela Kwesokudla uphinde ufake i-PIN yakho" "Ukuze ubuke lolu hlelo, cindezela Kwesokudla uphinde ufake i-PIN yakho" @@ -181,8 +167,6 @@ "Cindezela okuthi KHETHA"" ukuze ufinyelele imenyu ye-TV." "Akukho kokufaka kwe-TV okutholakele" "Ayikwazi ukuthola kokufaka kwe-TV" - "I-PIP ayisekelwe" - "Akutholakali okokufaka okungaboniswa nge-PIP" "uhlobo le-Tuner alufanelekile; Sicela uqalise uhlelo lokusebenza leziteshi ezibukhoma lokufakwayo kwe-TV yohlobo le-tuner." "Ukushuna kwehlulekile" "Alukho uhlelo lokusebenza olutholakalele ukuphatha lesi senzo." @@ -259,8 +243,6 @@ "Londoloza" "Ukurekhoda kwesikhathi esisodwa kunokubaluleka okuphezulu kakhulu" "Khansela" - "Khansela" - "Khohlwa" "Misa" "Buka ishejuli yokurekhoda" "Lolu hlelo olulodwa" @@ -270,25 +252,29 @@ "Rekhoda lokhu esikhundleni" "Khansela lokhu kurekhoda" "Bukela njengamanje" + "Susa ukurekhodwa..." "Okungarekhodeka" "Ukurekhoda kushejuliwe" "Ukungqubuzana kokurekhoda" "Iyarekhoda" "Ukurekhoda kuhlulekile" "Ifunda izinhlelo ukuze idale amashejuli okurekhoda" - "Izinhlelo ezifundayo" - - + "Izinhlelo ezifundayo" + "Buka ukurekhoda kwakamuva" + "Ukurekhoda kwe-%1$s akuphelele." + "Ukurekhoda kwe-%1$s ne-%2$s akuphelele." + "Ukurekhoda kwe-%1$s, %2$s and %3$s akuphelele." + "Ukurekhoda kwe-%1$s akuqedanga ngenxa yesitoreji esingaphelele." + "Ukurekhoda kwe-%1$s ne-%2$s akuqedanga ngenxa yesitoreji esingaphelele." + "Ukurekhoda kwe-%1$s, %2$s ne-%3$s akuqedanga ngenxa yesitoreji esingaphelele." "I-DVR idinga isitoreji esiningi" "Uzokwazi ukurekhoda izinhlelo nge-DVR. Kodwa asikho isitoreji esanele kudivayisi yakho manje ukuze i-DVR isebenze. Sicela uxhume idrayivu yangaphandle engu-%1$sGB noma enkulu bese ulandela izinyathelo uyifomethe njengesitoreji sedivayisi." + "Indawo yokubeka ayanele" + "Lolu hlelo ngeke lirekhodwe ngoba asikho isitoreji esanele. Zama ukususa ukurekhoda okukhona." "Isitoreji esilahlekile" - "Esinye sesitoreji esisetshenziswa yi-DVR silahlekile. Sicela uxhume idrayivu engaphandle oyisebenzise ngaphambilini ukuze uphinde unike amandla i-DVR. Okunye, ungakhetha ukukhohlwa isitoreji uma singasatholakali." - "Khohlwa isitoreji?" - "Konke okuqukethwe kwakho okurekhodiwe namashejuli azolahleka." "Misa ukurekhoda?" "Okuqukethwe okurekhodiwe kuzolondolozwa." - - + "Ukurekhodwa kwe-%1$s kuzomiswa ngoba kugqubuzana nalolu hlelo. Okuqukethwe okurekhodiwe ngeke kulondolozwe." "Ukurekhoda kushejuliwe kodwa kunokugqubuzana" "Ukurekhoda kuqalile kodwa kunokugxubuzana" "%1$s izorekhodwa." @@ -306,14 +292,27 @@ "Uhlelo olufanayo seluvele luhlelwe ukurekhodwa ngo-%1$s." "Sekuvele kurekhodiwe" "Lolu hlelo seluvele lurekhodiwe. Lutholakala kulabhulali ye-DVR." - - - - - - - - + "Ukurekhodwa kochungechunge kuhleliwe" + + %1$d ukurekhodwa kushejulelwe i-%2$s. + %1$d ukurekhodwa kushejulelwe i-%2$s. + + + %1$d ukurekhodwa kushejulelwe i-%2$s. %3$d kwazo ngeke kurekhodwe ngenxa yokushayisana. + %1$d ukurekhodwa kushejulelwe i-%2$s. %3$d kwazo ngeke kurekhodwe ngenxa yokushayisana. + + + %1$d ukurekhodwa kushejulelwe i-%2$s. %3$d iziqephu zalo chungechunge nolunye uchungechunge ngeke zirekhodwe ngenxa yokushayisana. + %1$d ukurekhodwa kushejulelwe i-%2$s. %3$d iziqephu zalo chungechunge nolunye uchungechunge ngeke zirekhodwe ngenxa yokushayisana. + + + %1$d ukurekhodwa kushejulelwe i-%2$s. 1 isiqephu solunye uchungechunge ngeke sirekhodwe ngenxa yokushayisana. + %1$d ukurekhodwa kushejulelwe i-%2$s. 1 isiqephu solunye uchungechunge ngeke sirekhodwe ngenxa yokushayisana. + + + %1$d ukurekhodwa kushejulelwe i-%2$s. %3$d iziqephu zolunye uchungechunge ngeke zize zirekhodwe ngenxa yokushayisana. + %1$d ukurekhodwa kushejulelwe i-%2$s. %3$d iziqephu zolunye uchungechunge ngeke zize zirekhodwe ngenxa yokushayisana. + "Uhlelo olurekhodiwe alutholakali." "Ukurekhodwa okuhlobene" "(Ayikho incazelo yohlelo)" @@ -336,6 +335,7 @@ "Misa ukurekhodwa kochungechunge" "Iziqephu ezirekhodiwe zizohlala zitholakala kulabhulali ye-DVR." "Misa" + "Azikho iziqephu ezisemoyeni okwamanje." "Azikho iziqephu ezitholakalayo.\nZizorekhodwa uma zitholakala." (%1$d amaminithi) diff --git a/res/values/attr.xml b/res/values/attr.xml deleted file mode 100644 index 1261ea41..00000000 --- a/res/values/attr.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - diff --git a/res/values/attrs.xml b/res/values/attrs.xml new file mode 100644 index 00000000..a26a9ef2 --- /dev/null +++ b/res/values/attrs.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/values/colors.xml b/res/values/colors.xml index b6b40563..c3cbc22c 100644 --- a/res/values/colors.xml +++ b/res/values/colors.xml @@ -109,6 +109,7 @@ #80EEEEEE + #99000000 #FFEEEEEE diff --git a/res/values/dimens.xml b/res/values/dimens.xml index ca601775..fc8eb009 100644 --- a/res/values/dimens.xml +++ b/res/values/dimens.xml @@ -193,7 +193,7 @@ 40dp 16sp 24dp - -52dp + -72dp 64dp 16dp 8dp @@ -222,8 +222,6 @@ 0dp 0.5dp 4dp - 12dp - 4sp 6dp 6dp 10dp @@ -246,23 +244,9 @@ 60dp 24dp - - 56dp - 27dp - 27dp - 240dp - 135dp - 384dp - 216dp - - - 20dp - 24dp - 56dp 32dp - 288dp 696dp @@ -276,7 +260,7 @@ 44dp 8dp 16dp - 536dp + 456dp 28sp 20sp -10sp @@ -292,6 +276,7 @@ 48dp + 324dp @@ -338,9 +323,7 @@ - 196dp - - + 132dp 1dp 960dp @@ -351,9 +334,7 @@ 32dp 14dp 15dp - 12dp 15dp - 56dp 46dp 32dp 18dp @@ -363,26 +344,29 @@ 24dp 5dp 143dp - 32dp 12dp 4dp - - 192dp - 108dp - 2dp + + 175dp + 144dp + 108dp + 2dp + 20dp + 36.5dp - 146dp - 82dp + 108dp + 81dp 27.5dp - 48dp - 265dp - 40dp + 48dp + + 36dp 219dp diff --git a/res/values/google-services.xml b/res/values/google-services.xml deleted file mode 100755 index 379a3453..00000000 --- a/res/values/google-services.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - https://live-channels-1237.firebaseio.com - 399597460505 - AIzaSyDK2BtNulo2ltWIogD64y1hBWKrdg9Sa7k - AIzaSyDK2BtNulo2ltWIogD64y1hBWKrdg9Sa7k - 1:399597460505:android:6c699100869b1467 - diff --git a/res/values/integers.xml b/res/values/integers.xml index 81ccbeb7..aa68465e 100644 --- a/res/values/integers.xml +++ b/res/values/integers.xml @@ -87,4 +87,6 @@ 500 + 1 + 2 diff --git a/res/values/strings.xml b/res/values/strings.xml index a58f2d36..a1f438fa 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -63,11 +63,9 @@ The play means a playback of video and audio. [CHAR LIMIT=NONE] --> Play controls - Recent channels + Channels TV options - - PIP options %1$dX @@ -101,12 +99,6 @@ Display mode PIP - - On - - Off Multi-audio @@ -116,54 +108,6 @@ Settings - - Source - - Swap - - On - - Off - - Sound - - Main - - PIP window - - Layout - - Bottom right - - Top right - - Top left - - Bottom left - - Side by side - - Size - - Big - - Small - - - Input source TV (antenna/cable) @@ -175,8 +119,11 @@ Blocked channel - - Unknown language + + Unknown language + + + Closed captions %1$d @@ -371,6 +318,15 @@ Try again, PIN doesn\'t match + + Enter your ZIP Code. + + Live TV app will use the ZIP Code to provide a complete program guide for the TV channels. + + Enter your ZIP Code + + Invalid ZIP Code + @@ -391,6 +347,8 @@ Open source licenses Open source licenses + + Send feedback Version @@ -486,10 +444,6 @@ No TV input found Cannot find the TV input - - PIP is not supported - - There is no available input which can be shown with PIP Developer options Watch history + DVR history Fetch program guide - Send feedback Store TS for debugging: On Store TS for debugging: Off Store some TS data before exceptions/crash for debugging. + + [Fail] + [Success] + [Clip] + Recently watched + + DVR history + No result @@ -548,13 +510,6 @@ Cancel recording Stop recording - - Record program - - Record season - - Delete schedule Recordings & schedules @@ -586,15 +541,10 @@ Series Others - None - Channel unknown - No available tuners to record this channel. - The channel is already being recorded. The channel cannot be recorded. The program cannot be recorded. - There is no item. %1$s has been scheduled to be recorded - Cancel recording - Stop and keep recording - Stop and delete recording Watch @@ -709,18 +656,12 @@ - Tune Cancel Delete schedule Record program - Record this episode only - Done - Open DVR - - Cancel - - Forget + + Open storage settings Stop @@ -749,6 +690,8 @@ Cancel this recording Watch now + + Delete recordings… Recordable @@ -766,24 +709,36 @@ recording schedules. --> Reading programs to create recording schedules - Reading programs - - Updating series recording - Insufficient storage space - No sufficient storage space for recording. Please clean up the storage. + Reading programs + + View recent recordings + + The recording of %1$s is incomplete. + + The recordings of %1$s and %2$s are incomplete. + + The recordings of %1$s, %2$s and %3$s are incomplete. + + The recording of %1$s didn\'t complete due to insufficient storage. + + The recordings of %1$s and %2$s didn\'t complete due to insufficient storage. + + The recordings of %1$s, %2$s and %3$s didn\'t complete due to insufficient storage. DVR needs more storage - + You will be able to record programs with DVR. However there is not enough storage on your device now for DVR to work. Please connect an external drive that is %1$sGB or larger and follow the steps to format it as device storage. + + Not enough storage + + This program will not be recorded because there is not enough storage. Try deleting some existing recordings. Missing storage - Some of the storage used by DVR is missing. Please connect the external drive you used before to re-enable DVR. Alternately, you can choose to forget the storage if it\'s no longer available. - - Forget storage? - - All your recorded content and schedules will be lost. + Some of the storage used by DVR is missing. Please connect the external drive you used before to re-enable DVR. Alternately, you can forget the storage in the storage settings, if it\'s no longer available. The recording seems to be deleted. @@ -847,50 +802,44 @@ This program has already been recorded. It’s available in the DVR library. Series recording scheduled - - - %1$d recording has been scheduled for %2$s. - %1$d recordings have been scheduled for %2$s. + + + %1$d recording has been scheduled for %2$s. + %1$d recordings have been scheduled for %2$s. - + - %1$d recording has been scheduled for %2$s. %3$d of them will not be recorded due to conflicts. - %1$d recordings have been scheduled for %2$s. %3$d of them will not be recorded due to conflicts. + %1$d recording has been scheduled for %2$s. It will not be recorded due to conflicts. + %1$d recordings have been scheduled for %2$s. %3$d of them will not be recorded due to conflicts. - - - %1$d recording has been scheduled for %2$s. 1 episode of this series and other series will not be recorded due to conflicts. - %1$d recordings have been scheduled for %2$s. 1 episode of this series and other series will not be recorded due to conflicts. + + %1$d recording has been scheduled for %2$s. %3$d episodes of this series and other series will not be recorded due to conflicts. + %1$d recordings have been scheduled for %2$s. %3$d episodes of this series and other series will not be recorded due to conflicts. - - - %1$d recording has been scheduled for %2$s. %3$d episodes of this series and other series will not be recorded due to conflicts. - %1$d recordings have been scheduled for %2$s. %3$d episodes of this series and other series will not be recorded due to conflicts. - - - - %1$d recording has been scheduled for %2$s. 1 episode of other series will not be recorded due to conflicts. - %1$d recordings have been scheduled for %2$s. 1 episode of other series will not be recorded due to conflicts. + + + %1$d recording has been scheduled for %2$s. 1 episode of other series will not be recorded due to conflicts. + %1$d recordings have been scheduled for %2$s. 1 episode of other series will not be recorded due to conflicts. - - - %1$d recording has been scheduled for %2$s. %3$d episodes of other series will not be recorded due to conflicts. - %1$d recordings have been scheduled for %2$s. %3$d episodes of other series will not be recorded due to conflicts. + + + %1$d recording has been scheduled for %2$s. %3$d episodes of other series will not be recorded due to conflicts. + %1$d recordings have been scheduled for %2$s. %3$d episodes of other series will not be recorded due to conflicts. - Recorded Program Recorded program not found. @@ -902,11 +851,6 @@ Recording till %1$s - - - %1$s is the selected account - No account available - @@ -944,6 +888,8 @@ Recorded episodes will remain available in the DVR library. Stop + + No episodes are on air now. No episodes are available.\nThey will be recorded once they are available. diff --git a/res/values/styles.xml b/res/values/styles.xml index 3a34af52..84885f13 100644 --- a/res/values/styles.xml +++ b/res/values/styles.xml @@ -92,7 +92,7 @@ 14sp @color/lb_basic_card_title_text_color sans-serif-condensed - true + @integer/dvr_card_title_max_lines_folded end @@ -143,7 +143,7 @@ 60dp - diff --git a/src/com/android/tv/Features.java b/src/com/android/tv/Features.java index 7e8e3689..d8b7ae26 100644 --- a/src/com/android/tv/Features.java +++ b/src/com/android/tv/Features.java @@ -27,11 +27,18 @@ import android.content.pm.PackageManager; import android.os.Build; import android.support.annotation.VisibleForTesting; import android.support.v4.os.BuildCompat; +import android.text.TextUtils; +import android.util.Log; import com.android.tv.common.feature.Feature; import com.android.tv.common.feature.GServiceFeature; import com.android.tv.common.feature.PropertyFeature; +import com.android.tv.config.RemoteConfig; +import com.android.tv.util.LocationUtils; import com.android.tv.util.PermissionUtils; +import com.android.tv.util.Utils; + +import java.util.Locale; /** * List of {@link Feature} for the Live TV App. @@ -39,6 +46,9 @@ import com.android.tv.util.PermissionUtils; *

Remove the {@code Feature} once it is launched. */ public final class Features { + private static final String TAG = "Features"; + private static final boolean DEBUG = false; + /** * UI for opting in to analytics. * @@ -61,6 +71,11 @@ public final class Features { @Override public boolean isEnabled(Context context) { + if (Utils.isDeveloper()) { + // we enable tuner for developers to test tuner in any platform. + return true; + } + // This is special handling just for USB Tuner. // It does not require any N API's but relies on a improvements in N for AC3 support // After release, change class to this to just be {@link BuildCompat#isAtLeastN()}. @@ -69,6 +84,25 @@ public final class Features { }; + /** + * Use network tuner if it is available and there is no other tuner types. + */ + public static final Feature NETWORK_TUNER = + new Feature() { + @Override + public boolean isEnabled(Context context) { + if (!TUNER.isEnabled(context)) { + return false; + } + if (Utils.isDeveloper()) { + // Network tuner will be enabled for developers. + return true; + } + return Locale.US.getCountry().equalsIgnoreCase( + LocationUtils.getCurrentCountry(context)); + } + }; + private static final String GSERVICE_KEY_UNHIDE = "live_channels_unhide"; /** * A flag which indicates that LC app is unhidden even when there is no input. @@ -95,6 +129,36 @@ public final class Features { } }; + /** + * Use AC3 software decode. + */ + public static final Feature AC3_SOFTWARE_DECODE = + new Feature() { + private final String[] SUPPORTED_COUNTRIES = { + }; + + private Boolean mEnabled; + + @Override + public boolean isEnabled(Context context) { + if (mEnabled == null) { + if (mEnabled == null) { + // We will not cache the result of fallback solution. + String country = LocationUtils.getCurrentCountry(context); + for (int i = 0; i < SUPPORTED_COUNTRIES.length; ++i) { + if (SUPPORTED_COUNTRIES[i].equalsIgnoreCase(country)) { + return true; + } + } + if (DEBUG) Log.d(TAG, "AC3 flag false after country check"); + return false; + } + } + if (DEBUG) Log.d(TAG, "AC3 flag " + mEnabled); + return mEnabled; + } + }; + /** * Enable a conflict dialog between currently watched channel and upcoming recording. */ diff --git a/src/com/android/tv/InputSessionManager.java b/src/com/android/tv/InputSessionManager.java index e4b0f456..faf76555 100644 --- a/src/com/android/tv/InputSessionManager.java +++ b/src/com/android/tv/InputSessionManager.java @@ -37,7 +37,6 @@ import android.text.TextUtils; import android.util.ArraySet; import android.util.Log; -import com.android.tv.common.SoftPreconditions; import com.android.tv.data.Channel; import com.android.tv.ui.TunableTvView; import com.android.tv.ui.TunableTvView.OnTuneListener; @@ -73,6 +72,8 @@ public class InputSessionManager { Collections.synchronizedSet(new ArraySet<>()); private final Set mOnTvViewChannelChangeListeners = new ArraySet<>(); + private final Set mOnRecordingSessionChangeListeners = + new ArraySet<>(); public InputSessionManager(Context context) { mContext = context.getApplicationContext(); @@ -113,6 +114,9 @@ public class InputSessionManager { RecordingSession session = new RecordingSession(inputId, tag, callback, handler, endTimeMs); mRecordingSessions.add(session); if (DEBUG) Log.d(TAG, "Recording session created: " + session); + for (OnRecordingSessionChangeListener listener : mOnRecordingSessionChangeListeners) { + listener.onRecordingSessionChange(true, mRecordingSessions.size()); + } return session; } @@ -123,6 +127,9 @@ public class InputSessionManager { mRecordingSessions.remove(session); session.release(); if (DEBUG) Log.d(TAG, "Recording session released: " + session); + for (OnRecordingSessionChangeListener listener : mOnRecordingSessionChangeListeners) { + listener.onRecordingSessionChange(false, mRecordingSessions.size()); + } } /** @@ -148,9 +155,17 @@ public class InputSessionManager { } } - /** - * Returns the current {@link TvView} channel. - */ + /** Adds the {@link OnRecordingSessionChangeListener}. */ + public void addOnRecordingSessionChangeListener(OnRecordingSessionChangeListener listener) { + mOnRecordingSessionChangeListeners.add(listener); + } + + /** Removes the {@link OnRecordingSessionChangeListener}. */ + public void removeRecordingSessionChangeListener(OnRecordingSessionChangeListener listener) { + mOnRecordingSessionChangeListeners.remove(listener); + } + + /** Returns the current {@link TvView} channel. */ @MainThread public Uri getCurrentTvViewChannelUri() { for (TvViewSession session : mTvViewSessions) { @@ -546,4 +561,9 @@ public class InputSessionManager { public interface OnTvViewChannelChangeListener { void onTvViewChannelChange(@Nullable Uri channelUri); } + + /** Called when recording session is created or destroyed. */ + public interface OnRecordingSessionChangeListener { + void onRecordingSessionChange(boolean create, int count); + } } diff --git a/src/com/android/tv/MainActivity.java b/src/com/android/tv/MainActivity.java index 58850b5f..94006b72 100644 --- a/src/com/android/tv/MainActivity.java +++ b/src/com/android/tv/MainActivity.java @@ -29,7 +29,6 @@ import android.content.res.Configuration; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.BitmapFactory; -import android.graphics.Point; import android.hardware.display.DisplayManager; import android.media.AudioManager; import android.media.MediaMetadata; @@ -71,7 +70,6 @@ import android.view.accessibility.AccessibilityManager; import android.widget.FrameLayout; import android.widget.Toast; -import com.android.tv.analytics.DurationTimer; import com.android.tv.analytics.SendChannelStatusRunnable; import com.android.tv.analytics.SendConfigInfoRunnable; import com.android.tv.analytics.Tracker; @@ -91,14 +89,14 @@ import com.android.tv.data.ProgramDataManager; import com.android.tv.data.StreamInfo; import com.android.tv.data.WatchedHistoryManager; import com.android.tv.data.epg.EpgFetcher; +import com.android.tv.dialog.HalfSizedDialogFragment; import com.android.tv.dialog.PinDialogFragment; import com.android.tv.dialog.SafeDismissDialogFragment; -import com.android.tv.dvr.ConflictChecker; import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.DvrUiHelper; -import com.android.tv.dvr.ScheduledRecording; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.recorder.ConflictChecker; import com.android.tv.dvr.ui.DvrStopRecordingFragment; -import com.android.tv.dvr.ui.HalfSizedDialogFragment; +import com.android.tv.dvr.ui.DvrUiHelper; import com.android.tv.experiments.Experiments; import com.android.tv.menu.Menu; import com.android.tv.onboarding.OnboardingActivity; @@ -107,9 +105,11 @@ import com.android.tv.parental.ParentalControlSettings; import com.android.tv.receiver.AudioCapabilitiesReceiver; import com.android.tv.recommendation.NotificationService; import com.android.tv.search.ProgramGuideSearchFragment; +import com.android.tv.tuner.TunerInputController; import com.android.tv.tuner.TunerPreferences; import com.android.tv.tuner.setup.TunerSetupActivity; import com.android.tv.tuner.tvinput.TunerTvInputService; +import com.android.tv.tuner.util.PostalCodeUtils; import com.android.tv.ui.AppLayerTvView; import com.android.tv.ui.ChannelBannerView; import com.android.tv.ui.InputBannerView; @@ -119,6 +119,7 @@ import com.android.tv.ui.SelectInputView.OnInputSelectedCallback; import com.android.tv.ui.TunableTvView; import com.android.tv.ui.TunableTvView.BlockScreenType; import com.android.tv.ui.TunableTvView.OnTuneListener; +import com.android.tv.ui.TuningBlockView; import com.android.tv.ui.TvOverlayManager; import com.android.tv.ui.TvViewUiManager; import com.android.tv.ui.sidepanel.ClosedCaptionFragment; @@ -130,21 +131,20 @@ import com.android.tv.ui.sidepanel.SettingsFragment; import com.android.tv.ui.sidepanel.SideFragment; import com.android.tv.util.AccountHelper; import com.android.tv.util.CaptionSettings; +import com.android.tv.util.Debug; +import com.android.tv.util.DurationTimer; import com.android.tv.util.ImageCache; import com.android.tv.util.ImageLoader; import com.android.tv.util.OnboardingUtils; import com.android.tv.util.PermissionUtils; -import com.android.tv.util.PipInputManager; -import com.android.tv.util.PipInputManager.PipInput; import com.android.tv.util.RecurringRunner; -import com.android.tv.util.SearchManagerHelper; import com.android.tv.util.SetupUtils; import com.android.tv.util.SystemProperties; import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.TvSettings; -import com.android.tv.util.TvSettings.PipSound; import com.android.tv.util.TvTrackInfoUtils; import com.android.tv.util.Utils; +import com.android.tv.util.ViewCache; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -181,8 +181,12 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC private static final float FRAME_RATE_FOR_FILM = 23.976f; private static final float FRAME_RATE_EPSILON = 0.1f; - private static final float MEDIA_SESSION_STOPPED_SPEED = 0.0f; - private static final float MEDIA_SESSION_PLAYING_SPEED = 1.0f; + private static PlaybackState MEDIA_SESSION_STATE_PLAYING = new PlaybackState.Builder() + .setState(PlaybackState.STATE_PLAYING, PlaybackState.PLAYBACK_POSITION_UNKNOWN, 1.0f) + .build(); + private static PlaybackState MEDIA_SESSION_STATE_STOPPED = new PlaybackState.Builder() + .setState(PlaybackState.STATE_STOPPED, PlaybackState.PLAYBACK_POSITION_UNKNOWN, 0.0f) + .build(); private static final int PERMISSIONS_REQUEST_READ_TV_LISTINGS = 1; @@ -227,12 +231,12 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC private static final int MSG_CHANNEL_DOWN_PRESSED = 1000; private static final int MSG_CHANNEL_UP_PRESSED = 1001; - private static final int MSG_UPDATE_CHANNEL_BANNER_BY_INFO_UPDATE = 1002; @Retention(RetentionPolicy.SOURCE) @IntDef({UPDATE_CHANNEL_BANNER_REASON_FORCE_SHOW, UPDATE_CHANNEL_BANNER_REASON_TUNE, UPDATE_CHANNEL_BANNER_REASON_TUNE_FAST, UPDATE_CHANNEL_BANNER_REASON_UPDATE_INFO, - UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK}) + UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK, + UPDATE_CHANNEL_BANNER_REASON_UPDATE_STREAM_INFO}) private @interface ChannelBannerUpdateReason {} /** * Updates channel banner because the channel banner is forced to show. @@ -254,6 +258,10 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC * Updates channel banner because the current watched channel is locked or unlocked. */ private static final int UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK = 5; + /** + * Updates channel banner because of stream info updating. + */ + private static final int UPDATE_CHANNEL_BANNER_REASON_UPDATE_STREAM_INFO = 6; private static final int TVVIEW_SET_MAIN_TIMEOUT_MS = 3000; @@ -261,12 +269,13 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC // Delay 1 second in order not to interrupt the first tune. private static final long LAZY_INITIALIZATION_DELAY = TimeUnit.SECONDS.toMillis(1); + private static final int UNDEFINED_TRACK_INDEX = -1; + private AccessibilityManager mAccessibilityManager; private ChannelDataManager mChannelDataManager; private ProgramDataManager mProgramDataManager; private TvInputManagerHelper mTvInputManagerHelper; private ChannelTuner mChannelTuner; - private PipInputManager mPipInputManager; private final TvOptionsManager mTvOptionsManager = new TvOptionsManager(this); private TvViewUiManager mTvViewUiManager; private TimeShiftManager mTimeShiftManager; @@ -278,7 +287,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC private View mContentView; private TunableTvView mTvView; - private TunableTvView mPipView; private Bundle mTuneParams; private boolean mChannelBannerHiddenBySideFragment; // TODO: Move the scene views into TvTransitionManager or TvOverlayManager. @@ -303,10 +311,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC private AudioManager mAudioManager; private int mAudioFocusStatus; private boolean mTunePending; - private boolean mPipEnabled; - private Channel mPipChannel; - private boolean mPipSwap; - @PipSound private int mPipSound = TvSettings.PIP_SOUND_MAIN; // Default private boolean mDebugNonFullSizeScreen; private boolean mActivityResumed; private boolean mActivityStarted; @@ -331,7 +335,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC private boolean mIsCurrentChannelUnblockedByUser; private boolean mWasChannelUnblockedBeforeShrunkenByUser; private Channel mChannelBeforeShrunkenTvView; - private Channel mPipChannelBeforeShrunkenTvView; private boolean mIsCompletingShrunkenTvView; // TODO: Need to consider the case that TIS explicitly request PIN code while TV view is @@ -350,9 +353,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC private RecurringRunner mSendConfigInfoRecurringRunner; private RecurringRunner mChannelStatusRecurringRunner; - // A caller which started this activity. (e.g. TvSearch) - private String mSource; - private final Handler mHandler = new MainActivityHandler(this); private final Set mOnActionClickListeners = new ArraySet<>(); @@ -372,15 +372,13 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC if (!mActivityResumed && mVisibleBehind) { // ACTION_SCREEN_ON is usually called after onResume. But, if media is played // under launcher with requestVisibleBehind(true), onResume will not be called. - // In this case, we need to resume TvView and PipView explicitly. + // In this case, we need to resume TvView explicitly. resumeTvIfNeeded(); - resumePipIfNeeded(); } } else if (intent.getAction().equals( TvInputManager.ACTION_PARENTAL_CONTROLS_ENABLED_CHANGED)) { if (DEBUG) Log.d(TAG, "Received parental control settings change"); - checkChannelLockNeeded(mTvView); - checkChannelLockNeeded(mPipView); + checkChannelLockNeeded(mTvView, null); applyParentalControlSettings(); } } @@ -407,10 +405,11 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC new ChannelTuner.Listener() { @Override public void onLoadFinished() { + Debug.getTimer(Debug.TAG_START_UP_TIMER).log( + "MainActivity.mChannelTunerListener.onLoadFinished"); SetupUtils.getInstance(MainActivity.this).markNewChannelsBrowsable(); if (mActivityResumed) { resumeTvIfNeeded(); - resumePipIfNeeded(); } mKeypadChannelSwitchView.setChannels(mChannelTuner.getBrowsableChannelList()); } @@ -430,13 +429,12 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC } }; - private final Runnable mRestoreMainViewRunnable = - new Runnable() { - @Override - public void run() { - restoreMainTvView(); - } - }; + private final Runnable mRestoreMainViewRunnable = new Runnable() { + @Override + public void run() { + restoreMainTvView(); + } + }; private ProgramGuideSearchFragment mSearchFragment; private final TvInputCallback mTvInputCallback = new TvInputCallback() { @@ -456,11 +454,14 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC boolean parentalControlEnabled = mTvInputManagerHelper.getParentalControlSettings() .isParentalControlsEnabled(); mTvView.onParentalControlChanged(parentalControlEnabled); - mPipView.onParentalControlChanged(parentalControlEnabled); } @Override protected void onCreate(Bundle savedInstanceState) { + // Restarts the global duration timer to avoid the case that TvApplication starts much + // earlier than MainActivity. + Debug.getTimer(Debug.TAG_START_UP_TIMER).start(); + Debug.getTimer(Debug.TAG_START_UP_TIMER).log("MainActivity.onCreate restarts timer"); if (DEBUG) Log.d(TAG,"onCreate()"); TvApplication.setCurrentRunningProcess(this, true); super.onCreate(savedInstanceState); @@ -478,32 +479,14 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC return; } - // Check this permission for the EPG fetch. - // TODO: check {@link shouldShowRequestPermissionRationale}. - // While testing, no way to allow the permission when the dialog shows up. So we'd better - // not show the dialog. - if (!TvCommonUtils.isRunningInTest() && Utils.hasInternalTvInputs(this, true) - && checkSelfPermission(android.Manifest.permission.ACCESS_COARSE_LOCATION) - != PackageManager.PERMISSION_GRANTED) { - requestPermissions(new String[] {android.Manifest.permission.ACCESS_COARSE_LOCATION}, - PERMISSIONS_REQUEST_ACCESS_COARSE_LOCATION); - } - - DisplayManager displayManager = (DisplayManager) getSystemService(Context.DISPLAY_SERVICE); - Display display = displayManager.getDisplay(Display.DEFAULT_DISPLAY); - Point size = new Point(); - display.getSize(size); - int screenWidth = size.x; - int screenHeight = size.y; - mDefaultRefreshRate = display.getRefreshRate(); - setContentView(R.layout.activity_tv); - mContentView = findViewById(android.R.id.content); + TvApplication tvApplication = (TvApplication) getApplication(); + mProgramDataManager = tvApplication.getProgramDataManager(); + mTvInputManagerHelper = tvApplication.getTvInputManagerHelper(); mTvView = (TunableTvView) findViewById(R.id.main_tunable_tv_view); - int shrunkenTvViewHeight = getResources().getDimensionPixelSize( - R.dimen.shrunken_tvview_height); - mTvView.initialize((AppLayerTvView) findViewById(R.id.main_tv_view), false, screenHeight, - shrunkenTvViewHeight); + mTvView.initialize((AppLayerTvView) findViewById(R.id.main_tv_view), + (TuningBlockView) findViewById(R.id.tuning_block), mProgramDataManager, + mTvInputManagerHelper); mTvView.setOnUnhandledInputEventListener(new OnUnhandledInputEventListener() { @Override public boolean onUnhandledInputEvent(InputEvent event) { @@ -526,7 +509,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC return false; } }); - long channelId = Utils.getLastWatchedChannelId(this); String inputId = Utils.getLastWatchedTunerInputId(this); if (!isPassthroughInput && inputId != null @@ -534,27 +516,34 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC mTvView.warmUpInput(inputId, TvContract.buildChannelUri(channelId)); } - TvApplication tvApplication = (TvApplication) getApplication(); + // Check this permission for the EPG fetch. + // TODO: check {@link shouldShowRequestPermissionRationale}. + // While testing, no way to allow the permission when the dialog shows up. So we'd better + // not show the dialog. + if (!TvCommonUtils.isRunningInTest() && Utils.hasInternalTvInputs(this, true) + && TextUtils.isEmpty(PostalCodeUtils.getLastPostalCode(this)) + && checkSelfPermission(android.Manifest.permission.ACCESS_COARSE_LOCATION) + != PackageManager.PERMISSION_GRANTED) { + requestPermissions(new String[] {android.Manifest.permission.ACCESS_COARSE_LOCATION}, + PERMISSIONS_REQUEST_ACCESS_COARSE_LOCATION); + } + tvApplication.getMainActivityWrapper().onMainActivityCreated(this); if (BuildConfig.ENG && SystemProperties.ALLOW_STRICT_MODE.getValue()) { Toast.makeText(this, "Using Strict Mode for eng builds", Toast.LENGTH_SHORT).show(); } mTracker = tvApplication.getTracker(); - mTvInputManagerHelper = tvApplication.getTvInputManagerHelper(); if (Features.TUNER.isEnabled(this)) { mTvInputManagerHelper.addCallback(mTvInputCallback); } mTunerInputId = TunerTvInputService.getInputId(this); mChannelDataManager = tvApplication.getChannelDataManager(); - mProgramDataManager = tvApplication.getProgramDataManager(); mProgramDataManager.addOnCurrentProgramUpdatedListener(Channel.INVALID_ID, mOnCurrentProgramUpdatedListener); mProgramDataManager.setPrefetchEnabled(true); mChannelTuner = new ChannelTuner(mChannelDataManager, mTvInputManagerHelper); mChannelTuner.addListener(mChannelTunerListener); mChannelTuner.start(); - mPipInputManager = new PipInputManager(this, mTvInputManagerHelper, mChannelTuner); - mPipInputManager.start(); mMemoryManageables.add(mProgramDataManager); mMemoryManageables.add(ImageCache.getInstance()); mMemoryManageables.add(TvContentRatingCache.getInstance()); @@ -584,9 +573,9 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC } }); - mPipView = (TunableTvView) findViewById(R.id.pip_tunable_tv_view); - mPipView.initialize((AppLayerTvView) findViewById(R.id.pip_tv_view), true, screenHeight, - shrunkenTvViewHeight); + DisplayManager displayManager = (DisplayManager) getSystemService(Context.DISPLAY_SERVICE); + Display display = displayManager.getDisplay(Display.DEFAULT_DISPLAY); + mDefaultRefreshRate = display.getRefreshRate(); if (!PermissionUtils.hasAccessWatchedHistory(this)) { WatchedHistoryManager watchedHistoryManager = new WatchedHistoryManager( @@ -594,12 +583,10 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC watchedHistoryManager.start(); mTvView.setWatchedHistoryManager(watchedHistoryManager); } - mTvViewUiManager = new TvViewUiManager(this, mTvView, mPipView, + mTvViewUiManager = new TvViewUiManager(this, mTvView, (FrameLayout) findViewById(android.R.id.content), mTvOptionsManager); - mPipView.setFixedSurfaceSize(screenWidth / 2, screenHeight / 2); - mPipView.setBlockScreenType(TunableTvView.BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW); - + mContentView = findViewById(android.R.id.content); ViewGroup sceneContainer = (ViewGroup) findViewById(R.id.scene_container); mChannelBannerView = (ChannelBannerView) getLayoutInflater().inflate( R.layout.channel_banner, sceneContainer, false); @@ -641,7 +628,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC } }); mSearchFragment = new ProgramGuideSearchFragment(); - mOverlayManager = new TvOverlayManager(this, mChannelTuner, mTvView, + mOverlayManager = new TvOverlayManager(this, mChannelTuner, mTvView, mTvOptionsManager, mKeypadChannelSwitchView, mChannelBannerView, inputBannerView, selectInputView, sceneContainer, mSearchFragment); @@ -692,6 +679,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC mDvrConflictChecker = new ConflictChecker(this); } initForTest(); + Debug.getTimer(Debug.TAG_START_UP_TIMER).log("MainActivity.onCreate end"); } @Override @@ -726,9 +714,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC case PERMISSIONS_REQUEST_ACCESS_COARSE_LOCATION: if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED && Experiments.CLOUD_EPG.get()) { - EpgFetcher.getInstance(this).startImmediately(); - } else { - EpgFetcher.getInstance(this).stop(); + EpgFetcher.getInstance(this).startImmediately(false); } break; } @@ -798,14 +784,14 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC Intent notificationIntent = new Intent(this, NotificationService.class); notificationIntent.setAction(NotificationService.ACTION_SHOW_RECOMMENDATION); startService(notificationIntent); + TunerInputController.executeNetworkTunerDiscoveryAsyncTask(this); } @Override protected void onResume() { + Debug.getTimer(Debug.TAG_START_UP_TIMER).log("MainActivity.onResume start"); if (DEBUG) Log.d(TAG, "onResume()"); super.onResume(); - // Refresh the remote config, it is throttled automatically. - TvApplication.getSingletons(this).getRemoteConfig().fetch(null); if (!PermissionUtils.hasAccessAllEpg(this) && checkSelfPermission(PERMISSION_READ_TV_LISTINGS) != PackageManager.PERMISSION_GRANTED) { @@ -830,12 +816,16 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC // visible behind. requestVisibleBehind(true); } - if (Utils.hasRecordingFailedReason(getApplicationContext(), - TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE)) { + Set failedScheduledRecordingInfoSet = + Utils.getFailedScheduledRecordingInfoSet(getApplicationContext()); + if (Utils.hasRecordingFailedReason( + getApplicationContext(), TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE) + && !failedScheduledRecordingInfoSet.isEmpty()) { runAfterAttachedToWindow(new Runnable() { @Override public void run() { - DvrUiHelper.showDvrInsufficientSpaceErrorDialog(MainActivity.this); + DvrUiHelper.showDvrInsufficientSpaceErrorDialog(MainActivity.this, + failedScheduledRecordingInfoSet); } }); } @@ -843,7 +833,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC if (mChannelTuner.areAllChannelsLoaded()) { SetupUtils.getInstance(this).markNewChannelsBrowsable(); resumeTvIfNeeded(); - resumePipIfNeeded(); } mOverlayManager.showMenuWithTimeShiftPauseIfNeeded(); @@ -881,6 +870,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC if (mDvrConflictChecker != null) { mDvrConflictChecker.start(); } + Debug.getTimer(Debug.TAG_START_UP_TIMER).log("MainActivity.onResume end"); } @Override @@ -893,9 +883,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC mActivityResumed = false; mOverlayManager.hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_DEFAULT); mTvView.setBlockScreenType(TunableTvView.BLOCK_SCREEN_TYPE_NO_UI); - if (mPipEnabled) { - mTvViewUiManager.hidePipForPause(); - } mBackKeyPressed = false; mShowLockedChannelsTemporarily = false; mShouldTuneToTunerChannel = false; @@ -903,7 +890,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC mAudioFocusStatus = AudioManager.AUDIOFOCUS_LOSS; mAudioManager.abandonAudioFocus(this); if (mMediaSession.isActive()) { - mMediaSession.setActive(false); + setMediaSessionActiveAndPlaybackState(false); } mTracker.sendScreenView(""); } else { @@ -962,21 +949,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC mTvView.setBlockScreenType(getDesiredBlockScreenType()); } - private void resumePipIfNeeded() { - if (mPipEnabled && !(mPipView.isPlaying() && mPipView.isShown())) { - if (mPipInputManager.areInSamePipInput( - mChannelTuner.getCurrentChannel(), mPipChannel)) { - enablePipView(false, false); - } else { - if (!mPipView.isPlaying()) { - startPip(false); - } else { - mTvViewUiManager.showPipForResume(); - } - } - } - } - private void startTv(Uri channelUri) { if (DEBUG) Log.d(TAG, "startTv Uri=" + channelUri); if ((channelUri == null || !TvContract.isChannelUriForPassthroughInput(channelUri)) @@ -1027,9 +999,9 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC } } - mTvView.start(mTvInputManagerHelper); + mTvView.start(); setVolumeByAudioFocusStatus(); - tune(); + tune(true); } @Override @@ -1072,7 +1044,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC private void stopAll(boolean keepVisibleBehind) { mOverlayManager.hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_WITHOUT_ANIMATION); stopTv("stopAll()", keepVisibleBehind); - stopPip(); } public TvInputManagerHelper getTvInputManagerHelper() { @@ -1145,10 +1116,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC return mProgramDataManager; } - public PipInputManager getPipInputManager() { - return mPipInputManager; - } - public TvOptionsManager getTvOptionsManager() { return mTvOptionsManager; } @@ -1272,19 +1239,11 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC mChannelBeforeShrunkenTvView = mTvView.getCurrentChannel(); mWasChannelUnblockedBeforeShrunkenByUser = mIsCurrentChannelUnblockedByUser; mAllowedRatingBeforeShrunken = mLastAllowedRatingForCurrentChannel; - - if (willMainViewBeTunerInput && mChannelTuner.isCurrentChannelPassthrough() - && mPipEnabled) { - mPipChannelBeforeShrunkenTvView = mPipChannel; - enablePipView(false, false); - } else { - mPipChannelBeforeShrunkenTvView = null; - } mTvViewUiManager.startShrunkenTvView(); if (showLockedChannelsTemporarily) { mShowLockedChannelsTemporarily = true; - checkChannelLockNeeded(mTvView); + checkChannelLockNeeded(mTvView, null); } mTvView.setBlockScreenType(getDesiredBlockScreenType()); @@ -1320,23 +1279,15 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC mIsCompletingShrunkenTvView = false; mIsCurrentChannelUnblockedByUser = mWasChannelUnblockedBeforeShrunkenByUser; mTvView.setBlockScreenType(getDesiredBlockScreenType()); - if (mPipChannelBeforeShrunkenTvView != null) { - enablePipView(true, false); - mPipChannelBeforeShrunkenTvView = null; - } } }; mTvViewUiManager.fadeOutTvView(tuneAction); // Will automatically fade-in when video becomes available. } else { - checkChannelLockNeeded(mTvView); + checkChannelLockNeeded(mTvView, null); mIsCompletingShrunkenTvView = false; mIsCurrentChannelUnblockedByUser = mWasChannelUnblockedBeforeShrunkenByUser; mTvView.setBlockScreenType(getDesiredBlockScreenType()); - if (mPipChannelBeforeShrunkenTvView != null) { - enablePipView(true, false); - mPipChannelBeforeShrunkenTvView = null; - } } } @@ -1363,7 +1314,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC mChannelTuner.moveToAdjacentBrowsableChannel(true); } if (mTunePending) { - tune(); + tune(true); } } else { mInputIdUnderSetup = null; @@ -1488,11 +1439,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC }); } else if (Intent.ACTION_VIEW.equals(intent.getAction())) { Uri uri = intent.getData(); - try { - mSource = uri.getQueryParameter(Utils.PARAM_SOURCE); - } catch (UnsupportedOperationException e) { - // ignore this exception. - } // When the URI points to the programs (directory, not an individual item), go to the // program guide. The intention here is to respond to // "content://android.media.tv/program", not "content://android.media.tv/program/XXX". @@ -1568,38 +1514,20 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC } private void setVolumeByAudioFocusStatus() { - if (mPipSound == TvSettings.PIP_SOUND_MAIN) { - setVolumeByAudioFocusStatus(mTvView); - } else { // mPipSound == TvSettings.PIP_SOUND_PIP_WINDOW - setVolumeByAudioFocusStatus(mPipView); - } - } - - private void setVolumeByAudioFocusStatus(TunableTvView tvView) { - SoftPreconditions.checkState(tvView == mTvView || tvView == mPipView); - if (tvView.isPlaying()) { + if (mTvView.isPlaying()) { switch (mAudioFocusStatus) { case AudioManager.AUDIOFOCUS_GAIN: - tvView.setStreamVolume(AUDIO_MAX_VOLUME); + mTvView.setStreamVolume(AUDIO_MAX_VOLUME); break; case AudioManager.AUDIOFOCUS_LOSS: case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: - tvView.setStreamVolume(AUDIO_MIN_VOLUME); + mTvView.setStreamVolume(AUDIO_MIN_VOLUME); break; case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: - tvView.setStreamVolume(AUDIO_DUCKING_VOLUME); + mTvView.setStreamVolume(AUDIO_DUCKING_VOLUME); break; } } - if (tvView == mTvView) { - if (mPipView != null && mPipView.isPlaying()) { - mPipView.setStreamVolume(AUDIO_MIN_VOLUME); - } - } else { // tvView == mPipView - if (mTvView != null && mTvView.isPlaying()) { - mTvView.setStreamVolume(AUDIO_MIN_VOLUME); - } - } } private void stopTv() { @@ -1619,7 +1547,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC } mAudioManager.abandonAudioFocus(this); if (mMediaSession.isActive()) { - mMediaSession.setActive(false); + setMediaSessionActiveAndPlaybackState(false); } } TvApplication.getSingletons(this).getMainActivityWrapper() @@ -1632,95 +1560,11 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC return mTvView.isPlaying() && mTvView.getCurrentChannel() != null; } - private void startPip(final boolean fromUserInteraction) { - if (mPipChannel == null) { - Log.w(TAG, "PIP channel id is an invalid id."); - return; - } - if (DEBUG) Log.d(TAG, "startPip() " + mPipChannel); - mPipView.start(mTvInputManagerHelper); - boolean success = mPipView.tuneTo(mPipChannel, null, new OnTuneListener() { - @Override - public void onUnexpectedStop(Channel channel) { - Log.w(TAG, "The PIP is Unexpectedly stopped"); - enablePipView(false, false); - } - - @Override - public void onTuneFailed(Channel channel) { - Log.w(TAG, "Fail to start the PIP during channel tuning"); - if (fromUserInteraction) { - Toast.makeText(MainActivity.this, R.string.msg_no_pip_support, - Toast.LENGTH_SHORT).show(); - enablePipView(false, false); - } - } - - @Override - public void onStreamInfoChanged(StreamInfo info) { - mTvViewUiManager.updatePipView(); - mHandler.removeCallbacks(mRestoreMainViewRunnable); - restoreMainTvView(); - } - - @Override - public void onChannelRetuned(Uri channel) { - if (channel == null) { - return; - } - Channel currentChannel = - mChannelDataManager.getChannel(ContentUris.parseId(channel)); - if (currentChannel == null) { - Log.e(TAG, "onChannelRetuned is called from PIP input but can't find a channel" - + " with the URI " + channel); - return; - } - if (isChannelChangeKeyDownReceived()) { - // Ignore this message if the user is changing the channel. - return; - } - mPipChannel = currentChannel; - mPipView.setCurrentChannel(mPipChannel); - } - - @Override - public void onContentBlocked() { - updateMediaSession(); - } - - @Override - public void onContentAllowed() { - updateMediaSession(); - } - }); - if (!success) { - Log.w(TAG, "Fail to start the PIP"); - return; - } - if (fromUserInteraction) { - checkChannelLockNeeded(mPipView); - } - // Explicitly make the PIP view main to make the selected input an HDMI-CEC active source. - mPipView.setMain(); - scheduleRestoreMainTvView(); - mTvViewUiManager.onPipStart(); - setVolumeByAudioFocusStatus(); - } - private void scheduleRestoreMainTvView() { mHandler.removeCallbacks(mRestoreMainViewRunnable); mHandler.postDelayed(mRestoreMainViewRunnable, TVVIEW_SET_MAIN_TIMEOUT_MS); } - private void stopPip() { - if (DEBUG) Log.d(TAG, "stopPip"); - if (mPipView.isPlaying()) { - mPipView.stop(); - mPipSwap = false; - mTvViewUiManager.onPipStop(); - } - } - /** * Says {@code text} when accessibility is turned on. */ @@ -1735,7 +1579,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC } } - private void tune() { + private void tune(boolean updateChannelBanner) { if (DEBUG) Log.d(TAG, "tune()"); mTuneDurationTimer.start(); @@ -1825,7 +1669,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC if (!isUnderShrunkenTvView()) { mLastAllowedRatingForCurrentChannel = null; } - mHandler.removeMessages(MSG_UPDATE_CHANNEL_BANNER_BY_INFO_UPDATE); // For every tune, we need to inform the tuned channel or input to a user, // if Talkback is turned on. sendAccessibilityText(!mChannelTuner.isCurrentChannelPassthrough() ? @@ -1852,8 +1695,13 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC TvApplication.getSingletons(this).getMainActivityWrapper() .notifyCurrentChannelChange(this, channel); } - checkChannelLockNeeded(mTvView); - updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_TUNE); + // We have to provide channel here instead of using TvView's channel, because TvView's + // channel might be null when there's tuner conflict. In that case, TvView will resets + // its current channel onConnectionFailed(). + checkChannelLockNeeded(mTvView, channel); + if (updateChannelBanner) { + updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_TUNE); + } if (mActivityResumed) { // requestVisibleBehind should be called after onResume() is called. But, when // launcher is over the TV app and the screen is turned off and on, tune() can @@ -1897,19 +1745,18 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC private void updateMediaSession() { if (getCurrentChannel() == null) { - mMediaSession.setActive(false); + setMediaSessionActiveAndPlaybackState(false); return; } // If the channel is blocked, display a lock and a short text on the Now Playing Card if (mTvView.isScreenBlocked() || mTvView.getBlockedContentRating() != null) { - setMediaSessionPlaybackState(false); Bitmap art = BitmapFactory.decodeResource( getResources(), R.drawable.ic_message_lock_preview); updateMediaMetadata( getResources().getString(R.string.channel_banner_locked_channel_title), art); - mMediaSession.setActive(true); + setMediaSessionActiveAndPlaybackState(true); return; } @@ -1924,13 +1771,11 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC cardTitleText = getCurrentChannelName(); } updateMediaMetadata(cardTitleText, null); - setMediaSessionPlaybackState(true); - if (posterArtUri == null) { posterArtUri = TvContract.buildChannelLogoUri(getCurrentChannelId()).toString(); } updatePosterArt(getCurrentChannel(), currentProgram, cardTitleText, null, posterArtUri); - mMediaSession.setActive(true); + setMediaSessionActiveAndPlaybackState(true); } private void updatePosterArt(Channel currentChannel, Program currentProgram, @@ -2017,12 +1862,17 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC } } - private void setMediaSessionPlaybackState(boolean isPlaying) { - PlaybackState.Builder builder = new PlaybackState.Builder(); - builder.setState(isPlaying ? PlaybackState.STATE_PLAYING : PlaybackState.STATE_STOPPED, - PlaybackState.PLAYBACK_POSITION_UNKNOWN, - isPlaying ? MEDIA_SESSION_PLAYING_SPEED : MEDIA_SESSION_STOPPED_SPEED); - mMediaSession.setPlaybackState(builder.build()); + private void setMediaSessionActiveAndPlaybackState(boolean isPlaying) { + if (isPlaying) { + mMediaSession.setActive(true); + // setMediaSessionPlaybackState has to be called after calling mMediaSession.setActive + // b/31933276 + mMediaSession.setPlaybackState(MEDIA_SESSION_STATE_PLAYING); + } else { + mMediaSession.setPlaybackState(MEDIA_SESSION_STATE_STOPPED); + mMediaSession.setActive(false); + } + } private void addToRecentChannels(long channelId) { @@ -2042,33 +1892,27 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC return mRecentChannels; } - private void checkChannelLockNeeded(TunableTvView tvView) { - Channel channel = tvView.getCurrentChannel(); - if (tvView.isPlaying() && channel != null) { + private void checkChannelLockNeeded(TunableTvView tvView, Channel currentChannel) { + if (currentChannel == null) { + currentChannel = tvView.getCurrentChannel(); + } + if (tvView.isPlaying() && currentChannel != null) { if (getParentalControlSettings().isParentalControlsEnabled() - && channel.isLocked() + && currentChannel.isLocked() && !mShowLockedChannelsTemporarily && !(isUnderShrunkenTvView() - && channel.equals(mChannelBeforeShrunkenTvView) + && currentChannel.equals(mChannelBeforeShrunkenTvView) && mWasChannelUnblockedBeforeShrunkenByUser)) { - if (DEBUG) Log.d(TAG, "Channel " + channel.getId() + " is locked"); - blockScreen(tvView); + if (DEBUG) Log.d(TAG, "Channel " + currentChannel.getId() + " is locked"); + blockOrUnblockScreen(tvView, true); } else { - unblockScreen(tvView); + blockOrUnblockScreen(tvView, false); } } } - private void blockScreen(TunableTvView tvView) { - tvView.blockScreen(); - if (tvView == mTvView) { - updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK); - updateMediaSession(); - } - } - - private void unblockScreen(TunableTvView tvView) { - tvView.unblockScreen(); + private void blockOrUnblockScreen(TunableTvView tvView, boolean blockOrUnblock) { + tvView.blockOrUnblockScreen(blockOrUnblock); if (tvView == mTvView) { updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK); updateMediaSession(); @@ -2089,36 +1933,59 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC private void updateChannelBannerAndShowIfNeeded(@ChannelBannerUpdateReason int reason) { if(DEBUG) Log.d(TAG, "updateChannelBannerAndShowIfNeeded(reason=" + reason + ")"); + if (isChannelChangeKeyDownReceived() && reason != UPDATE_CHANNEL_BANNER_REASON_TUNE + && reason != UPDATE_CHANNEL_BANNER_REASON_TUNE_FAST) { + // Tuning is still ongoing, no need to update banner for other reasons + return; + } if (!mChannelTuner.isCurrentChannelPassthrough()) { int lockType = ChannelBannerView.LOCK_NONE; - if (mTvView.isScreenBlocked()) { + if (reason == UPDATE_CHANNEL_BANNER_REASON_TUNE_FAST) { + if (getParentalControlSettings().isParentalControlsEnabled() + && getCurrentChannel().isLocked()) { + lockType = ChannelBannerView.LOCK_CHANNEL_INFO; + } else { + // Do not show detailed program information while fast-tuning. + lockType = ChannelBannerView.LOCK_PROGRAM_DETAIL; + } + } else if (reason == UPDATE_CHANNEL_BANNER_REASON_TUNE) { + if (getParentalControlSettings().isParentalControlsEnabled()) { + if (getCurrentChannel().isLocked()) { + lockType = ChannelBannerView.LOCK_CHANNEL_INFO; + } else { + // If parental control is turned on, + // assumes that program is locked by default and waits for onContentAllowed. + lockType = ChannelBannerView.LOCK_PROGRAM_DETAIL; + } + } + } else if (mTvView.isScreenBlocked()) { lockType = ChannelBannerView.LOCK_CHANNEL_INFO; } else if (mTvView.getBlockedContentRating() != null || (getParentalControlSettings().isParentalControlsEnabled() - && !mTvView.isVideoAvailable())) { + && !mTvView.isVideoOrAudioAvailable())) { // If the parental control is enabled, do not show the program detail until the // video becomes available. lockType = ChannelBannerView.LOCK_PROGRAM_DETAIL; } - if (lockType == ChannelBannerView.LOCK_NONE) { - if (reason == UPDATE_CHANNEL_BANNER_REASON_TUNE_FAST) { - // Do not show detailed program information while fast-tuning. - lockType = ChannelBannerView.LOCK_PROGRAM_DETAIL; - } else if (reason == UPDATE_CHANNEL_BANNER_REASON_TUNE - && getParentalControlSettings().isParentalControlsEnabled()) { - // If parental control is turned on, - // assumes that program is locked by default and waits for onContentAllowed. - lockType = ChannelBannerView.LOCK_PROGRAM_DETAIL; - } - } // If lock type is not changed, we don't need to update channel banner by parental // control. - if (!mChannelBannerView.setLockType(lockType) + int previousLockType = mChannelBannerView.setLockType(lockType); + if (previousLockType == lockType && reason == UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK) { return; + } else if (reason == UPDATE_CHANNEL_BANNER_REASON_UPDATE_STREAM_INFO) { + mChannelBannerView.updateStreamInfo(mTvView); + // If parental control is enabled, we shows program description when the video is + // available, instead of tuning. Therefore we need to check it here if the program + // description is previously hidden by parental control. + if (previousLockType == ChannelBannerView.LOCK_PROGRAM_DETAIL && + lockType != ChannelBannerView.LOCK_PROGRAM_DETAIL) { + mChannelBannerView.updateViews(false); + } + } else { + mChannelBannerView.updateViews(reason == UPDATE_CHANNEL_BANNER_REASON_TUNE + || reason == UPDATE_CHANNEL_BANNER_REASON_TUNE_FAST); } - - mChannelBannerView.updateViews(mTvView); } boolean needToShowBanner = (reason == UPDATE_CHANNEL_BANNER_REASON_FORCE_SHOW || reason == UPDATE_CHANNEL_BANNER_REASON_TUNE @@ -2197,7 +2064,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC if (bestTrack != null) { String selectedTrack = getSelectedTrack(TvTrackInfo.TYPE_AUDIO); if (!bestTrack.getId().equals(selectedTrack)) { - selectTrack(TvTrackInfo.TYPE_AUDIO, bestTrack); + selectTrack(TvTrackInfo.TYPE_AUDIO, bestTrack, UNDEFINED_TRACK_INDEX); } else { mTvOptionsManager.onMultiAudioChanged( Utils.getMultiAudioString(this, bestTrack, false)); @@ -2210,7 +2077,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC private void applyClosedCaption() { List tracks = getTracks(TvTrackInfo.TYPE_SUBTITLE); if (tracks == null) { - mTvOptionsManager.onClosedCaptionsChanged(null); + mTvOptionsManager.onClosedCaptionsChanged(null, UNDEFINED_TRACK_INDEX); return; } @@ -2219,17 +2086,19 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC String selectedTrackId = getSelectedTrack(TvTrackInfo.TYPE_SUBTITLE); TvTrackInfo alternativeTrack = null; + int alternativeTrackIndex = UNDEFINED_TRACK_INDEX; if (enabled) { String language = mCaptionSettings.getLanguage(); String trackId = mCaptionSettings.getTrackId(); - for (TvTrackInfo track : tracks) { + for (int i = 0; i < tracks.size(); i++) { + TvTrackInfo track = tracks.get(i); if (Utils.isEqualLanguage(track.getLanguage(), language)) { if (track.getId().equals(trackId)) { if (!track.getId().equals(selectedTrackId)) { - selectTrack(TvTrackInfo.TYPE_SUBTITLE, track); + selectTrack(TvTrackInfo.TYPE_SUBTITLE, track, i); } else { // Already selected. Update the option string only. - mTvOptionsManager.onClosedCaptionsChanged(track); + mTvOptionsManager.onClosedCaptionsChanged(track, i); } if (DEBUG) { Log.d(TAG, "Subtitle Track Selected {id=" + track.getId() @@ -2238,14 +2107,16 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC return; } else if (alternativeTrack == null) { alternativeTrack = track; + alternativeTrackIndex = i; } } } if (alternativeTrack != null) { if (!alternativeTrack.getId().equals(selectedTrackId)) { - selectTrack(TvTrackInfo.TYPE_SUBTITLE, alternativeTrack); + selectTrack(TvTrackInfo.TYPE_SUBTITLE, alternativeTrack, alternativeTrackIndex); } else { - mTvOptionsManager.onClosedCaptionsChanged(alternativeTrack); + mTvOptionsManager + .onClosedCaptionsChanged(alternativeTrack, alternativeTrackIndex); } if (DEBUG) { Log.d(TAG, "Subtitle Track Selected {id=" + alternativeTrack.getId() @@ -2255,11 +2126,11 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC } } if (selectedTrackId != null) { - selectTrack(TvTrackInfo.TYPE_SUBTITLE, null); + selectTrack(TvTrackInfo.TYPE_SUBTITLE, null, UNDEFINED_TRACK_INDEX); if (DEBUG) Log.d(TAG, "Subtitle Track Unselected"); return; } - mTvOptionsManager.onClosedCaptionsChanged(null); + mTvOptionsManager.onClosedCaptionsChanged(null, UNDEFINED_TRACK_INDEX); } /** @@ -2274,12 +2145,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC } } - public void showSearchActivity() { - // HACK: Once we moved the window layer to TYPE_APPLICATION_SUB_PANEL, - // the voice button doesn't work. So we directly call the voice action. - SearchManagerHelper.getInstance(this).launchAssistAction(); - } - public void showProgramGuideSearchFragment() { getFragmentManager().beginTransaction().replace(R.id.fragment_container, mSearchFragment) .addToBackStack(null).commit(); @@ -2295,13 +2160,11 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC @Override protected void onDestroy() { if (DEBUG) Log.d(TAG, "onDestroy()"); - SideFragment.releasePreloadedRecycledViews(); + SideFragment.releaseRecycledViewPool(); + ViewCache.getInstance().clear(); if (mTvView != null) { mTvView.release(); } - if (mPipView != null) { - mPipView.release(); - } if (mChannelTuner != null) { mChannelTuner.removeListener(mChannelTunerListener); mChannelTuner.stop(); @@ -2314,9 +2177,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC mProgramDataManager.setPrefetchEnabled(false); } } - if (mPipInputManager != null) { - mPipInputManager.stop(); - } if (mOverlayManager != null) { mOverlayManager.release(); } @@ -2340,8 +2200,11 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC mChannelStatusRecurringRunner.stop(); mChannelStatusRecurringRunner = null; } - if (mTvInputManagerHelper != null && Features.TUNER.isEnabled(this)) { - mTvInputManagerHelper.removeCallback(mTvInputCallback); + if (mTvInputManagerHelper != null) { + mTvInputManagerHelper.clearTvInputLabels(); + if (Features.TUNER.isEnabled(this)) { + mTvInputManagerHelper.removeCallback(mTvInputCallback); + } } super.onDestroy(); } @@ -2410,7 +2273,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC * G debug: refresh cloud epg * I KEYCODE_TV_INPUT * O debug: show display mode option - * P debug: togglePipView * S KEYCODE_CAPTIONS: select subtitle * W debug: toggle screen size * V KEYCODE_MEDIA_RECORD debug: record the current channel for 30 sec @@ -2422,8 +2284,9 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC finishChannelChangeIfNeeded(); if (event.getKeyCode() == KeyEvent.KEYCODE_SEARCH) { - showSearchActivity(); - return true; + // Prevent MainActivity from being closed by onVisibleBehindCanceled() + mOtherActivityLaunched = true; + return false; } switch (mOverlayManager.onKeyUp(keyCode, event)) { case KEY_EVENT_HANDLER_RESULT_DISPATCH_TO_OVERLAY: @@ -2472,7 +2335,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC } switch (keyCode) { case KeyEvent.KEYCODE_DPAD_RIGHT: - if (!mTvView.isVideoAvailable() + if (!mTvView.isVideoOrAudioAvailable() && mTvView.getVideoUnavailableReason() == TunableTvView.VIDEO_UNAVAILABLE_REASON_NO_RESOURCE) { DvrUiHelper.startSchedulesActivityForTuneConflict(this, @@ -2491,7 +2354,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC @Override public void done(boolean success) { if (success) { - unblockScreen(mTvView); + blockOrUnblockScreen(mTvView, false); mIsCurrentChannelUnblockedByUser = true; } } @@ -2578,22 +2441,17 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC Toast.makeText(this, R.string.dvr_msg_cannot_record_program, Toast.LENGTH_SHORT).show(); } else { - if (!DvrUiHelper.checkStorageStatusAndShowErrorMessage(this, - currentChannel.getInputId())) { - return true; - } Program program = mProgramDataManager .getCurrentProgram(currentChannel.getId()); - if (program == null) { - DvrUiHelper - .showChannelRecordDurationOptions(this, currentChannel); - } else if (DvrUiHelper.handleCreateSchedule(this, program)) { - String msg = getString( - R.string.dvr_msg_current_program_scheduled, - program.getTitle(), Utils.toTimeString( - program.getEndTimeUtcMillis(), false)); - Toast.makeText(this, msg, Toast.LENGTH_SHORT).show(); - } + DvrUiHelper.checkStorageStatusAndShowErrorMessage(this, + currentChannel.getInputId(), new Runnable() { + @Override + public void run() { + DvrUiHelper.requestRecordingCurrentProgram( + MainActivity.this, + currentChannel, program, false); + } + }); } } else { DvrUiHelper.showStopRecordingDialog(this, currentChannel.getId(), @@ -2643,10 +2501,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC } return true; } - case KeyEvent.KEYCODE_P: { - togglePipView(); - return true; - } case KeyEvent.KEYCODE_CTRL_LEFT: case KeyEvent.KEYCODE_CTRL_RIGHT: { mUseKeycodeBlacklist = !mUseKeycodeBlacklist; @@ -2680,22 +2534,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC return false; } - @Override - public void onBackPressed() { - // The activity should be returned to the caller of this activity - // when the mSource is not null. - if (!mOverlayManager.getSideFragmentManager().isActive() && isPlaying() - && mSource == null) { - // If back key would exit TV app, - // show McLauncher instead so we can get benefit of McLauncher's shyMode. - Intent startMain = new Intent(Intent.ACTION_MAIN); - startMain.addCategory(Intent.CATEGORY_HOME); - startActivity(startMain); - } else { - super.onBackPressed(); - } - } - @Override public void onUserInteraction() { super.onUserInteraction(); @@ -2725,65 +2563,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC } } - public void togglePipView() { - enablePipView(!mPipEnabled, true); - mOverlayManager.getMenu().update(); - } - - public boolean isPipEnabled() { - return mPipEnabled; - } - - public void tuneToChannelForPip(Channel channel) { - if (!mPipEnabled) { - throw new IllegalStateException("tuneToChannelForPip is called when PIP is off"); - } - if (mPipChannel.equals(channel)) { - return; - } - mPipChannel = channel; - startPip(true); - } - - private void enablePipView(boolean enable, boolean fromUserInteraction) { - if (enable == mPipEnabled) { - return; - } - if (enable) { - List pipAvailableInputs = mPipInputManager.getPipInputList(true); - if (pipAvailableInputs.isEmpty()) { - Toast.makeText(this, R.string.msg_no_available_input_by_pip, Toast.LENGTH_SHORT) - .show(); - return; - } - // TODO: choose the last pip input. - Channel pipChannel = pipAvailableInputs.get(0).getChannel(); - if (pipChannel != null) { - mPipEnabled = true; - mPipChannel = pipChannel; - startPip(fromUserInteraction); - mTvViewUiManager.restorePipSize(); - mTvViewUiManager.restorePipLayout(); - mTvOptionsManager.onPipChanged(mPipEnabled); - } else { - Toast.makeText(this, R.string.msg_no_available_input_by_pip, Toast.LENGTH_SHORT) - .show(); - } - } else { - mPipEnabled = false; - mPipChannel = null; - // Recover the stream volume of the main TV view, if needed. - if (mPipSound == TvSettings.PIP_SOUND_PIP_WINDOW) { - setVolumeByAudioFocusStatus(mTvView); - mPipSound = TvSettings.PIP_SOUND_MAIN; - mTvOptionsManager.onPipSoundChanged(mPipSound); - } - stopPip(); - mTvViewUiManager.restoreDisplayMode(false); - mTvOptionsManager.onPipChanged(mPipEnabled); - } - } - private boolean isChannelChangeKeyDownReceived() { return mHandler.hasMessages(MSG_CHANNEL_UP_PRESSED) || mHandler.hasMessages(MSG_CHANNEL_DOWN_PRESSED); @@ -2811,10 +2590,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC if (SystemProperties.LOG_KEYEVENT.getValue()) { Log.d(TAG, "dispatchKeyEventToSession(" + event + ")"); } - if (mPipEnabled && mChannelTuner.isCurrentChannelPassthrough()) { - // If PIP is enabled, key events will be used by UI. - return false; - } boolean handled = false; if (mTvView != null) { handled = mTvView.dispatchKeyEvent(event); @@ -2832,21 +2607,15 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC } private boolean isKeyEventBlocked() { - // If the current channel is passthrough channel without a PIP view, - // we always don't handle the key events in TV activity. Instead, the key event will - // be handled by the passthrough TV input. - return mChannelTuner.isCurrentChannelPassthrough() && !mPipEnabled; + // If the current channel is a passthrough channel, we don't handle the key events in TV + // activity. Instead, the key event will be handled by the passthrough TV input. + return mChannelTuner.isCurrentChannelPassthrough(); } private void tuneToLastWatchedChannelForTunerInput() { if (!mChannelTuner.isCurrentChannelPassthrough()) { return; } - if (mPipEnabled) { - if (!mPipChannel.isPassthrough()) { - enablePipView(false, true); - } - } stopTv(); startTv(null); } @@ -2857,16 +2626,16 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC mTvView.reset(); } } else { - if (mPipEnabled && mPipInputManager.areInSamePipInput(channel, mPipChannel)) { - enablePipView(false, true); - } if (!mTvView.isPlaying()) { startTv(channel.getUri()); } else if (channel.equals(mTvView.getCurrentChannel())) { updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_TUNE); + } else if (channel == mChannelTuner.getCurrentChannel()) { + // Channel banner is already updated in moveToAdjacentChannel + tune(false); } else if (mChannelTuner.moveToChannel(channel)) { // Channel banner would be updated inside of tune. - tune(); + tune(true); } else { showSettingsFragment(); } @@ -2888,90 +2657,11 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC } } - public Channel getPipChannel() { - return mPipChannel; - } - - /** - * Swap the main and the sub screens while in the PIP mode. - */ - public void swapPip() { - if (!mPipEnabled || mTvView == null || mPipView == null) { - Log.e(TAG, "swapPip() - not in PIP"); - mPipSwap = false; - return; - } - - Channel channel = mTvView.getCurrentChannel(); - boolean tvViewBlocked = mTvView.isScreenBlocked(); - boolean pipViewBlocked = mPipView.isScreenBlocked(); - if (channel == null || !mTvView.isPlaying()) { - // If the TV view is not currently playing or its current channel is null, swapping here - // basically means disabling the PIP mode and getting back to the full screen since - // there's no point of keeping a blank PIP screen at the bottom which is not tune-able. - enablePipView(false, true); - mOverlayManager.hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_DEFAULT); - mPipSwap = false; - return; - } - - // Reset the TV view and tune the PIP view to the previous channel of the TV view. - mTvView.reset(); - mPipView.reset(); - Channel oldPipChannel = mPipChannel; - tuneToChannelForPip(channel); - if (tvViewBlocked) { - mPipView.blockScreen(); - } else { - mPipView.unblockScreen(); - } - - if (oldPipChannel != null) { - // Tune the TV view to the previous PIP channel. - tuneToChannel(oldPipChannel); - } - if (pipViewBlocked) { - mTvView.blockScreen(); - } else { - mTvView.unblockScreen(); - } - if (mPipSound == TvSettings.PIP_SOUND_MAIN) { - setVolumeByAudioFocusStatus(mTvView); - } else { // mPipSound == TvSettings.PIP_SOUND_PIP_WINDOW - setVolumeByAudioFocusStatus(mPipView); - } - mPipSwap = !mPipSwap; - mTvOptionsManager.onPipSwapChanged(mPipSwap); - } - - /** - * Toggle where the sound is coming from when the user is watching the PIP. - */ - public void togglePipSoundMode() { - if (!mPipEnabled || mTvView == null || mPipView == null) { - Log.e(TAG, "togglePipSoundMode() - not in PIP"); - return; - } - if (mPipSound == TvSettings.PIP_SOUND_MAIN) { - setVolumeByAudioFocusStatus(mPipView); - mPipSound = TvSettings.PIP_SOUND_PIP_WINDOW; - } else { // mPipSound == TvSettings.PIP_SOUND_PIP_WINDOW - setVolumeByAudioFocusStatus(mTvView); - mPipSound = TvSettings.PIP_SOUND_MAIN; - } - restoreMainTvView(); - mTvOptionsManager.onPipSoundChanged(mPipSound); - } - /** * Set the main TV view which holds HDMI-CEC active source based on the sound mode */ private void restoreMainTvView() { - if (mPipSound == TvSettings.PIP_SOUND_MAIN) { - mTvView.setMain(); - } else { // mPipSound == TvSettings.PIP_SOUND_PIP_WINDOW - mPipView.setMain(); - } + mTvView.setMain(); } @Override @@ -2981,9 +2671,8 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC mAudioFocusStatus = AudioManager.AUDIOFOCUS_LOSS; mAudioManager.abandonAudioFocus(this); if (mMediaSession.isActive()) { - mMediaSession.setActive(false); + setMediaSessionActiveAndPlaybackState(false); } - stopPip(); mVisibleBehind = false; if (!mOtherActivityLaunched && Build.VERSION.SDK_INT == Build.VERSION_CODES.M) { // Workaround: in M, onStop is not called, even though it should be called after @@ -3012,13 +2701,13 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC return mTvView.getSelectedTrack(type); } - private void selectTrack(int type, TvTrackInfo track) { + private void selectTrack(int type, TvTrackInfo track, int trackIndex) { mTvView.selectTrack(type, track == null ? null : track.getId()); if (type == TvTrackInfo.TYPE_AUDIO) { mTvOptionsManager.onMultiAudioChanged(track == null ? null : Utils.getMultiAudioString(this, track, false)); } else if (type == TvTrackInfo.TYPE_SUBTITLE) { - mTvOptionsManager.onClosedCaptionsChanged(track); + mTvOptionsManager.onClosedCaptionsChanged(track, trackIndex); } } @@ -3163,6 +2852,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC if (mActivityStarted) { initAnimations(); initSideFragments(); + initMenuItemViews(); } } }, LAZY_INITIALIZATION_DELAY); @@ -3174,7 +2864,11 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC } private void initSideFragments() { - SideFragment.preloadRecycledViews(this); + SideFragment.preloadItemViews(this); + } + + private void initMenuItemViews() { + mOverlayManager.getMenu().preloadItemViews(); } @Override @@ -3207,10 +2901,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC sendMessageDelayed(Message.obtain(msg), getDelay(startTime)); mainActivity.moveToAdjacentChannel(true, true); break; - case MSG_UPDATE_CHANNEL_BANNER_BY_INFO_UPDATE: - mainActivity.updateChannelBannerAndShowIfNeeded( - UPDATE_CHANNEL_BANNER_REASON_UPDATE_INFO); - break; } } @@ -3225,14 +2915,12 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC private class MyOnTuneListener implements OnTuneListener { boolean mUnlockAllowedRatingBeforeShrunken = true; boolean mWasUnderShrunkenTvView; - long mStreamInfoUpdateTimeThresholdMs; Channel mChannel; public MyOnTuneListener() { } private void onTune(Channel channel, boolean wasUnderShrukenTvView) { - mStreamInfoUpdateTimeThresholdMs = - System.currentTimeMillis() + FIRST_STREAM_INFO_UPDATE_DELAY_MILLIS; + Debug.getTimer(Debug.TAG_START_UP_TIMER).log("MainActivity.MyOnTuneListener.onTune"); mChannel = channel; mWasUnderShrunkenTvView = wasUnderShrukenTvView; } @@ -3258,20 +2946,9 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC mTracker.sendChannelTuneTime(info.getCurrentChannel(), mTuneDurationTimer.reset()); } - // If updateChannelBanner() is called without delay, the stream info seems flickering - // when the channel is quickly changed. - if (!mHandler.hasMessages(MSG_UPDATE_CHANNEL_BANNER_BY_INFO_UPDATE) - && info.isVideoAvailable()) { - if (System.currentTimeMillis() > mStreamInfoUpdateTimeThresholdMs) { - updateChannelBannerAndShowIfNeeded( - UPDATE_CHANNEL_BANNER_REASON_UPDATE_INFO); - } else { - mHandler.sendMessageDelayed(mHandler.obtainMessage( - MSG_UPDATE_CHANNEL_BANNER_BY_INFO_UPDATE), - mStreamInfoUpdateTimeThresholdMs - System.currentTimeMillis()); - } + if (info.isVideoOrAudioAvailable() && mChannel == getCurrentChannel()) { + updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_UPDATE_STREAM_INFO); } - applyDisplayRefreshRate(info.getVideoFrameRate()); mTvViewUiManager.updateTvView(); applyMultiAudio(); @@ -3308,6 +2985,9 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC @Override public void onContentBlocked() { + Debug.getTimer(Debug.TAG_START_UP_TIMER).log( + "MainActivity.MyOnTuneListener.onContentBlocked removes timer"); + Debug.removeTimer(Debug.TAG_START_UP_TIMER); mTuneDurationTimer.reset(); TvContentRating rating = mTvView.getBlockedContentRating(); // When tuneTo was called while TV view was shrunken, if the channel id is the same @@ -3319,8 +2999,10 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC mUnlockAllowedRatingBeforeShrunken = isUnderShrunkenTvView(); mTvView.unblockContent(rating); } - mChannelBannerView.setBlockingContentRating(rating); - updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK); + if (!isChannelChangeKeyDownReceived()) { + mChannelBannerView.setBlockingContentRating(rating); + updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK); + } mTvViewUiManager.fadeInTvView(); } @@ -3329,8 +3011,10 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC if (!isUnderShrunkenTvView()) { mUnlockAllowedRatingBeforeShrunken = false; } - mChannelBannerView.setBlockingContentRating(null); - updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK); + if (!isChannelChangeKeyDownReceived()) { + mChannelBannerView.setBlockingContentRating(null); + updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK); + } } } -} +} \ No newline at end of file diff --git a/src/com/android/tv/SetupPassthroughActivity.java b/src/com/android/tv/SetupPassthroughActivity.java index 8a263a26..6459693b 100644 --- a/src/com/android/tv/SetupPassthroughActivity.java +++ b/src/com/android/tv/SetupPassthroughActivity.java @@ -44,64 +44,72 @@ public class SetupPassthroughActivity extends Activity { private TvInputInfo mTvInputInfo; private Intent mActivityAfterCompletion; + private boolean mEpgFetcherDuringScan; @Override public void onCreate(Bundle savedInstanceState) { + if (DEBUG) Log.d(TAG, "onCreate"); super.onCreate(savedInstanceState); - Intent intent = getIntent(); - SoftPreconditions.checkState( - intent.getAction().equals(TvCommonConstants.INTENT_ACTION_INPUT_SETUP)); ApplicationSingletons appSingletons = TvApplication.getSingletons(this); TvInputManagerHelper inputManager = appSingletons.getTvInputManagerHelper(); + Intent intent = getIntent(); String inputId = intent.getStringExtra(TvCommonConstants.EXTRA_INPUT_ID); mTvInputInfo = inputManager.getTvInputInfo(inputId); - if (DEBUG) Log.d(TAG, "TvInputId " + inputId + " / TvInputInfo " + mTvInputInfo); - if (mTvInputInfo == null) { - Log.w(TAG, "There is no input with the ID " + inputId + "."); - finish(); - return; - } - Intent setupIntent = intent.getExtras().getParcelable(TvCommonConstants.EXTRA_SETUP_INTENT); - if (DEBUG) Log.d(TAG, "Setup activity launch intent: " + setupIntent); - if (setupIntent == null) { - Log.w(TAG, "The input (" + mTvInputInfo.getId() + ") doesn't have setup."); - finish(); - return; - } - SetupUtils.grantEpgPermission(this, mTvInputInfo.getServiceInfo().packageName); mActivityAfterCompletion = intent.getParcelableExtra( TvCommonConstants.EXTRA_ACTIVITY_AFTER_COMPLETION); - if (DEBUG) Log.d(TAG, "Activity after completion " + mActivityAfterCompletion); - // If EXTRA_SETUP_INTENT is not removed, an infinite recursion happens during - // setupIntent.putExtras(intent.getExtras()). - Bundle extras = intent.getExtras(); - extras.remove(TvCommonConstants.EXTRA_SETUP_INTENT); - setupIntent.putExtras(extras); - try { - startActivityForResult(setupIntent, REQUEST_START_SETUP_ACTIVITY); - } catch (ActivityNotFoundException e) { - Log.e(TAG, "Can't find activity: " + setupIntent.getComponent()); - finish(); - return; - } - if (Utils.isInternalTvInput(this, mTvInputInfo.getId()) && Experiments.CLOUD_EPG.get()) { - EpgFetcher.getInstance(this).stop(); + boolean needToFetchEpg = Utils.isInternalTvInput(this, mTvInputInfo.getId()) + && Experiments.CLOUD_EPG.get(); + if (needToFetchEpg) { + // In case when the activity is restored, this flag should be restored as well. + mEpgFetcherDuringScan = true; } - } - - @Override - protected void onDestroy() { - if (mTvInputInfo != null && Utils.isInternalTvInput(this, mTvInputInfo.getId()) - && Experiments.CLOUD_EPG.get()) { - EpgFetcher.getInstance(this).start(); + if (savedInstanceState == null) { + SoftPreconditions.checkState( + intent.getAction().equals(TvCommonConstants.INTENT_ACTION_INPUT_SETUP)); + if (DEBUG) Log.d(TAG, "TvInputId " + inputId + " / TvInputInfo " + mTvInputInfo); + if (mTvInputInfo == null) { + Log.w(TAG, "There is no input with the ID " + inputId + "."); + finish(); + return; + } + Intent setupIntent = + intent.getExtras().getParcelable(TvCommonConstants.EXTRA_SETUP_INTENT); + if (DEBUG) Log.d(TAG, "Setup activity launch intent: " + setupIntent); + if (setupIntent == null) { + Log.w(TAG, "The input (" + mTvInputInfo.getId() + ") doesn't have setup."); + finish(); + return; + } + SetupUtils.grantEpgPermission(this, mTvInputInfo.getServiceInfo().packageName); + if (DEBUG) Log.d(TAG, "Activity after completion " + mActivityAfterCompletion); + // If EXTRA_SETUP_INTENT is not removed, an infinite recursion happens during + // setupIntent.putExtras(intent.getExtras()). + Bundle extras = intent.getExtras(); + extras.remove(TvCommonConstants.EXTRA_SETUP_INTENT); + setupIntent.putExtras(extras); + try { + startActivityForResult(setupIntent, REQUEST_START_SETUP_ACTIVITY); + } catch (ActivityNotFoundException e) { + Log.e(TAG, "Can't find activity: " + setupIntent.getComponent()); + finish(); + return; + } + if (needToFetchEpg) { + EpgFetcher.getInstance(this).onChannelScanStarted(); + } } - super.onDestroy(); } @Override public void onActivityResult(int requestCode, final int resultCode, final Intent data) { + if (DEBUG) Log.d(TAG, "onActivityResult"); boolean setupComplete = requestCode == REQUEST_START_SETUP_ACTIVITY && resultCode == Activity.RESULT_OK; + // Tells EpgFetcher that channel source setup is finished. + if (mEpgFetcherDuringScan) { + EpgFetcher.getInstance(this).onChannelScanFinished(); + mEpgFetcherDuringScan = false; + } if (!setupComplete) { setResult(resultCode, data); finish(); @@ -122,4 +130,4 @@ public class SetupPassthroughActivity extends Activity { } }); } -} +} \ No newline at end of file diff --git a/src/com/android/tv/TimeShiftManager.java b/src/com/android/tv/TimeShiftManager.java index 2d6d45c4..e1024705 100644 --- a/src/com/android/tv/TimeShiftManager.java +++ b/src/com/android/tv/TimeShiftManager.java @@ -887,10 +887,11 @@ public class TimeShiftManager { } long fetchStartTimeMs = Utils.floorTime(startTimeMs, MAX_DUMMY_PROGRAM_DURATION); - boolean needToLoad = addDummyPrograms(fetchStartTimeMs, - endTimeMs + PREFETCH_DURATION_FOR_NEXT); + long fetchEndTimeMs = Utils.ceilTime(endTimeMs + PREFETCH_DURATION_FOR_NEXT, + MAX_DUMMY_PROGRAM_DURATION); + boolean needToLoad = addDummyPrograms(fetchStartTimeMs, fetchEndTimeMs); if (needToLoad) { - Range period = Range.create(fetchStartTimeMs, endTimeMs); + Range period = Range.create(fetchStartTimeMs, fetchEndTimeMs); mProgramLoadQueue.add(period); startTaskIfNeeded(); } @@ -1012,7 +1013,7 @@ public class TimeShiftManager { for (int i = 0, j = 0; i < mPrograms.size() && j < loadedPrograms.size(); ++j) { Program loadedProgram = loadedPrograms.get(j); // Skip previous programs. - while (program.getEndTimeUtcMillis() < loadedProgram.getStartTimeUtcMillis()) { + while (program.getEndTimeUtcMillis() <= loadedProgram.getStartTimeUtcMillis()) { // Reached end of mPrograms. if (++i == mPrograms.size()) { return; diff --git a/src/com/android/tv/TvApplication.java b/src/com/android/tv/TvApplication.java index 0e18a259..159df7b6 100644 --- a/src/com/android/tv/TvApplication.java +++ b/src/com/android/tv/TvApplication.java @@ -37,6 +37,7 @@ import android.util.Log; import android.view.KeyEvent; import com.android.tv.analytics.Analytics; +import com.android.tv.util.Debug; import com.android.tv.analytics.StubAnalytics; import com.android.tv.analytics.StubAnalytics; import com.android.tv.analytics.Tracker; @@ -53,10 +54,10 @@ import com.android.tv.data.ProgramDataManager; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrDataManagerImpl; import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.DvrRecordingService; import com.android.tv.dvr.DvrScheduleManager; import com.android.tv.dvr.DvrStorageStatusManager; import com.android.tv.dvr.DvrWatchedPositionManager; +import com.android.tv.dvr.recorder.DvrRecordingService; import com.android.tv.tuner.TunerPreferences; import com.android.tv.tuner.tvinput.TunerTvInputService; import com.android.tv.tuner.util.TunerInputInfoUtils; @@ -106,6 +107,8 @@ public class TvApplication extends Application implements ApplicationSingletons @Override public void onCreate() { + Debug.getTimer(Debug.TAG_START_UP_TIMER).start(); + Debug.getTimer(Debug.TAG_START_UP_TIMER).log("TvApplication.onCreate start"); super.onCreate(); SharedPreferencesUtils.initialize(this, new Runnable() { @Override @@ -149,13 +152,13 @@ public class TvApplication extends Application implements ApplicationSingletons mAnalytics = StubAnalytics.getInstance(this); } mTracker = mAnalytics.getDefaultTracker(); - mTvInputManagerHelper = new TvInputManagerHelper(this); - mTvInputManagerHelper.start(); + getTvInputManagerHelper(); // In SetupFragment, transitions are set in the constructor. Because the fragment can be // created in Activity.onCreate() by the framework, SetupAnimationHelper should be // initialized here before Activity.onCreate() is called. SetupAnimationHelper.initialize(this); Log.i(TAG, "Started Live TV " + mVersionName); + Debug.getTimer(Debug.TAG_START_UP_TIMER).log("TvApplication.onCreate end"); } private void setCurrentRunningProcess(boolean isMainProcess) { @@ -167,8 +170,10 @@ public class TvApplication extends Application implements ApplicationSingletons if (CommonFeatures.DVR.isEnabled(this)) { mDvrStorageStatusManager = new DvrStorageStatusManager(this, mRunningInMainProcess); } + // Fetch remote config + getSingletons(this).getRemoteConfig().fetch(null); if (mRunningInMainProcess) { - mTvInputManagerHelper.addCallback(new TvInputCallback() { + getTvInputManagerHelper().addCallback(new TvInputCallback() { @Override public void onInputAdded(String inputId) { if (Features.TUNER.isEnabled(TvApplication.this) && TextUtils.equals(inputId, @@ -268,7 +273,7 @@ public class TvApplication extends Application implements ApplicationSingletons @Override public ChannelDataManager getChannelDataManager() { if (mChannelDataManager == null) { - mChannelDataManager = new ChannelDataManager(this, mTvInputManagerHelper); + mChannelDataManager = new ChannelDataManager(this, getTvInputManagerHelper()); mChannelDataManager.start(); } return mChannelDataManager; @@ -314,6 +319,10 @@ public class TvApplication extends Application implements ApplicationSingletons */ @Override public TvInputManagerHelper getTvInputManagerHelper() { + if (mTvInputManagerHelper == null) { + mTvInputManagerHelper = new TvInputManagerHelper(this); + mTvInputManagerHelper.start(); + } return mTvInputManagerHelper; } diff --git a/src/com/android/tv/TvOptionsManager.java b/src/com/android/tv/TvOptionsManager.java index 7871cbe7..493e039c 100644 --- a/src/com/android/tv/TvOptionsManager.java +++ b/src/com/android/tv/TvOptionsManager.java @@ -18,14 +18,13 @@ package com.android.tv; import android.content.Context; import android.media.tv.TvTrackInfo; +import android.support.annotation.IntDef; import android.util.SparseArray; import com.android.tv.data.DisplayMode; -import com.android.tv.util.TvSettings; -import com.android.tv.util.TvSettings.PipLayout; -import com.android.tv.util.TvSettings.PipSize; -import com.android.tv.util.TvSettings.PipSound; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.Locale; /** @@ -33,39 +32,34 @@ import java.util.Locale; * captions and display mode. Can be also used to create MenuAction items to control such options. */ public class TvOptionsManager { + @Retention(RetentionPolicy.SOURCE) + @IntDef({OPTION_CLOSED_CAPTIONS, OPTION_DISPLAY_MODE, OPTION_SYSTEMWIDE_PIP, OPTION_MULTI_AUDIO, + OPTION_MORE_CHANNELS, OPTION_DEVELOPER, OPTION_SETTINGS}) + public @interface OptionType {} public static final int OPTION_CLOSED_CAPTIONS = 0; public static final int OPTION_DISPLAY_MODE = 1; - public static final int OPTION_IN_APP_PIP = 2; - public static final int OPTION_SYSTEMWIDE_PIP = 3; - public static final int OPTION_MULTI_AUDIO = 4; - public static final int OPTION_MORE_CHANNELS = 5; - public static final int OPTION_DEVELOPER = 6; - public static final int OPTION_SETTINGS = 7; - - public static final int OPTION_PIP_INPUT = 100; - public static final int OPTION_PIP_SWAP = 101; - public static final int OPTION_PIP_SOUND = 102; - public static final int OPTION_PIP_LAYOUT = 103 ; - public static final int OPTION_PIP_SIZE = 104; + public static final int OPTION_SYSTEMWIDE_PIP = 2; + public static final int OPTION_MULTI_AUDIO = 3; + public static final int OPTION_MORE_CHANNELS = 4; + public static final int OPTION_DEVELOPER = 5; + public static final int OPTION_SETTINGS = 6; private final Context mContext; private final SparseArray mOptionChangedListeners = new SparseArray<>(); private String mClosedCaptionsLanguage; private int mDisplayMode; - private boolean mPip; private String mMultiAudio; - private String mPipInput; - private boolean mPipSwap; - @PipSound private int mPipSound; - @PipLayout private int mPipLayout; - @PipSize private int mPipSize; public TvOptionsManager(Context context) { mContext = context; } - public String getOptionString(int option) { + /** + * Returns a suitable displayed string for the given option type under current settings. + * @param option the type of option, should be one of {@link OptionType}. + */ + public String getOptionString(@OptionType int option) { switch (option) { case OPTION_CLOSED_CAPTIONS: if (mClosedCaptionsLanguage == null) { @@ -77,101 +71,48 @@ public class TvOptionsManager { .isDisplayModeAvailable(mDisplayMode) ? DisplayMode.getLabel(mDisplayMode, mContext) : DisplayMode.getLabel(DisplayMode.MODE_NORMAL, mContext); - case OPTION_IN_APP_PIP: - return mContext.getString( - mPip ? R.string.options_item_pip_on : R.string.options_item_pip_off); case OPTION_MULTI_AUDIO: return mMultiAudio; - case OPTION_PIP_INPUT: - return mPipInput; - case OPTION_PIP_SWAP: - return mContext.getString(mPipSwap ? R.string.pip_options_item_swap_on - : R.string.pip_options_item_swap_off); - case OPTION_PIP_SOUND: - if (mPipSound == TvSettings.PIP_SOUND_MAIN) { - return mContext.getString(R.string.pip_options_item_sound_main); - } else if (mPipSound == TvSettings.PIP_SOUND_PIP_WINDOW) { - return mContext.getString(R.string.pip_options_item_sound_pip_window); - } - break; - case OPTION_PIP_LAYOUT: - if (mPipLayout == TvSettings.PIP_LAYOUT_BOTTOM_RIGHT) { - return mContext.getString(R.string.pip_options_item_layout_bottom_right); - } else if (mPipLayout == TvSettings.PIP_LAYOUT_TOP_RIGHT) { - return mContext.getString(R.string.pip_options_item_layout_top_right); - } else if (mPipLayout == TvSettings.PIP_LAYOUT_TOP_LEFT) { - return mContext.getString(R.string.pip_options_item_layout_top_left); - } else if (mPipLayout == TvSettings.PIP_LAYOUT_BOTTOM_LEFT) { - return mContext.getString(R.string.pip_options_item_layout_bottom_left); - } else if (mPipLayout == TvSettings.PIP_LAYOUT_SIDE_BY_SIDE) { - return mContext.getString(R.string.pip_options_item_layout_side_by_side); - } - break; - case OPTION_PIP_SIZE: - if (mPipSize == TvSettings.PIP_SIZE_BIG) { - return mContext.getString(R.string.pip_options_item_size_big); - } else if (mPipSize == TvSettings.PIP_SIZE_SMALL) { - return mContext.getString(R.string.pip_options_item_size_small); - } - break; } return ""; } - public void onClosedCaptionsChanged(TvTrackInfo track) { - mClosedCaptionsLanguage = (track == null) ? null - : (track.getLanguage() != null) ? track.getLanguage() - : mContext.getString(R.string.default_language); + /** + * Handles changing selection of closed caption. + */ + public void onClosedCaptionsChanged(TvTrackInfo track, int trackIndex) { + mClosedCaptionsLanguage = (track == null) ? + null : (track.getLanguage() != null) ? track.getLanguage() + : mContext.getString(R.string.closed_caption_unknown_language, trackIndex + 1); notifyOptionChanged(OPTION_CLOSED_CAPTIONS); } + /** + * Handles changing selection of display mode. + */ public void onDisplayModeChanged(int displayMode) { mDisplayMode = displayMode; notifyOptionChanged(OPTION_DISPLAY_MODE); } - public void onPipChanged(boolean pip) { - mPip = pip; - notifyOptionChanged(OPTION_IN_APP_PIP); - } - + /** + * Handles changing selection of multi-audio. + */ public void onMultiAudioChanged(String multiAudio) { mMultiAudio = multiAudio; notifyOptionChanged(OPTION_MULTI_AUDIO); } - public void onPipInputChanged(String pipInput) { - mPipInput = pipInput; - notifyOptionChanged(OPTION_PIP_INPUT); - } - - public void onPipSwapChanged(boolean pipSwap) { - mPipSwap = pipSwap; - notifyOptionChanged(OPTION_PIP_SWAP); - } - - public void onPipSoundChanged(@PipSound int pipSound) { - mPipSound = pipSound; - notifyOptionChanged(OPTION_PIP_SOUND); - } - - public void onPipLayoutChanged(@PipLayout int pipLayout) { - mPipLayout = pipLayout; - notifyOptionChanged(OPTION_PIP_LAYOUT); - } - - public void onPipSizeChanged(@PipSize int pipSize) { - mPipSize = pipSize; - notifyOptionChanged(OPTION_PIP_SIZE); - } - - private void notifyOptionChanged(int option) { + private void notifyOptionChanged(@OptionType int option) { OptionChangedListener listener = mOptionChangedListeners.get(option); if (listener != null) { - listener.onOptionChanged(getOptionString(option)); + listener.onOptionChanged(option, getOptionString(option)); } } + /** + * Sets listeners to changes of the given option type. + */ public void setOptionChangedListener(int option, OptionChangedListener listener) { mOptionChangedListeners.put(option, listener); } @@ -180,6 +121,6 @@ public class TvOptionsManager { * An interface used to monitor option changes. */ public interface OptionChangedListener { - void onOptionChanged(String newOption); + void onOptionChanged(@OptionType int optionType, String newString); } -} +} \ No newline at end of file diff --git a/src/com/android/tv/analytics/DurationTimer.java b/src/com/android/tv/analytics/DurationTimer.java deleted file mode 100644 index ad2d91f8..00000000 --- a/src/com/android/tv/analytics/DurationTimer.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.analytics; - -import android.os.SystemClock; - -/** - * Times a duration. - */ -public final class DurationTimer { - public static final long TIME_NOT_SET = -1; - - private long startTimeMs = TIME_NOT_SET; - - /** - * Returns true if the timer is running. - */ - public boolean isRunning() { - return startTimeMs != TIME_NOT_SET; - } - - /** - * Start the timer. - */ - public void start() { - startTimeMs = SystemClock.elapsedRealtime(); - } - - /** - * Returns the current duration in milliseconds or {@link #TIME_NOT_SET} if the timer is not - * running. - */ - public long getDuration() { - return isRunning() ? SystemClock.elapsedRealtime() - startTimeMs : TIME_NOT_SET; - } - - /** - * Stops the timer and resets its value to {@link #TIME_NOT_SET}. - * - * @return the current duration in milliseconds or {@link #TIME_NOT_SET} if the timer is not - * running. - */ - public long reset() { - long duration = getDuration(); - startTimeMs = TIME_NOT_SET; - return duration; - } -} diff --git a/src/com/android/tv/data/Channel.java b/src/com/android/tv/data/Channel.java index 30f84236..4da56311 100644 --- a/src/com/android/tv/data/Channel.java +++ b/src/com/android/tv/data/Channel.java @@ -51,6 +51,16 @@ public final class Channel { public static final int LOAD_IMAGE_TYPE_APP_LINK_ICON = 2; public static final int LOAD_IMAGE_TYPE_APP_LINK_POSTER_ART = 3; + /** + * Compares the channel numbers of channels which belong to the same input. + */ + public static final Comparator CHANNEL_NUMBER_COMPARATOR = new Comparator() { + @Override + public int compare(Channel lhs, Channel rhs) { + return ChannelNumber.compare(lhs.getDisplayNumber(), rhs.getDisplayNumber()); + } + }; + /** * When a TIS doesn't provide any information about app link, and it doesn't have a leanback * launch intent, there will be no app link card for the TIS. @@ -87,8 +97,14 @@ public final class Channel { TvContract.Channels.COLUMN_APP_LINK_ICON_URI, TvContract.Channels.COLUMN_APP_LINK_POSTER_ART_URI, TvContract.Channels.COLUMN_APP_LINK_INTENT_URI, + TvContract.Channels.COLUMN_INTERNAL_PROVIDER_FLAG2, // Only used in bundled input }; + /** + * Channel number delimiter between major and minor parts. + */ + public static final char CHANNEL_NUMBER_DELIMITER = '-'; + /** * Creates {@code Channel} object from cursor. * @@ -103,7 +119,7 @@ public final class Channel { channel.mPackageName = Utils.intern(cursor.getString(index++)); channel.mInputId = Utils.intern(cursor.getString(index++)); channel.mType = Utils.intern(cursor.getString(index++)); - channel.mDisplayNumber = cursor.getString(index++); + channel.mDisplayNumber = normalizeDisplayNumber(cursor.getString(index++)); channel.mDisplayName = cursor.getString(index++); channel.mDescription = cursor.getString(index++); channel.mVideoFormat = Utils.intern(cursor.getString(index++)); @@ -114,17 +130,29 @@ public final class Channel { channel.mAppLinkIconUri = cursor.getString(index++); channel.mAppLinkPosterArtUri = cursor.getString(index++); channel.mAppLinkIntentUri = cursor.getString(index++); + if (Utils.isBundledInput(channel.mInputId)) { + channel.mRecordingProhibited = cursor.getInt(index++) != 0; + } return channel; } /** - * Creates a {@link Channel} object from the DVR database. + * Replaces the channel number separator with dash('-'). */ - public static Channel fromDvrCursor(Cursor c) { - Channel channel = new Channel(); - int index = -1; - channel.mDvrId = c.getLong(++index); - return channel; + public static String normalizeDisplayNumber(String string) { + if (!TextUtils.isEmpty(string)) { + int length = string.length(); + for (int i = 0; i < length; i++) { + char c = string.charAt(i); + if (c == '.' || Character.isWhitespace(c) + || Character.getType(c) == Character.DASH_PUNCTUATION) { + StringBuilder sb = new StringBuilder(string); + sb.setCharAt(i, CHANNEL_NUMBER_DELIMITER); + return sb.toString(); + } + } + } + return string; } /** ID of this channel. Matches to BaseColumns._ID. */ @@ -147,8 +175,10 @@ public final class Channel { private String mAppLinkIntentUri; private Intent mAppLinkIntent; private int mAppLinkType; + private String mLogoUri; + private boolean mRecordingProhibited; - private long mDvrId; + private boolean mChannelLogoExist; private Channel() { // Do nothing. @@ -230,10 +260,14 @@ public final class Channel { } /** - * Returns an ID in DVR database. + * Returns channel logo uri which is got from cloud, it's used only for ChannelLogoFetcher. */ - public long getDvrId() { - return mDvrId; + public String getLogoUri() { + return mLogoUri; + } + + public boolean isRecordingProhibited() { + return mRecordingProhibited; } /** @@ -278,6 +312,13 @@ public final class Channel { mLocked = locked; } + /** + * Sets channel logo uri which is got from cloud. + */ + public void setLogoUri(String logoUri) { + mLogoUri = logoUri; + } + /** * Check whether {@code other} has same read-only channel info as this. But, it cannot check two * channels have same logos. It also excludes browsable and locked, because two fields are @@ -298,7 +339,8 @@ public final class Channel { && mAppLinkColor == other.mAppLinkColor && Objects.equals(mAppLinkIconUri, other.mAppLinkIconUri) && Objects.equals(mAppLinkPosterArtUri, other.mAppLinkPosterArtUri) - && Objects.equals(mAppLinkIntentUri, other.mAppLinkIntentUri); + && Objects.equals(mAppLinkIntentUri, other.mAppLinkIntentUri) + && Objects.equals(mRecordingProhibited, other.mRecordingProhibited); } @Override @@ -315,7 +357,8 @@ public final class Channel { + ", isPassthrough=" + mIsPassthrough + ", browsable=" + mBrowsable + ", locked=" + mLocked - + ", appLinkText=" + mAppLinkText + "}"; + + ", appLinkText=" + mAppLinkText + + ", recordingProhibited=" + mRecordingProhibited + "}"; } void copyFrom(Channel other) { @@ -340,6 +383,8 @@ public final class Channel { mAppLinkIntentUri = other.mAppLinkIntentUri; mAppLinkIntent = other.mAppLinkIntent; mAppLinkType = other.mAppLinkType; + mRecordingProhibited = other.mRecordingProhibited; + mChannelLogoExist = other.mChannelLogoExist; } /** @@ -389,8 +434,6 @@ public final class Channel { mChannel.mDisplayName = "name"; mChannel.mDescription = "description"; mChannel.mBrowsable = true; - mChannel.mLocked = false; - mChannel.mIsPassthrough = false; } public Builder(Channel other) { @@ -422,7 +465,7 @@ public final class Channel { @VisibleForTesting public Builder setDisplayNumber(String displayNumber) { - mChannel.mDisplayNumber = displayNumber; + mChannel.mDisplayNumber = normalizeDisplayNumber(displayNumber); return this; } @@ -485,6 +528,11 @@ public final class Channel { return this; } + public Builder setRecordingProhibited(boolean recordingProhibited) { + mChannel.mRecordingProhibited = recordingProhibited; + return this; + } + public Channel build() { Channel channel = new Channel(); channel.copyFrom(mChannel); @@ -523,6 +571,21 @@ public final class Channel { ImageLoader.loadBitmap(context, uriString, maxWidth, maxHeight, callback); } + /** + * Sets if the channel logo exists. This method should be only called from + * {@link ChannelDataManager}. + */ + void setChannelLogoExist(boolean exist) { + mChannelLogoExist = exist; + } + + /** + * Returns if channel logo exists. + */ + public boolean channelLogoExists() { + return mChannelLogoExist; + } + /** * Returns the type of app link for this channel. * It returns {@link #APP_LINK_TYPE_CHANNEL} if the channel has a non null app link text and @@ -655,4 +718,4 @@ public final class Channel { return label; } } -} +} \ No newline at end of file diff --git a/src/com/android/tv/data/ChannelDataManager.java b/src/com/android/tv/data/ChannelDataManager.java index 6f9ea6d7..eb3871fc 100644 --- a/src/com/android/tv/data/ChannelDataManager.java +++ b/src/com/android/tv/data/ChannelDataManager.java @@ -21,10 +21,14 @@ import android.content.ContentValues; import android.content.Context; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; +import android.content.res.AssetFileDescriptor; import android.database.ContentObserver; +import android.database.sqlite.SQLiteException; import android.media.tv.TvContract; import android.media.tv.TvContract.Channels; import android.media.tv.TvInputManager.TvInputCallback; +import android.net.Uri; +import android.os.AsyncTask; import android.os.Handler; import android.os.Looper; import android.os.Message; @@ -43,6 +47,8 @@ import com.android.tv.util.PermissionUtils; import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.Utils; +import java.io.FileNotFoundException; +import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -192,7 +198,6 @@ public class ChannelDataManager { mStarted = false; mDbLoadFinished = false; - ChannelLogoFetcher.stopFetchingChannelLogos(); mInputManager.removeCallback(mTvInputCallback); mContentResolver.unregisterContentObserver(mChannelObserver); mHandler.removeCallbacksAndMessages(null); @@ -590,6 +595,36 @@ public class ChannelDataManager { } } + private class checkChannelLogoExistTask extends AsyncTask { + private final Channel mChannel; + + public checkChannelLogoExistTask(Channel channel) { + mChannel = channel; + } + + @Override + protected Boolean doInBackground(Void... params) { + boolean result = false; + try { + AssetFileDescriptor f = mContext.getContentResolver().openAssetFileDescriptor( + TvContract.buildChannelLogoUri(mChannel.getId()), "r"); + result = true; + f.close(); + } catch (SQLiteException | IOException | NullPointerException e) { + // File not found or asset file not found. + } + return result; + } + + @Override + protected void onPostExecute(Boolean result) { + ChannelWrapper wrapper = mChannelWrapperMap.get(mChannel.getId()); + if (wrapper != null) { + wrapper.mChannel.setChannelLogoExist(result); + } + } + } + private final class QueryAllChannelsTask extends AsyncDbTask.AsyncChannelQueryTask { public QueryAllChannelsTask(ContentResolver contentResolver) { @@ -625,6 +660,8 @@ public class ChannelDataManager { boolean newlyAdded = !removedChannelIds.remove(channelId); ChannelWrapper channelWrapper; if (newlyAdded) { + new checkChannelLogoExistTask(channel) + .executeOnExecutor(AsyncTask.SERIAL_EXECUTOR); channelWrapper = new ChannelWrapper(channel); mChannelWrapperMap.put(channel.getId(), channelWrapper); if (!channelWrapper.mInputRemoved) { @@ -640,9 +677,9 @@ public class ChannelDataManager { // {@link #applyUpdatedValuesToDb} is called. Therefore, the value // between DB and ChannelDataManager could be different for a while. // Therefore, we'll keep the values in ChannelDataManager. - channelWrapper.mChannel.copyFrom(channel); channel.setBrowsable(oldChannel.isBrowsable()); channel.setLocked(oldChannel.isLocked()); + channelWrapper.mChannel.copyFrom(channel); if (!channelWrapper.mInputRemoved) { channelUpdated = true; updatedChannelWrappers.add(channelWrapper); @@ -693,7 +730,6 @@ public class ChannelDataManager { r.run(); } mPostRunnablesAfterChannelUpdate.clear(); - ChannelLogoFetcher.startFetchingChannelLogos(mContext); } } diff --git a/src/com/android/tv/data/ChannelLogoFetcher.java b/src/com/android/tv/data/ChannelLogoFetcher.java index 5a549f83..256ecdb2 100644 --- a/src/com/android/tv/data/ChannelLogoFetcher.java +++ b/src/com/android/tv/data/ChannelLogoFetcher.java @@ -16,155 +16,68 @@ package com.android.tv.data; +import android.content.ContentProviderOperation; import android.content.Context; -import android.database.Cursor; +import android.content.OperationApplicationException; +import android.content.SharedPreferences; import android.graphics.Bitmap.CompressFormat; import android.media.tv.TvContract; -import android.media.tv.TvContract.Channels; import android.net.Uri; import android.os.AsyncTask; -import android.support.annotation.WorkerThread; +import android.os.RemoteException; +import android.support.annotation.AnyThread; import android.text.TextUtils; import android.util.Log; -import com.android.tv.util.AsyncDbTask; +import com.android.tv.common.SharedPreferencesUtils; import com.android.tv.util.BitmapUtils; import com.android.tv.util.BitmapUtils.ScaledBitmapInfo; import com.android.tv.util.PermissionUtils; -import java.io.BufferedReader; import java.io.IOException; -import java.io.InputStreamReader; import java.io.OutputStream; import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Locale; import java.util.Map; -import java.util.Set; +import java.util.List; /** - * Utility class for TMS data. - * This class is thread safe. + * Fetches channel logos from the cloud into the database. It's for the channels which have no logos + * or need update logos. This class is thread safe. */ public class ChannelLogoFetcher { private static final String TAG = "ChannelLogoFetcher"; private static final boolean DEBUG = false; - /** - * The name of the file which contains the TMS data. - * The file has multiple records and each of them is a string separated by '|' like - * STATION_NAME|SHORT_NAME|CALL_SIGN|LOGO_URI. - */ - private static final String TMS_US_TABLE_FILE = "tms_us.table"; - private static final String TMS_KR_TABLE_FILE = "tms_kr.table"; - private static final String FIELD_SEPARATOR = "\\|"; - private static final String NAME_SEPARATOR_FOR_TMS = "\\(|\\)|\\{|\\}|\\[|\\]"; - private static final String NAME_SEPARATOR_FOR_DB = "\\W"; - private static final int INDEX_NAME = 0; - private static final int INDEX_SHORT_NAME = 1; - private static final int INDEX_CALL_SIGN = 2; - private static final int INDEX_LOGO_URI = 3; - - private static final String COLUMN_CHANNEL_LOGO = "logo"; + private static final String PREF_KEY_IS_FIRST_TIME_FETCH_CHANNEL_LOGO = + "is_first_time_fetch_channel_logo"; - private static final Object sLock = new Object(); - private static final Set sChannelIdBlackListSet = new HashSet<>(); - private static LoadChannelTask sQueryTask; private static FetchLogoTask sFetchTask; /** - * Fetch the channel logos from TMS data and insert them into TvProvider. + * Fetches the channel logos from the cloud data and insert them into TvProvider. * The previous task is canceled and a new task starts. */ - public static void startFetchingChannelLogos(Context context) { + @AnyThread + public static synchronized void startFetchingChannelLogos( + Context context, List channels) { if (!PermissionUtils.hasAccessAllEpg(context)) { // TODO: support this feature for non-system LC app. b/23939816 return; } - synchronized (sLock) { - stopFetchingChannelLogos(); - if (DEBUG) Log.d(TAG, "Request to start fetching logos."); - sQueryTask = new LoadChannelTask(context); - sQueryTask.executeOnDbThread(); + if (sFetchTask != null) { + sFetchTask.cancel(true); } - } - - /** - * Stops the current fetching tasks. This can be called when the Activity pauses. - */ - public static void stopFetchingChannelLogos() { - synchronized (sLock) { - if (DEBUG) Log.d(TAG, "Request to stop fetching logos."); - if (sQueryTask != null) { - sQueryTask.cancel(true); - sQueryTask = null; - } - if (sFetchTask != null) { - sFetchTask.cancel(true); - sFetchTask = null; - } + if (DEBUG) Log.d(TAG, "Request to start fetching logos."); + if (channels == null || channels.isEmpty()) { + return; } + sFetchTask = new FetchLogoTask(context, channels); + sFetchTask.execute(); } private ChannelLogoFetcher() { } - private static final class LoadChannelTask extends AsyncDbTask> { - private final Context mContext; - - public LoadChannelTask(Context context) { - mContext = context; - } - - @Override - protected List doInBackground(Void... arg) { - // Load channels which doesn't have channel logos. - if (DEBUG) Log.d(TAG, "Starts loading the channels from DB"); - String[] projection = - new String[] { Channels._ID, Channels.COLUMN_DISPLAY_NAME }; - String selection = COLUMN_CHANNEL_LOGO + " IS NULL AND " - + Channels.COLUMN_PACKAGE_NAME + "=?"; - String[] selectionArgs = new String[] { mContext.getPackageName() }; - try (Cursor c = mContext.getContentResolver().query(Channels.CONTENT_URI, - projection, selection, selectionArgs, null)) { - if (c == null) { - Log.e(TAG, "Query returns null cursor", new RuntimeException()); - return null; - } - List channels = new ArrayList<>(); - while (!isCancelled() && c.moveToNext()) { - long channelId = c.getLong(0); - if (sChannelIdBlackListSet.contains(channelId)) { - continue; - } - channels.add(new Channel.Builder().setId(c.getLong(0)) - .setDisplayName(c.getString(1).toUpperCase(Locale.getDefault())) - .build()); - } - return channels; - } - } - - @Override - protected void onPostExecute(List channels) { - synchronized (sLock) { - if (DEBUG) { - int count = channels == null ? 0 : channels.size(); - Log.d(TAG, count + " channels are loaded"); - } - if (sQueryTask == this) { - sQueryTask = null; - if (channels != null && !channels.isEmpty()) { - sFetchTask = new FetchLogoTask(mContext, channels); - sFetchTask.execute(); - } - } - } - } - } - private static final class FetchLogoTask extends AsyncTask { private final Context mContext; private final List mChannels; @@ -180,83 +93,53 @@ public class ChannelLogoFetcher { if (DEBUG) Log.d(TAG, "Fetching the channel logos has been canceled"); return null; } - // Load the TMS table data. - if (DEBUG) Log.d(TAG, "Loads TMS data"); - Map channelNameLogoUriMap = new HashMap<>(); - try { - channelNameLogoUriMap.putAll(readTmsFile(mContext, TMS_US_TABLE_FILE)); - if (isCancelled()) { - if (DEBUG) Log.d(TAG, "Fetching the channel logos has been canceled"); - return null; + List channelsToUpdate = new ArrayList<>(); + List channelsToRemove = new ArrayList<>(); + // Updates or removes the logo by comparing the logo uri which is got from the cloud + // and the stored one. And we assume that the data got form the cloud is 100% + // correct and completed. + SharedPreferences sharedPreferences = + mContext.getSharedPreferences( + SharedPreferencesUtils.SHARED_PREF_CHANNEL_LOGO_URIS, + Context.MODE_PRIVATE); + SharedPreferences.Editor sharedPreferencesEditor = sharedPreferences.edit(); + Map uncheckedChannels = sharedPreferences.getAll(); + boolean isFirstTimeFetchChannelLogo = sharedPreferences.getBoolean( + PREF_KEY_IS_FIRST_TIME_FETCH_CHANNEL_LOGO, true); + // Iterating channels. + for (Channel channel : mChannels) { + String channelIdString = Long.toString(channel.getId()); + String storedChannelLogoUri = (String) uncheckedChannels.remove(channelIdString); + if (!TextUtils.isEmpty(channel.getLogoUri()) + && !TextUtils.equals(storedChannelLogoUri, channel.getLogoUri())) { + channelsToUpdate.add(channel); + sharedPreferencesEditor.putString(channelIdString, channel.getLogoUri()); + } else if (TextUtils.isEmpty(channel.getLogoUri()) + && (!TextUtils.isEmpty(storedChannelLogoUri) + || isFirstTimeFetchChannelLogo)) { + channelsToRemove.add(channel); + sharedPreferencesEditor.remove(channelIdString); } - channelNameLogoUriMap.putAll(readTmsFile(mContext, TMS_KR_TABLE_FILE)); - } catch (IOException e) { - Log.e(TAG, "Loading TMS data failed.", e); - return null; } - if (isCancelled()) { - if (DEBUG) Log.d(TAG, "Fetching the channel logos has been canceled"); - return null; + + // Removes non existing channels from SharedPreferences. + for (String channelId : uncheckedChannels.keySet()) { + sharedPreferencesEditor.remove(channelId); } - // Iterating channels. - for (Channel channel : mChannels) { + // Updates channel logos. + for (Channel channel : channelsToUpdate) { if (isCancelled()) { if (DEBUG) Log.d(TAG, "Fetching the channel logos has been canceled"); return null; } - // Download the channel logo. - if (TextUtils.isEmpty(channel.getDisplayName())) { - if (DEBUG) { - Log.d(TAG, "The channel with ID (" + channel.getId() - + ") doesn't have the display name."); - } - sChannelIdBlackListSet.add(channel.getId()); - continue; - } - String channelName = channel.getDisplayName().trim(); - String logoUri = channelNameLogoUriMap.get(channelName); - if (TextUtils.isEmpty(logoUri)) { - if (DEBUG) { - Log.d(TAG, "Can't find a logo URI for channel '" + channelName + "'"); - } - // Find the candidate names. If the channel name is CNN-HD, then find CNNHD - // and CNN. Or if the channel name is KQED+, then find KQED. - String[] splitNames = channelName.split(NAME_SEPARATOR_FOR_DB); - if (splitNames.length > 1) { - StringBuilder sb = new StringBuilder(); - for (String splitName : splitNames) { - sb.append(splitName); - } - logoUri = channelNameLogoUriMap.get(sb.toString()); - if (DEBUG) { - if (TextUtils.isEmpty(logoUri)) { - Log.d(TAG, "Can't find a logo URI for channel '" + sb.toString() - + "'"); - } - } - } - if (TextUtils.isEmpty(logoUri) - && splitNames[0].length() != channelName.length()) { - logoUri = channelNameLogoUriMap.get(splitNames[0]); - if (DEBUG) { - if (TextUtils.isEmpty(logoUri)) { - Log.d(TAG, "Can't find a logo URI for channel '" + splitNames[0] - + "'"); - } - } - } - } - if (TextUtils.isEmpty(logoUri)) { - sChannelIdBlackListSet.add(channel.getId()); - continue; - } + // Downloads the channel logo. + String logoUri = channel.getLogoUri(); ScaledBitmapInfo bitmapInfo = BitmapUtils.decodeSampledBitmapFromUriString( mContext, logoUri, Integer.MAX_VALUE, Integer.MAX_VALUE); if (bitmapInfo == null) { Log.e(TAG, "Failed to load bitmap. {channelName=" + channel.getDisplayName() + ", " + "logoUri=" + logoUri + "}"); - sChannelIdBlackListSet.add(channel.getId()); continue; } if (isCancelled()) { @@ -264,12 +147,15 @@ public class ChannelLogoFetcher { return null; } - // Insert the logo to DB. + // Inserts the logo to DB. Uri dstLogoUri = TvContract.buildChannelLogoUri(channel.getId()); try (OutputStream os = mContext.getContentResolver().openOutputStream(dstLogoUri)) { bitmapInfo.bitmap.compress(CompressFormat.PNG, 100, os); } catch (IOException e) { Log.e(TAG, "Failed to write " + logoUri + " to " + dstLogoUri, e); + // Removes it from the shared preference for the failed channels to make it + // retry next time. + sharedPreferencesEditor.remove(Long.toString(channel.getId())); continue; } if (DEBUG) { @@ -277,63 +163,30 @@ public class ChannelLogoFetcher { + dstLogoUri + "}"); } } - if (DEBUG) Log.d(TAG, "Fetching logos has been finished successfully."); - return null; - } - @WorkerThread - private Map readTmsFile(Context context, String fileName) - throws IOException { - try (BufferedReader reader = new BufferedReader(new InputStreamReader( - context.getAssets().open(fileName)))) { - Map channelNameLogoUriMap = new HashMap<>(); - String line; - while ((line = reader.readLine()) != null && !isCancelled()) { - String[] data = line.split(FIELD_SEPARATOR); - if (data.length != INDEX_LOGO_URI + 1) { - if (DEBUG) Log.d(TAG, "Invalid or comment row: " + line); - continue; - } - addChannelNames(channelNameLogoUriMap, - data[INDEX_NAME].toUpperCase(Locale.getDefault()), - data[INDEX_LOGO_URI]); - addChannelNames(channelNameLogoUriMap, - data[INDEX_SHORT_NAME].toUpperCase(Locale.getDefault()), - data[INDEX_LOGO_URI]); - addChannelNames(channelNameLogoUriMap, - data[INDEX_CALL_SIGN].toUpperCase(Locale.getDefault()), - data[INDEX_LOGO_URI]); + // Removes the logos for the channels that have logos before but now + // their logo uris are null. + boolean deleteChannelLogoFailed = false; + if (!channelsToRemove.isEmpty()) { + ArrayList ops = new ArrayList<>(); + for (Channel channel : channelsToRemove) { + ops.add(ContentProviderOperation.newDelete( + TvContract.buildChannelLogoUri(channel.getId())).build()); + } + try { + mContext.getContentResolver().applyBatch(TvContract.AUTHORITY, ops); + } catch (RemoteException | OperationApplicationException e) { + deleteChannelLogoFailed = true; + Log.e(TAG, "Error deleting obsolete channels", e); } - return channelNameLogoUriMap; } - } - - private void addChannelNames(Map channelNameLogoUriMap, String channelName, - String logoUri) { - if (!TextUtils.isEmpty(channelName)) { - channelNameLogoUriMap.put(channelName, logoUri); - // Find the candidate names. - // If the name is like "W05AAD (W05AA-D)", then split the names into "W05AAD" and - // "W05AA-D" - String[] splitNames = channelName.split(NAME_SEPARATOR_FOR_TMS); - if (splitNames.length > 1) { - for (String name : splitNames) { - name = name.trim(); - if (channelNameLogoUriMap.get(name) == null) { - channelNameLogoUriMap.put(name, logoUri); - } - } - } - } - } - - @Override - protected void onPostExecute(Void result) { - synchronized (sLock) { - if (sFetchTask == this) { - sFetchTask = null; - } + if (isFirstTimeFetchChannelLogo && !deleteChannelLogoFailed) { + sharedPreferencesEditor.putBoolean( + PREF_KEY_IS_FIRST_TIME_FETCH_CHANNEL_LOGO, false); } + sharedPreferencesEditor.commit(); + if (DEBUG) Log.d(TAG, "Fetching logos has been finished successfully."); + return null; } } } diff --git a/src/com/android/tv/data/ChannelNumber.java b/src/com/android/tv/data/ChannelNumber.java index 59021609..29054aa5 100644 --- a/src/com/android/tv/data/ChannelNumber.java +++ b/src/com/android/tv/data/ChannelNumber.java @@ -17,37 +17,38 @@ package com.android.tv.data; import android.support.annotation.NonNull; +import android.text.TextUtils; import android.view.KeyEvent; +import com.android.tv.util.StringUtils; + +import java.util.Objects; + /** * A convenience class to handle channel number. */ public final class ChannelNumber implements Comparable { - public static final String PRIMARY_CHANNEL_DELIMITER = "-"; - public static final String[] CHANNEL_DELIMITERS = {"-", ".", " "}; - private static final int[] CHANNEL_DELIMITER_KEYCODES = { KeyEvent.KEYCODE_MINUS, KeyEvent.KEYCODE_NUMPAD_SUBTRACT, KeyEvent.KEYCODE_PERIOD, KeyEvent.KEYCODE_NUMPAD_DOT, KeyEvent.KEYCODE_SPACE }; + /** The major part of the channel number. */ public String majorNumber; + /** The flag which indicates whether it has a delimiter or not. */ public boolean hasDelimiter; + /** The major part of the channel number. */ public String minorNumber; public ChannelNumber() { reset(); } - public ChannelNumber(String major, boolean hasDelimiter, String minor) { - setChannelNumber(major, hasDelimiter, minor); - } - public void reset() { setChannelNumber("", false, ""); } - public void setChannelNumber(String majorNumber, boolean hasDelimiter, String minorNumber) { + private void setChannelNumber(String majorNumber, boolean hasDelimiter, String minorNumber) { this.majorNumber = majorNumber; this.hasDelimiter = hasDelimiter; this.minorNumber = minorNumber; @@ -56,7 +57,7 @@ public final class ChannelNumber implements Comparable { @Override public String toString() { if (hasDelimiter) { - return majorNumber + PRIMARY_CHANNEL_DELIMITER + minorNumber; + return majorNumber + Channel.CHANNEL_NUMBER_DELIMITER + minorNumber; } return majorNumber; } @@ -75,6 +76,22 @@ public final class ChannelNumber implements Comparable { return major - opponentMajor; } + @Override + public boolean equals(Object obj) { + if (obj instanceof ChannelNumber) { + ChannelNumber channelNumber = (ChannelNumber) obj; + return TextUtils.equals(majorNumber, channelNumber.majorNumber) + && TextUtils.equals(minorNumber, channelNumber.minorNumber) + && hasDelimiter == channelNumber.hasDelimiter; + } + return super.equals(obj); + } + + @Override + public int hashCode() { + return Objects.hash(majorNumber, hasDelimiter, minorNumber); + } + public static boolean isChannelNumberDelimiterKey(int keyCode) { for (int delimiterKeyCode : CHANNEL_DELIMITER_KEYCODES) { if (delimiterKeyCode == keyCode) { @@ -84,22 +101,22 @@ public final class ChannelNumber implements Comparable { return false; } + /** + * Returns the ChannelNumber instance. + *

+ * Note that all the channel number argument should be normalized by + * {@link Channel#normalizeDisplayNumber}. The channels retrieved from + * {@link ChannelDataManager} are already normalized. + */ public static ChannelNumber parseChannelNumber(String number) { if (number == null) { return null; } ChannelNumber ret = new ChannelNumber(); - int indexOfDelimiter = -1; - for (String delimiter : CHANNEL_DELIMITERS) { - indexOfDelimiter = number.indexOf(delimiter); - if (indexOfDelimiter >= 0) { - break; - } - } + int indexOfDelimiter = number.indexOf(Channel.CHANNEL_NUMBER_DELIMITER); if (indexOfDelimiter == 0 || indexOfDelimiter == number.length() - 1) { return null; - } - if (indexOfDelimiter < 0) { + } else if (indexOfDelimiter < 0) { ret.majorNumber = number; if (!isInteger(ret.majorNumber)) { return null; @@ -115,25 +132,31 @@ public final class ChannelNumber implements Comparable { return ret; } + /** + * Compares the channel numbers. + *

+ * Note that all the channel number arguments should be normalized by + * {@link Channel#normalizeDisplayNumber}. The channels retrieved from + * {@link ChannelDataManager} are already normalized. + */ public static int compare(String lhs, String rhs) { ChannelNumber lhsNumber = parseChannelNumber(lhs); ChannelNumber rhsNumber = parseChannelNumber(rhs); + // Null first if (lhsNumber == null && rhsNumber == null) { - return 0; + return StringUtils.compare(lhs, rhs); } else if (lhsNumber == null /* && rhsNumber != null */) { return -1; - } else if (lhsNumber != null && rhsNumber == null) { + } else if (rhsNumber == null) { return 1; } return lhsNumber.compareTo(rhsNumber); } - public static boolean isInteger(String string) { + private static boolean isInteger(String string) { try { Integer.parseInt(string); - } catch(NumberFormatException e) { - return false; - } catch(NullPointerException e) { + } catch(NumberFormatException | NullPointerException e) { return false; } return true; diff --git a/src/com/android/tv/data/InternalDataUtils.java b/src/com/android/tv/data/InternalDataUtils.java index 6054f089..e33ca18f 100644 --- a/src/com/android/tv/data/InternalDataUtils.java +++ b/src/com/android/tv/data/InternalDataUtils.java @@ -21,7 +21,7 @@ import android.text.TextUtils; import android.util.Log; import com.android.tv.data.Program.CriticScore; -import com.android.tv.dvr.RecordedProgram; +import com.android.tv.dvr.data.RecordedProgram; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; diff --git a/src/com/android/tv/data/StreamInfo.java b/src/com/android/tv/data/StreamInfo.java index fe461f14..709863cf 100644 --- a/src/com/android/tv/data/StreamInfo.java +++ b/src/com/android/tv/data/StreamInfo.java @@ -38,5 +38,9 @@ public interface StreamInfo { int getAudioChannelCount(); boolean hasClosedCaption(); boolean isVideoAvailable(); + /** + * Returns true, if video or audio is available. + */ + boolean isVideoOrAudioAvailable(); int getVideoUnavailableReason(); } diff --git a/src/com/android/tv/data/epg/EpgFetcher.java b/src/com/android/tv/data/epg/EpgFetcher.java index 3b093b6a..ddd68ad7 100644 --- a/src/com/android/tv/data/epg/EpgFetcher.java +++ b/src/com/android/tv/data/epg/EpgFetcher.java @@ -16,13 +16,11 @@ package com.android.tv.data.epg; -import android.Manifest; import android.annotation.SuppressLint; import android.content.ContentProviderOperation; import android.content.ContentValues; import android.content.Context; import android.content.OperationApplicationException; -import android.content.pm.PackageManager; import android.database.Cursor; import android.location.Address; import android.media.tv.TvContentRating; @@ -46,9 +44,11 @@ import com.android.tv.TvApplication; import com.android.tv.common.WeakHandler; import com.android.tv.data.Channel; import com.android.tv.data.ChannelDataManager; +import com.android.tv.data.ChannelLogoFetcher; import com.android.tv.data.InternalDataUtils; import com.android.tv.data.Lineup; import com.android.tv.data.Program; +import com.android.tv.tuner.util.PostalCodeUtils; import com.android.tv.util.LocationUtils; import com.android.tv.util.RecurringRunner; import com.android.tv.util.Utils; @@ -56,8 +56,10 @@ import com.android.tv.util.Utils; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Objects; import java.util.concurrent.TimeUnit; @@ -69,14 +71,27 @@ public class EpgFetcher { private static final boolean DEBUG = false; private static final int MSG_FETCH_EPG = 1; + private static final int MSG_FAST_FETCH_EPG = 2; private static final long EPG_PREFETCH_RECURRING_PERIOD_MS = TimeUnit.HOURS.toMillis(4); private static final long EPG_READER_INIT_WAIT_MS = TimeUnit.MINUTES.toMillis(1); private static final long LOCATION_INIT_WAIT_MS = TimeUnit.SECONDS.toMillis(10); private static final long LOCATION_ERROR_WAIT_MS = TimeUnit.HOURS.toMillis(1); + private static final long NO_INFO_FETCHED_WAIT_MS = TimeUnit.SECONDS.toMillis(10); private static final long PROGRAM_QUERY_DURATION = TimeUnit.DAYS.toMillis(30); + private static final long PROGRAM_FETCH_SHORT_DURATION_SEC = TimeUnit.HOURS.toSeconds(3); + private static final long PROGRAM_FETCH_LONG_DURATION_SEC = TimeUnit.DAYS.toSeconds(2) + + EPG_PREFETCH_RECURRING_PERIOD_MS / 1000; + + // This equals log2(EPG_PREFETCH_RECURRING_PERIOD_MS / NO_INFO_FETCHED_WAIT_MS + 1), + // since we will double waiting time every other trial, therefore this limit the maximum + // waiting time less than half of EPG_PREFETCH_RECURRING_PERIOD_MS. + private static final int NO_INFO_RETRY_LIMIT = 31 - Integer.numberOfLeadingZeros( + (int) (EPG_PREFETCH_RECURRING_PERIOD_MS / NO_INFO_FETCHED_WAIT_MS + 1)); + private static final int BATCH_OPERATION_COUNT = 100; + private static final int QUERY_CHANNEL_COUNT = 50; private static final String SUPPORTED_COUNTRY_CODE = Locale.US.getCountry(); private static final String CONTENT_RATING_SEPARATOR = ","; @@ -96,8 +111,11 @@ public class EpgFetcher { private EpgFetcherHandler mHandler; private RecurringRunner mRecurringRunner; private boolean mStarted; + private boolean mScanningChannels; + private int mFetchRetryCount; private long mLastEpgTimestamp = -1; + // @GuardedBy("this") private String mLineupId; public static synchronized EpgFetcher getInstance(Context context) { @@ -122,21 +140,33 @@ public class EpgFetcher { @Override public void onLoadFinished() { if (DEBUG) Log.d(TAG, "ChannelDataManager.onLoadFinished()"); - handleChannelChanged(); + if (!mScanningChannels) { + handleChannelChanged(); + } } @Override public void onChannelListUpdated() { if (DEBUG) Log.d(TAG, "ChannelDataManager.onChannelListUpdated()"); - handleChannelChanged(); + if (!mScanningChannels) { + handleChannelChanged(); + } } @Override public void onChannelBrowsableChanged() { if (DEBUG) Log.d(TAG, "ChannelDataManager.onChannelBrowsableChanged()"); - handleChannelChanged(); + if (!mScanningChannels) { + handleChannelChanged(); + } } }); + // Warm up to get address, because the first call of getCurrentAddress is usually failed. + try { + LocationUtils.getCurrentAddress(mContext); + } catch (SecurityException | IOException e) { + // Do nothing + } } private void handleChannelChanged() { @@ -145,7 +175,9 @@ public class EpgFetcher { stop(); } } else { - start(); + if (canStart()) { + start(); + } } } @@ -173,17 +205,14 @@ public class EpgFetcher { if (!TextUtils.isEmpty(getLastLineupId())) { return true; } - if (mContext.checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) - != PackageManager.PERMISSION_GRANTED) { - if (DEBUG) Log.d(TAG, "No permission to check the current location."); - return false; + if (!TextUtils.isEmpty(PostalCodeUtils.getLastPostalCode(mContext))) { + return true; } - try { Address address = LocationUtils.getCurrentAddress(mContext); if (address != null && !TextUtils.equals(address.getCountryCode(), SUPPORTED_COUNTRY_CODE)) { - if (DEBUG) Log.d(TAG, "Country not supported: " + address.getCountryCode()); + Log.i(TAG, "Country not supported: " + address.getCountryCode()); return false; } } catch (SecurityException e) { @@ -197,9 +226,13 @@ public class EpgFetcher { /** * Starts fetching EPG. + * + * @param resetNextRunTime if true, next run time is reset, so EPG will be fetched + * {@link #EPG_PREFETCH_RECURRING_PERIOD_MS} later. + * otherwise, EPG is fetched when this method is called. */ @MainThread - public void start() { + private void startInternal(boolean resetNextRunTime) { if (DEBUG) Log.d(TAG, "start()"); if (mStarted) { if (DEBUG) Log.d(TAG, "EpgFetcher thread already started."); @@ -215,19 +248,35 @@ public class EpgFetcher { mHandler = new EpgFetcherHandler(handlerThread.getLooper(), this); mRecurringRunner = new RecurringRunner(mContext, EPG_PREFETCH_RECURRING_PERIOD_MS, new EpgRunner(), null); - mRecurringRunner.start(); + mRecurringRunner.start(resetNextRunTime); if (DEBUG) Log.d(TAG, "EpgFetcher thread started successfully."); } + @MainThread + public void start() { + if (System.currentTimeMillis() - getLastUpdatedEpgTimestamp() > + EPG_PREFETCH_RECURRING_PERIOD_MS) { + startImmediately(false); + } else { + startInternal(false); + } + } + /** * Starts fetching EPG immediately if possible without waiting for the timer. + * + * @param clearStoredLineupId if true, stored lineup id will be clear before fetching EPG. */ @MainThread - public void startImmediately() { - start(); + public void startImmediately(boolean clearStoredLineupId) { + startInternal(true); if (mStarted) { + if (clearStoredLineupId) { + if (DEBUG) Log.d(TAG, "Clear stored lineup id: " + mLineupId); + setLastLineupId(null); + } if (DEBUG) Log.d(TAG, "Starting fetcher immediately"); - fetchEpg(); + postFetchRequest(true, 0); } } @@ -246,48 +295,71 @@ public class EpgFetcher { mHandler.getLooper().quit(); } - private void fetchEpg() { - fetchEpg(0); + /** + * Notifies EPG fetcher that channel scanning is started. + */ + @MainThread + public void onChannelScanStarted() { + stop(); + mScanningChannels = true; } - private void fetchEpg(long delay) { - mHandler.removeMessages(MSG_FETCH_EPG); - mHandler.sendEmptyMessageDelayed(MSG_FETCH_EPG, delay); + /** + * Notifies EPG fetcher that channel scanning is finished. + */ + @MainThread + public void onChannelScanFinished() { + mScanningChannels = false; + start(); + } + + private void postFetchRequest(boolean fastFetch, long delay) { + int msg = fastFetch ? MSG_FAST_FETCH_EPG : MSG_FETCH_EPG; + mHandler.removeMessages(msg); + mHandler.sendEmptyMessageDelayed(msg, delay); } private void onFetchEpg() { + onFetchEpg(false); + } + + private void onFetchEpg(boolean fastFetch) { if (DEBUG) Log.d(TAG, "Start fetching EPG."); if (!mEpgReader.isAvailable()) { - if (DEBUG) Log.d(TAG, "EPG reader is not temporarily available."); - fetchEpg(EPG_READER_INIT_WAIT_MS); + Log.i(TAG, "EPG reader is not temporarily available."); + postFetchRequest(fastFetch, EPG_READER_INIT_WAIT_MS); return; } String lineupId = getLastLineupId(); if (lineupId == null) { - Address address; try { - address = LocationUtils.getCurrentAddress(mContext); + PostalCodeUtils.updatePostalCode(mContext); } catch (IOException e) { - if (DEBUG) Log.d(TAG, "Couldn't get the current location.", e); - fetchEpg(LOCATION_ERROR_WAIT_MS); - return; + if (TextUtils.isEmpty(PostalCodeUtils.getLastPostalCode(mContext))) { + if (DEBUG) Log.d(TAG, "Couldn't get the current location.", e); + postFetchRequest(fastFetch, LOCATION_ERROR_WAIT_MS); + return; + } } catch (SecurityException e) { - Log.w(TAG, "No permission to get the current location."); - return; - } - if (address == null) { - if (DEBUG) Log.d(TAG, "Null address returned."); - fetchEpg(LOCATION_INIT_WAIT_MS); + if (TextUtils.isEmpty(PostalCodeUtils.getLastPostalCode(mContext))) { + Log.w(TAG, "No permission to get the current location."); + return; + } + } catch (PostalCodeUtils.NoPostalCodeException e) { + Log.i(TAG, "Failed to get the current postal code."); + postFetchRequest(fastFetch, LOCATION_INIT_WAIT_MS); return; } - if (DEBUG) Log.d(TAG, "Current location is " + address); + String postalCode = PostalCodeUtils.getLastPostalCode(mContext); + if (DEBUG) Log.d(TAG, "The current postal code is " + postalCode); - lineupId = getLineupForAddress(address); + lineupId = pickLineupForPostalCode(postalCode); if (lineupId != null) { - if (DEBUG) Log.d(TAG, "Saving lineup " + lineupId + "found for " + address); + Log.i(TAG, "Selecting the lineup " + lineupId); setLastLineupId(lineupId); } else { - if (DEBUG) Log.d(TAG, "No lineup found for " + address); + Log.i(TAG, "Failed to get lineup id"); + retryFetchEpg(fastFetch); return; } } @@ -299,48 +371,109 @@ public class EpgFetcher { return; } - boolean updated = false; List channels = mEpgReader.getChannels(lineupId); - for (Channel channel : channels) { - List programs = new ArrayList<>(mEpgReader.getPrograms(channel.getId())); - Collections.sort(programs); - if (DEBUG) { - Log.d(TAG, "Fetched " + programs.size() + " programs for channel " + channel); - } - if (updateEpg(channel.getId(), programs)) { - updated = true; + if (channels.isEmpty()) { + Log.i(TAG, "Failed to get EPG channels."); + retryFetchEpg(fastFetch); + return; + } + mFetchRetryCount = 0; + if (!fastFetch) { + for (Channel channel : channels) { + if (!mStarted) { + break; + } + List programs = new ArrayList<>(mEpgReader.getPrograms(channel.getId())); + Collections.sort(programs); + Log.i(TAG, "Fetched " + programs.size() + " programs for channel " + channel); + updateEpg(channel.getId(), programs); } + setLastUpdatedEpgTimestamp(epgTimestamp); + } else { + handleFastFetch(channels, PROGRAM_FETCH_SHORT_DURATION_SEC); + if (DEBUG) Log.d(TAG, "First fast fetch Done."); + handleFastFetch(channels, PROGRAM_FETCH_LONG_DURATION_SEC); + if (DEBUG) Log.d(TAG, "Second fast fetch Done."); } - final boolean epgUpdated = updated; - setLastUpdatedEpgTimestamp(epgTimestamp); - mHandler.removeMessages(MSG_FETCH_EPG); + if (!fastFetch) { + mHandler.removeMessages(MSG_FETCH_EPG); + } if (DEBUG) Log.d(TAG, "Fetching EPG is finished."); + // Start to fetch channel logos after epg fetching finished. + ChannelLogoFetcher.startFetchingChannelLogos(mContext, channels); } - @Nullable - private String getLineupForAddress(Address address) { - String lineup = null; - if (TextUtils.equals(address.getCountryCode(), SUPPORTED_COUNTRY_CODE)) { - String postalCode = address.getPostalCode(); - if (!TextUtils.isEmpty(postalCode)) { - lineup = getLineupForPostalCode(postalCode); + private void retryFetchEpg(boolean fastFetch) { + if (mFetchRetryCount < NO_INFO_RETRY_LIMIT) { + postFetchRequest(fastFetch, NO_INFO_FETCHED_WAIT_MS * 1 << mFetchRetryCount); + mFetchRetryCount++; + } else { + mFetchRetryCount = 0; + } + } + + private void handleFastFetch(List channels, long duration) { + List channelIds = new ArrayList<>(channels.size()); + for (Channel channel : channels) { + channelIds.add(channel.getId()); + } + Map> allPrograms = new HashMap<>(); + List queryChannelIds = new ArrayList<>(QUERY_CHANNEL_COUNT); + for (Long channelId : channelIds) { + queryChannelIds.add(channelId); + if (queryChannelIds.size() >= QUERY_CHANNEL_COUNT) { + allPrograms.putAll( + new HashMap<>(mEpgReader.getPrograms(queryChannelIds, duration))); + queryChannelIds.clear(); } } - return lineup; + if (!queryChannelIds.isEmpty()) { + allPrograms.putAll( + new HashMap<>(mEpgReader.getPrograms(queryChannelIds, duration))); + } + for (Channel channel : channels) { + List programs = allPrograms.get(channel.getId()); + if (programs == null) continue; + Collections.sort(programs); + Log.i(TAG, "Fast fetched " + programs.size() + " programs for channel " + channel); + updateEpg(channel.getId(), programs); + } } @Nullable - private String getLineupForPostalCode(String postalCode) { + private String pickLineupForPostalCode(String postalCode) { List lineups = mEpgReader.getLineups(postalCode); + int maxCount = 0; + String maxLineupId = null; for (Lineup lineup : lineups) { - // TODO(EPG): handle more than OTA digital - if (lineup.type == Lineup.LINEUP_BROADCAST_DIGITAL) { - if (DEBUG) Log.d(TAG, "Setting lineup to " + lineup.name + "(" + lineup.id + ")"); - return lineup.id; + int count = getMatchedChannelCount(lineup.id); + Log.i(TAG, lineup.name + " (" + lineup.id + ") - " + count + " matches"); + if (count > maxCount) { + maxCount = count; + maxLineupId = lineup.id; } } - return null; + return maxLineupId; + } + + private int getMatchedChannelCount(String lineupId) { + // Construct a list of display numbers for existing channels. + List channels = mChannelDataManager.getChannelList(); + if (channels.isEmpty()) { + if (DEBUG) Log.d(TAG, "No existing channel to compare"); + return 0; + } + List numbers = new ArrayList<>(channels.size()); + for (Channel c : channels) { + // We only support local channels from physical tuners. + if (c.isPhysicalTunerChannel()) { + numbers.add(c.getDisplayNumber()); + } + } + + numbers.retainAll(mEpgReader.getChannelNumbers(lineupId)); + return numbers.size(); } private long getLastUpdatedEpgTimestamp() { @@ -357,16 +490,16 @@ public class EpgFetcher { KEY_LAST_UPDATED_EPG_TIMESTAMP, timestamp).commit(); } - private String getLastLineupId() { + synchronized private String getLastLineupId() { if (mLineupId == null) { mLineupId = PreferenceManager.getDefaultSharedPreferences(mContext) .getString(KEY_LAST_LINEUP_ID, null); } - if (DEBUG) Log.d(TAG, "Last lineup_id " + mLineupId); + if (DEBUG) Log.d(TAG, "Last lineup is " + mLineupId); return mLineupId; } - private void setLastLineupId(String lineupId) { + synchronized private void setLastLineupId(String lineupId) { mLineupId = lineupId; PreferenceManager.getDefaultSharedPreferences(mContext).edit() .putString(KEY_LAST_LINEUP_ID, lineupId).commit(); @@ -381,19 +514,9 @@ public class EpgFetcher { long startTimeMs = System.currentTimeMillis(); long endTimeMs = startTimeMs + PROGRAM_QUERY_DURATION; List oldPrograms = queryPrograms(channelId, startTimeMs, endTimeMs); - Program currentOldProgram = oldPrograms.size() > 0 ? oldPrograms.get(0) : null; int oldProgramsIndex = 0; int newProgramsIndex = 0; - // Skip the past programs. They will be automatically removed by the system. - if (currentOldProgram != null) { - long oldStartTimeUtcMillis = currentOldProgram.getStartTimeUtcMillis(); - for (Program program : newPrograms) { - if (program.getEndTimeUtcMillis() > oldStartTimeUtcMillis) { - break; - } - newProgramsIndex++; - } - } + // Compare the new programs with old programs one by one and update/delete the old one // or insert new program if there is no matching program in the database. ArrayList ops = new ArrayList<>(); @@ -439,7 +562,7 @@ public class EpgFetcher { } if (addNewProgram) { ops.add(ContentProviderOperation - .newInsert(TvContract.Programs.CONTENT_URI) + .newInsert(Programs.CONTENT_URI) .withValues(toContentValues(newProgram)) .build()); } @@ -501,27 +624,25 @@ public class EpgFetcher { @SuppressWarnings("deprecation") private static ContentValues toContentValues(Program program) { ContentValues values = new ContentValues(); - values.put(TvContract.Programs.COLUMN_CHANNEL_ID, program.getChannelId()); - putValue(values, TvContract.Programs.COLUMN_TITLE, program.getTitle()); - putValue(values, TvContract.Programs.COLUMN_EPISODE_TITLE, program.getEpisodeTitle()); + values.put(Programs.COLUMN_CHANNEL_ID, program.getChannelId()); + putValue(values, Programs.COLUMN_TITLE, program.getTitle()); + putValue(values, Programs.COLUMN_EPISODE_TITLE, program.getEpisodeTitle()); if (BuildCompat.isAtLeastN()) { - putValue(values, TvContract.Programs.COLUMN_SEASON_DISPLAY_NUMBER, - program.getSeasonNumber()); - putValue(values, TvContract.Programs.COLUMN_EPISODE_DISPLAY_NUMBER, - program.getEpisodeNumber()); + putValue(values, Programs.COLUMN_SEASON_DISPLAY_NUMBER, program.getSeasonNumber()); + putValue(values, Programs.COLUMN_EPISODE_DISPLAY_NUMBER, program.getEpisodeNumber()); } else { - putValue(values, TvContract.Programs.COLUMN_SEASON_NUMBER, program.getSeasonNumber()); - putValue(values, TvContract.Programs.COLUMN_EPISODE_NUMBER, program.getEpisodeNumber()); + putValue(values, Programs.COLUMN_SEASON_NUMBER, program.getSeasonNumber()); + putValue(values, Programs.COLUMN_EPISODE_NUMBER, program.getEpisodeNumber()); } - putValue(values, TvContract.Programs.COLUMN_SHORT_DESCRIPTION, program.getDescription()); - putValue(values, TvContract.Programs.COLUMN_POSTER_ART_URI, program.getPosterArtUri()); - putValue(values, TvContract.Programs.COLUMN_THUMBNAIL_URI, program.getThumbnailUri()); + putValue(values, Programs.COLUMN_SHORT_DESCRIPTION, program.getDescription()); + putValue(values, Programs.COLUMN_LONG_DESCRIPTION, program.getLongDescription()); + putValue(values, Programs.COLUMN_POSTER_ART_URI, program.getPosterArtUri()); + putValue(values, Programs.COLUMN_THUMBNAIL_URI, program.getThumbnailUri()); String[] canonicalGenres = program.getCanonicalGenres(); if (canonicalGenres != null && canonicalGenres.length > 0) { - putValue(values, TvContract.Programs.COLUMN_CANONICAL_GENRE, - Genres.encode(canonicalGenres)); + putValue(values, Programs.COLUMN_CANONICAL_GENRE, Genres.encode(canonicalGenres)); } else { - putValue(values, TvContract.Programs.COLUMN_CANONICAL_GENRE, ""); + putValue(values, Programs.COLUMN_CANONICAL_GENRE, ""); } TvContentRating[] ratings = program.getContentRatings(); if (ratings != null && ratings.length > 0) { @@ -530,14 +651,13 @@ public class EpgFetcher { sb.append(CONTENT_RATING_SEPARATOR); sb.append(ratings[i].flattenToString()); } - putValue(values, TvContract.Programs.COLUMN_CONTENT_RATING, sb.toString()); + putValue(values, Programs.COLUMN_CONTENT_RATING, sb.toString()); } else { - putValue(values, TvContract.Programs.COLUMN_CONTENT_RATING, ""); + putValue(values, Programs.COLUMN_CONTENT_RATING, ""); } - values.put(TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS, - program.getStartTimeUtcMillis()); - values.put(TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS, program.getEndTimeUtcMillis()); - putValue(values, TvContract.Programs.COLUMN_INTERNAL_PROVIDER_DATA, + values.put(Programs.COLUMN_START_TIME_UTC_MILLIS, program.getStartTimeUtcMillis()); + values.put(Programs.COLUMN_END_TIME_UTC_MILLIS, program.getEndTimeUtcMillis()); + putValue(values, Programs.COLUMN_INTERNAL_PROVIDER_DATA, InternalDataUtils.serializeInternalProviderData(program)); return values; } @@ -569,6 +689,9 @@ public class EpgFetcher { case MSG_FETCH_EPG: epgFetcher.onFetchEpg(); break; + case MSG_FAST_FETCH_EPG: + epgFetcher.onFetchEpg(true); + break; default: super.handleMessage(msg); break; @@ -579,7 +702,7 @@ public class EpgFetcher { private class EpgRunner implements Runnable { @Override public void run() { - fetchEpg(); + postFetchRequest(false, 0); } } } diff --git a/src/com/android/tv/data/epg/EpgReader.java b/src/com/android/tv/data/epg/EpgReader.java index 4f3b6f52..95cd933e 100644 --- a/src/com/android/tv/data/epg/EpgReader.java +++ b/src/com/android/tv/data/epg/EpgReader.java @@ -22,9 +22,10 @@ import android.support.annotation.WorkerThread; import com.android.tv.data.Channel; import com.android.tv.data.Lineup; import com.android.tv.data.Program; -import com.android.tv.dvr.SeriesInfo; +import com.android.tv.dvr.data.SeriesInfo; import java.util.List; +import java.util.Map; /** * An interface used to retrieve the EPG data. This class should be used in worker thread. @@ -43,21 +44,37 @@ public interface EpgReader { long getEpgTimestamp(); /** - * Returns the channels list. + * Returns the lineups list. + */ + List getLineups(@NonNull String postalCode); + + /** + * Returns the list of channel numbers (unsorted) for the given lineup. The result is used to + * choose the most appropriate lineup among others by comparing the channel numbers of the + * existing channels on the device. + */ + List getChannelNumbers(@NonNull String lineupId); + + /** + * Returns the list of channels for the given lineup. The returned channels should map into the + * existing channels on the device. This method is usually called after selecting the lineup. */ List getChannels(@NonNull String lineupId); /** - * Returns the lineups list. + * Returns the programs for the given channel. Must call {@link #getChannels(String)} + * beforehand. Note that the {@code Program} doesn't have valid program ID because it's not + * retrieved from TvProvider. */ - List getLineups(@NonNull String postalCode); + List getPrograms(long channelId); /** - * Returns the programs for the given channel. The result is sorted by the start time. + * Returns the programs for the given channels. * Note that the {@code Program} doesn't have valid program ID because it's not retrieved from * TvProvider. + * This method is only used to get programs for a short duration typically. */ - List getPrograms(long channelId); + Map> getPrograms(List channelIds, long duration); /** * Returns the series information for the given series ID. diff --git a/src/com/android/tv/data/epg/StubEpgReader.java b/src/com/android/tv/data/epg/StubEpgReader.java index 64093f89..220daf22 100644 --- a/src/com/android/tv/data/epg/StubEpgReader.java +++ b/src/com/android/tv/data/epg/StubEpgReader.java @@ -21,10 +21,11 @@ import android.content.Context; import com.android.tv.data.Channel; import com.android.tv.data.Lineup; import com.android.tv.data.Program; -import com.android.tv.dvr.SeriesInfo; +import com.android.tv.dvr.data.SeriesInfo; import java.util.Collections; import java.util.List; +import java.util.Map; /** * A stub class to read EPG. @@ -44,12 +45,17 @@ public class StubEpgReader implements EpgReader{ } @Override - public List getChannels(String lineupId) { + public List getLineups(String postalCode) { return Collections.emptyList(); } @Override - public List getLineups(String postalCode) { + public List getChannelNumbers(String lineupId) { + return Collections.emptyList(); + } + + @Override + public List getChannels(String lineupId) { return Collections.emptyList(); } @@ -58,6 +64,11 @@ public class StubEpgReader implements EpgReader{ return Collections.emptyList(); } + @Override + public Map> getPrograms(List channelIds, long duration) { + return Collections.emptyMap(); + } + @Override public SeriesInfo getSeriesInfo(String seriesId) { return null; diff --git a/src/com/android/tv/dialog/DvrHistoryDialogFragment.java b/src/com/android/tv/dialog/DvrHistoryDialogFragment.java new file mode 100644 index 00000000..2ed98b87 --- /dev/null +++ b/src/com/android/tv/dialog/DvrHistoryDialogFragment.java @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.dialog; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.app.AlertDialog; +import android.app.Dialog; +import android.os.Build.VERSION_CODES; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ListView; +import android.widget.TextView; + +import com.android.tv.ApplicationSingletons; +import com.android.tv.R; +import com.android.tv.TvApplication; +import com.android.tv.data.Channel; +import com.android.tv.data.ChannelDataManager; +import com.android.tv.dvr.DvrDataManager; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.data.ScheduledRecording.RecordingState; +import com.android.tv.util.Utils; + +import java.util.ArrayList; +import java.util.List; + +/** + * Displays the DVR history. + */ +@TargetApi(VERSION_CODES.N) +public class DvrHistoryDialogFragment extends SafeDismissDialogFragment { + public static final String DIALOG_TAG = DvrHistoryDialogFragment.class.getSimpleName(); + + private static final String TRACKER_LABEL = "DVR history"; + private final List mSchedules = new ArrayList<>(); + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + ApplicationSingletons singletons = TvApplication.getSingletons(getContext()); + DvrDataManager dataManager = singletons.getDvrDataManager(); + ChannelDataManager channelDataManager = singletons.getChannelDataManager(); + for (ScheduledRecording schedule : dataManager.getAllScheduledRecordings()) { + if (!schedule.isInProgress() && !schedule.isNotStarted()) { + mSchedules.add(schedule); + } + } + mSchedules.sort(ScheduledRecording.START_TIME_COMPARATOR.reversed()); + LayoutInflater inflater = LayoutInflater.from(getContext()); + ArrayAdapter adapter = new ArrayAdapter(getContext(), + R.layout.list_item_dvr_history, ScheduledRecording.toArray(mSchedules)) { + @NonNull + @Override + public View getView(int position, View convertView, ViewGroup parent) { + View view = inflater.inflate(R.layout.list_item_dvr_history, parent, false); + ScheduledRecording schedule = mSchedules.get(position); + setText(view, R.id.state, getStateString(schedule.getState())); + setText(view, R.id.schedule_time, getRecordingTimeText(schedule)); + setText(view, R.id.program_title, + schedule.getProgramTitleWithEpisodeNumber(getContext())); + setText(view, R.id.channel_name, getChannelNameText(schedule)); + return view; + } + + private void setText(View view, int id, String text) { + ((TextView) view.findViewById(id)).setText(text); + } + + private void setText(View view, int id, int text) { + ((TextView) view.findViewById(id)).setText(text); + } + + @SuppressLint("SwitchIntDef") + private int getStateString(@RecordingState int state) { + switch (state) { + case ScheduledRecording.STATE_RECORDING_CLIPPED: + return R.string.dvr_history_dialog_state_clip; + case ScheduledRecording.STATE_RECORDING_FAILED: + return R.string.dvr_history_dialog_state_fail; + case ScheduledRecording.STATE_RECORDING_FINISHED: + return R.string.dvr_history_dialog_state_success; + default: + break; + } + return 0; + } + + private String getChannelNameText(ScheduledRecording schedule) { + Channel channel = channelDataManager.getChannel(schedule.getChannelId()); + return channel == null ? null : + TextUtils.isEmpty(channel.getDisplayName()) ? channel.getDisplayNumber() : + channel.getDisplayName().trim() + " " + channel.getDisplayNumber(); + } + + private String getRecordingTimeText(ScheduledRecording schedule) { + return Utils.getDurationString(getContext(), schedule.getStartTimeMs(), + schedule.getEndTimeMs(), true, true, true, 0); + } + }; + ListView listView = new ListView(getActivity()); + listView.setAdapter(adapter); + return new AlertDialog.Builder(getActivity()).setTitle(R.string.dvr_history_dialog_title) + .setView(listView).create(); + } + + @Override + public String getTrackerLabel() { + return TRACKER_LABEL; + } +} diff --git a/src/com/android/tv/dialog/FullscreenDialogFragment.java b/src/com/android/tv/dialog/FullscreenDialogFragment.java index d16202a1..d00422a7 100644 --- a/src/com/android/tv/dialog/FullscreenDialogFragment.java +++ b/src/com/android/tv/dialog/FullscreenDialogFragment.java @@ -77,7 +77,7 @@ public class FullscreenDialogFragment extends SafeDismissDialogFragment { return mTrackerLabel; } - private class FullscreenDialog extends TvDialog { + private class FullscreenDialog extends Dialog { public FullscreenDialog(Context context, int theme) { super(context, theme); } diff --git a/src/com/android/tv/dialog/HalfSizedDialogFragment.java b/src/com/android/tv/dialog/HalfSizedDialogFragment.java new file mode 100644 index 00000000..315c6a93 --- /dev/null +++ b/src/com/android/tv/dialog/HalfSizedDialogFragment.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.dialog; + +import android.app.Dialog; +import android.content.DialogInterface; +import android.os.Bundle; +import android.os.Handler; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.android.tv.R; + +import java.util.concurrent.TimeUnit; + +public class HalfSizedDialogFragment extends SafeDismissDialogFragment { + public static final String DIALOG_TAG = HalfSizedDialogFragment.class.getSimpleName(); + public static final String TRACKER_LABEL = "Half sized dialog"; + + private static final long AUTO_DISMISS_TIME_THRESHOLD_MS = TimeUnit.SECONDS.toMillis(30); + + private OnActionClickListener mOnActionClickListener; + + private Handler mHandler = new Handler(); + private Runnable mAutoDismisser = new Runnable() { + @Override + public void run() { + dismiss(); + } + }; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + return inflater.inflate(R.layout.halfsized_dialog, container, false); + } + + @Override + public void onStart() { + super.onStart(); + mHandler.postDelayed(mAutoDismisser, AUTO_DISMISS_TIME_THRESHOLD_MS); + } + + @Override + public void onPause() { + super.onPause(); + if (mOnActionClickListener != null) { + // Dismisses the dialog to prevent the callback being forgotten during + // fragment re-creating. + dismiss(); + } + } + + @Override + public void onStop() { + super.onStop(); + mHandler.removeCallbacks(mAutoDismisser); + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + Dialog dialog = super.onCreateDialog(savedInstanceState); + dialog.setOnKeyListener(new DialogInterface.OnKeyListener() { + public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent keyEvent) { + mHandler.removeCallbacks(mAutoDismisser); + mHandler.postDelayed(mAutoDismisser, AUTO_DISMISS_TIME_THRESHOLD_MS); + return false; + } + }); + return dialog; + } + + @Override + public int getTheme() { + return R.style.Theme_TV_dialog_HalfSizedDialog; + } + + @Override + public String getTrackerLabel() { + return TRACKER_LABEL; + } + + /** + * Sets {@link OnActionClickListener} for the dialog fragment. If listener is set, the dialog + * will be automatically closed when it's paused to prevent the fragment being re-created by + * the framework, which will result the listener being forgotten. + */ + public void setOnActionClickListener(OnActionClickListener listener) { + mOnActionClickListener = listener; + } + + /** + * Returns {@link OnActionClickListener} for sub-classes or any inner fragments. + */ + protected OnActionClickListener getOnActionClickListener() { + return mOnActionClickListener; + } + + /** + * An interface to provide callbacks for half-sized dialogs. Subclasses or inner fragments + * should invoke {@link OnActionClickListener#onActionClick(long)} and provide the identifier + * of the action user clicked. + */ + public interface OnActionClickListener { + void onActionClick(long actionId); + } +} \ No newline at end of file diff --git a/src/com/android/tv/dialog/SafeDismissDialogFragment.java b/src/com/android/tv/dialog/SafeDismissDialogFragment.java index f671a87d..e3390b0a 100644 --- a/src/com/android/tv/dialog/SafeDismissDialogFragment.java +++ b/src/com/android/tv/dialog/SafeDismissDialogFragment.java @@ -17,11 +17,7 @@ package com.android.tv.dialog; import android.app.Activity; -import android.app.Dialog; import android.app.DialogFragment; -import android.content.Context; -import android.os.Bundle; -import android.view.KeyEvent; import com.android.tv.MainActivity; import com.android.tv.TvApplication; @@ -38,11 +34,6 @@ public abstract class SafeDismissDialogFragment extends DialogFragment private boolean mDismissPending = false; private Tracker mTracker; - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - return new TvDialog(getActivity(), getTheme()); - } - @Override public void onAttach(Activity activity) { super.onAttach(activity); @@ -92,21 +83,4 @@ public abstract class SafeDismissDialogFragment extends DialogFragment super.dismiss(); } } - - protected class TvDialog extends Dialog { - public TvDialog(Context context, int theme) { - super(context, theme); - } - - @Override - public boolean onKeyUp(int keyCode, KeyEvent event) { - // When a dialog is showing, key events are handled by the dialog instead of - // MainActivity. Therefore, unless a key is a global key, it should be handled here. - if (mAttached && keyCode == KeyEvent.KEYCODE_SEARCH && mActivity != null) { - mActivity.showSearchActivity(); - return true; - } - return super.onKeyUp(keyCode, event); - } - } } diff --git a/src/com/android/tv/dialog/WebDialogFragment.java b/src/com/android/tv/dialog/WebDialogFragment.java index 75f93bb2..171a256b 100644 --- a/src/com/android/tv/dialog/WebDialogFragment.java +++ b/src/com/android/tv/dialog/WebDialogFragment.java @@ -37,6 +37,7 @@ public class WebDialogFragment extends SafeDismissDialogFragment { private static final String TITLE = "TITLE"; private static final String TRACKER_LABEL = "TRACKER_LABEL"; + private WebView mWebView; private String mTrackerLabel; /** @@ -73,13 +74,21 @@ public class WebDialogFragment extends SafeDismissDialogFragment { String title = getArguments().getString(TITLE); getDialog().setTitle(title); - WebView webView = new WebView(getActivity()); - webView.setWebViewClient(new WebViewClient()); + mWebView = new WebView(getActivity()); + mWebView.setWebViewClient(new WebViewClient()); String url = getArguments().getString(URL); - webView.loadUrl(url); + mWebView.loadUrl(url); Log.d(TAG, "Loading web content from " + url); - return webView; + return mWebView; + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + if (mWebView != null) { + mWebView.destroy(); + } } @Override diff --git a/src/com/android/tv/dvr/BaseDvrDataManager.java b/src/com/android/tv/dvr/BaseDvrDataManager.java index 89661df3..a8637449 100644 --- a/src/com/android/tv/dvr/BaseDvrDataManager.java +++ b/src/com/android/tv/dvr/BaseDvrDataManager.java @@ -26,7 +26,10 @@ import android.util.Log; import com.android.tv.common.SoftPreconditions; import com.android.tv.common.feature.CommonFeatures; -import com.android.tv.dvr.ScheduledRecording.RecordingState; +import com.android.tv.dvr.data.RecordedProgram; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.data.ScheduledRecording.RecordingState; +import com.android.tv.dvr.data.SeriesRecording; import com.android.tv.util.Clock; import java.util.ArrayList; @@ -317,6 +320,42 @@ public abstract class BaseDvrDataManager implements WritableDvrDataManager { return result; } + @Override + public void checkAndRemoveEmptySeriesRecording(long... seriesRecordingIds) { + List toRemove = new ArrayList<>(); + for (long rId : seriesRecordingIds) { + SeriesRecording seriesRecording = getSeriesRecording(rId); + if (seriesRecording != null && isEmptySeriesRecording(seriesRecording)) { + toRemove.add(seriesRecording); + } + } + removeSeriesRecording(SeriesRecording.toArray(toRemove)); + } + + /** + * Returns {@code true}, if the series recording is empty and can be removed. If a series + * recording is in NORMAL state or has recordings or schedules, it is not empty and cannot be + * removed. + */ + protected final boolean isEmptySeriesRecording(@NonNull SeriesRecording seriesRecording) { + if (!seriesRecording.isStopped()) { + return false; + } + long seriesRecordingId = seriesRecording.getId(); + for (ScheduledRecording r : getAvailableScheduledRecordings()) { + if (r.getSeriesRecordingId() == seriesRecordingId) { + return false; + } + } + String seriesId = seriesRecording.getSeriesId(); + for (RecordedProgram r : getRecordedPrograms()) { + if (seriesId.equals(r.getSeriesId())) { + return false; + } + } + return true; + } + @Override public void forgetStorage(String inputId) { } } diff --git a/src/com/android/tv/dvr/ConflictChecker.java b/src/com/android/tv/dvr/ConflictChecker.java deleted file mode 100644 index 201e379e..00000000 --- a/src/com/android/tv/dvr/ConflictChecker.java +++ /dev/null @@ -1,277 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.dvr; - -import android.annotation.TargetApi; -import android.content.ContentUris; -import android.media.tv.TvContract; -import android.net.Uri; -import android.os.Build; -import android.os.Message; -import android.support.annotation.MainThread; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.util.ArraySet; -import android.util.Log; - -import com.android.tv.ApplicationSingletons; -import com.android.tv.InputSessionManager; -import com.android.tv.InputSessionManager.OnTvViewChannelChangeListener; -import com.android.tv.MainActivity; -import com.android.tv.TvApplication; -import com.android.tv.common.WeakHandler; -import com.android.tv.data.Channel; -import com.android.tv.data.ChannelDataManager; -import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.TimeUnit; - -/** - * Checking the runtime conflict of DVR recording. - *

- * This class runs only while the {@link MainActivity} is resumed and holds the upcoming conflicts. - */ -@TargetApi(Build.VERSION_CODES.N) -@MainThread -public class ConflictChecker { - private static final String TAG = "ConflictChecker"; - private static final boolean DEBUG = false; - - private static final int MSG_CHECK_CONFLICT = 1; - - private static final long CHECK_RETRY_PERIOD_MS = TimeUnit.SECONDS.toMillis(30); - - /** - * To show watch conflict dialog, the start time of the earliest conflicting schedule should be - * less than or equal to this time. - */ - private static final long MAX_WATCH_CONFLICT_CHECK_TIME_MS = TimeUnit.MINUTES.toMillis(5); - /** - * To show watch conflict dialog, the start time of the earliest conflicting schedule should be - * greater than or equal to this time. - */ - private static final long MIN_WATCH_CONFLICT_CHECK_TIME_MS = TimeUnit.SECONDS.toMillis(30); - - private final MainActivity mMainActivity; - private final ChannelDataManager mChannelDataManager; - private final DvrScheduleManager mScheduleManager; - private final InputSessionManager mSessionManager; - private final ConflictCheckerHandler mHandler = new ConflictCheckerHandler(this); - - private final List mUpcomingConflicts = new ArrayList<>(); - private final Set mOnUpcomingConflictChangeListeners = - new ArraySet<>(); - private final Map> mCheckedConflictsMap = new HashMap<>(); - - private final ScheduledRecordingListener mScheduledRecordingListener = - new ScheduledRecordingListener() { - @Override - public void onScheduledRecordingAdded(ScheduledRecording... scheduledRecordings) { - if (DEBUG) Log.d(TAG, "onScheduledRecordingAdded: " + scheduledRecordings); - mHandler.sendEmptyMessage(MSG_CHECK_CONFLICT); - } - - @Override - public void onScheduledRecordingRemoved(ScheduledRecording... scheduledRecordings) { - if (DEBUG) Log.d(TAG, "onScheduledRecordingRemoved: " + scheduledRecordings); - mHandler.sendEmptyMessage(MSG_CHECK_CONFLICT); - } - - @Override - public void onScheduledRecordingStatusChanged(ScheduledRecording... scheduledRecordings) { - if (DEBUG) Log.d(TAG, "onScheduledRecordingStatusChanged: " + scheduledRecordings); - mHandler.sendEmptyMessage(MSG_CHECK_CONFLICT); - } - }; - - private final OnTvViewChannelChangeListener mOnTvViewChannelChangeListener = - new OnTvViewChannelChangeListener() { - @Override - public void onTvViewChannelChange(@Nullable Uri channelUri) { - mHandler.sendEmptyMessage(MSG_CHECK_CONFLICT); - } - }; - - private boolean mStarted; - - public ConflictChecker(MainActivity mainActivity) { - mMainActivity = mainActivity; - ApplicationSingletons appSingletons = TvApplication.getSingletons(mainActivity); - mChannelDataManager = appSingletons.getChannelDataManager(); - mScheduleManager = appSingletons.getDvrScheduleManager(); - mSessionManager = appSingletons.getInputSessionManager(); - } - - /** - * Starts checking the conflict. - */ - public void start() { - if (mStarted) { - return; - } - mStarted = true; - mHandler.sendEmptyMessage(MSG_CHECK_CONFLICT); - mScheduleManager.addScheduledRecordingListener(mScheduledRecordingListener); - mSessionManager.addOnTvViewChannelChangeListener(mOnTvViewChannelChangeListener); - } - - /** - * Stops checking the conflict. - */ - public void stop() { - if (!mStarted) { - return; - } - mStarted = false; - mSessionManager.removeOnTvViewChannelChangeListener(mOnTvViewChannelChangeListener); - mScheduleManager.removeScheduledRecordingListener(mScheduledRecordingListener); - mHandler.removeCallbacksAndMessages(null); - } - - /** - * Returns the upcoming conflicts. - */ - public List getUpcomingConflicts() { - return new ArrayList<>(mUpcomingConflicts); - } - - /** - * Adds a {@link OnUpcomingConflictChangeListener}. - */ - public void addOnUpcomingConflictChangeListener(OnUpcomingConflictChangeListener listener) { - mOnUpcomingConflictChangeListeners.add(listener); - } - - /** - * Removes the {@link OnUpcomingConflictChangeListener}. - */ - public void removeOnUpcomingConflictChangeListener(OnUpcomingConflictChangeListener listener) { - mOnUpcomingConflictChangeListeners.remove(listener); - } - - private void notifyUpcomingConflictChanged() { - for (OnUpcomingConflictChangeListener l : mOnUpcomingConflictChangeListeners) { - l.onUpcomingConflictChange(); - } - } - - /** - * Remembers the user's decision to record while watching the channel. - */ - public void setCheckedConflictsForChannel(long mChannelId, List conflicts) { - mCheckedConflictsMap.put(mChannelId, new ArrayList<>(conflicts)); - } - - void onCheckConflict() { - // Checks the conflicting schedules and setup the next re-check time. - // If there are upcoming conflicts soon, it opens the conflict dialog. - if (DEBUG) Log.d(TAG, "Handling MSG_CHECK_CONFLICT"); - mHandler.removeMessages(MSG_CHECK_CONFLICT); - mUpcomingConflicts.clear(); - if (!mScheduleManager.isInitialized() - || !mChannelDataManager.isDbLoadFinished()) { - mHandler.sendEmptyMessageDelayed(MSG_CHECK_CONFLICT, CHECK_RETRY_PERIOD_MS); - notifyUpcomingConflictChanged(); - return; - } - if (mSessionManager.getCurrentTvViewChannelUri() == null) { - // As MainActivity is not using a tuner, no need to check the conflict. - notifyUpcomingConflictChanged(); - return; - } - Uri channelUri = mSessionManager.getCurrentTvViewChannelUri(); - if (TvContract.isChannelUriForPassthroughInput(channelUri)) { - notifyUpcomingConflictChanged(); - return; - } - long channelId = ContentUris.parseId(channelUri); - Channel channel = mChannelDataManager.getChannel(channelId); - // The conflicts caused by watching the channel. - List conflicts = mScheduleManager - .getConflictingSchedulesForWatching(channel.getId()); - long earliestToCheck = Long.MAX_VALUE; - long currentTimeMs = System.currentTimeMillis(); - for (ScheduledRecording schedule : conflicts) { - long startTimeMs = schedule.getStartTimeMs(); - if (startTimeMs < currentTimeMs + MIN_WATCH_CONFLICT_CHECK_TIME_MS) { - // The start time of the upcoming conflict remains less than the minimum - // check time. - continue; - } - if (startTimeMs > currentTimeMs + MAX_WATCH_CONFLICT_CHECK_TIME_MS) { - // The start time of the upcoming conflict remains greater than the - // maximum check time. Setup the next re-check time. - long nextCheckTimeMs = startTimeMs - MAX_WATCH_CONFLICT_CHECK_TIME_MS; - if (earliestToCheck > nextCheckTimeMs) { - earliestToCheck = nextCheckTimeMs; - } - } else { - // Found upcoming conflicts which will start soon. - mUpcomingConflicts.add(schedule); - // The schedule will be removed from the "upcoming conflict" when the - // recording is almost started. - long nextCheckTimeMs = startTimeMs - MIN_WATCH_CONFLICT_CHECK_TIME_MS; - if (earliestToCheck > nextCheckTimeMs) { - earliestToCheck = nextCheckTimeMs; - } - } - } - if (earliestToCheck != Long.MAX_VALUE) { - mHandler.sendEmptyMessageDelayed(MSG_CHECK_CONFLICT, - earliestToCheck - currentTimeMs); - } - if (DEBUG) Log.d(TAG, "upcoming conflicts: " + mUpcomingConflicts); - notifyUpcomingConflictChanged(); - if (!mUpcomingConflicts.isEmpty() - && !DvrUiHelper.isChannelWatchConflictDialogShown(mMainActivity)) { - // Don't show the conflict dialog if the user already knows. - List checkedConflicts = mCheckedConflictsMap.get( - channel.getId()); - if (checkedConflicts == null - || !checkedConflicts.containsAll(mUpcomingConflicts)) { - DvrUiHelper.showChannelWatchConflictDialog(mMainActivity, channel); - } - } - } - - private static class ConflictCheckerHandler extends WeakHandler { - ConflictCheckerHandler(ConflictChecker conflictChecker) { - super(conflictChecker); - } - - @Override - protected void handleMessage(Message msg, @NonNull ConflictChecker conflictChecker) { - switch (msg.what) { - case MSG_CHECK_CONFLICT: - conflictChecker.onCheckConflict(); - break; - } - } - } - - /** - * A listener for the change of upcoming conflicts. - */ - public interface OnUpcomingConflictChangeListener { - void onUpcomingConflictChange(); - } -} diff --git a/src/com/android/tv/dvr/DvrDataManager.java b/src/com/android/tv/dvr/DvrDataManager.java index 06613667..6d400b82 100644 --- a/src/com/android/tv/dvr/DvrDataManager.java +++ b/src/com/android/tv/dvr/DvrDataManager.java @@ -21,7 +21,10 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.Range; -import com.android.tv.dvr.ScheduledRecording.RecordingState; +import com.android.tv.dvr.data.RecordedProgram; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.data.ScheduledRecording.RecordingState; +import com.android.tv.dvr.data.SeriesRecording; import java.util.Collection; import java.util.List; @@ -210,6 +213,13 @@ public interface DvrDataManager { @NonNull Collection getDisallowedProgramIds(); + /** + * Checks each of the give series recordings to see if it's empty, i.e., it doesn't contains + * any available schedules or recorded programs, and it's status is + * {@link SeriesRecording#STATE_SERIES_STOPPED}; and removes those empty series recordings. + */ + void checkAndRemoveEmptySeriesRecording(long... seriesRecordingIds); + /** * Listens for the DVR schedules loading finished. */ diff --git a/src/com/android/tv/dvr/DvrDataManagerImpl.java b/src/com/android/tv/dvr/DvrDataManagerImpl.java index 46682a48..6d0a9959 100644 --- a/src/com/android/tv/dvr/DvrDataManagerImpl.java +++ b/src/com/android/tv/dvr/DvrDataManagerImpl.java @@ -42,7 +42,11 @@ import android.util.Range; import com.android.tv.TvApplication; import com.android.tv.common.SoftPreconditions; import com.android.tv.dvr.DvrStorageStatusManager.OnStorageMountChangedListener; -import com.android.tv.dvr.ScheduledRecording.RecordingState; +import com.android.tv.dvr.data.IdGenerator; +import com.android.tv.dvr.data.RecordedProgram; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.data.ScheduledRecording.RecordingState; +import com.android.tv.dvr.data.SeriesRecording; import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncAddScheduleTask; import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncAddSeriesRecordingTask; import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncDeleteScheduleTask; @@ -51,6 +55,8 @@ import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncDvrQueryScheduleTask; import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncDvrQuerySeriesRecordingTask; import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncUpdateScheduleTask; import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncUpdateSeriesRecordingTask; +import com.android.tv.dvr.provider.DvrDbSync; +import com.android.tv.dvr.recorder.SeriesRecordingScheduler; import com.android.tv.util.AsyncDbTask; import com.android.tv.util.AsyncDbTask.AsyncRecordedProgramQueryTask; import com.android.tv.util.Clock; @@ -267,11 +273,14 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { removeScheduledRecording(ScheduledRecording.toArray(toDelete)); } IdGenerator.SCHEDULED_RECORDING.setMaxId(maxId); + if (mRecordedProgramLoadFinished) { + validateSeriesRecordings(); + } mDvrLoadFinished = true; notifyDvrScheduleLoadFinished(); - mDbSync = new DvrDbSync(mContext, DvrDataManagerImpl.this); - mDbSync.start(); if (isInitialized()) { + mDbSync = new DvrDbSync(mContext, DvrDataManagerImpl.this); + mDbSync.start(); SeriesRecordingScheduler.getInstance(mContext).start(); } } @@ -306,6 +315,9 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { if (uri == null) { uri = RecordedPrograms.CONTENT_URI; } + if (recordedPrograms == null) { + recordedPrograms = Collections.emptyList(); + } int match = TvProviderUriMatcher.match(uri); if (match == TvProviderUriMatcher.MATCH_RECORDED_PROGRAM) { if (!mRecordedProgramLoadFinished) { @@ -318,7 +330,11 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { } mRecordedProgramLoadFinished = true; notifyRecordedProgramLoadFinished(); - } else if (recordedPrograms == null || recordedPrograms.isEmpty()) { + if (isInitialized()) { + mDbSync = new DvrDbSync(mContext, DvrDataManagerImpl.this); + mDbSync.start(); + } + } else if (recordedPrograms.isEmpty()) { List oldRecordedPrograms = new ArrayList<>(mRecordedPrograms.values()); mRecordedPrograms.clear(); @@ -355,6 +371,7 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { } } if (isInitialized()) { + validateSeriesRecordings(); SeriesRecordingScheduler.getInstance(mContext).start(); } } else if (match == TvProviderUriMatcher.MATCH_RECORDED_PROGRAM_ID) { @@ -363,11 +380,15 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { } long id = ContentUris.parseId(uri); if (DEBUG) Log.d(TAG, "changed recorded program #" + id + " to " + recordedPrograms); - if (recordedPrograms == null || recordedPrograms.isEmpty()) { + if (recordedPrograms.isEmpty()) { mRecordedProgramsForRemovedInput.remove(id); RecordedProgram old = mRecordedPrograms.remove(id); if (old != null) { notifyRecordedProgramsRemoved(old); + SeriesRecording r = mSeriesId2SeriesRecordings.get(old.getSeriesId()); + if (r != null && isEmptySeriesRecording(r)) { + removeSeriesRecording(r); + } } } else { RecordedProgram recordedProgram = recordedPrograms.get(0); @@ -592,10 +613,16 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { public void removeScheduledRecording(boolean forceRemove, ScheduledRecording... schedules) { List schedulesToDelete = new ArrayList<>(); List schedulesNotToDelete = new ArrayList<>(); + Set seriesRecordingIdsToCheck = new HashSet<>(); for (ScheduledRecording r : schedules) { mScheduledRecordings.remove(r.getId()); - getDeletedScheduleMap().remove(r.getId()); + getDeletedScheduleMap().remove(r.getProgramId()); mProgramId2ScheduledRecordings.remove(r.getProgramId()); + if (r.getSeriesRecordingId() != SeriesRecording.ID_NOT_SET + && (r.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED + || r.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS)) { + seriesRecordingIdsToCheck.add(r.getSeriesRecordingId()); + } boolean isScheduleForRemovedInput = mScheduledRecordingsForRemovedInput.remove(r.getProgramId()) != null; // If it belongs to the series recording and it's not started yet, just mark delete @@ -614,8 +641,19 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { } } if (mDvrLoadFinished) { + if (mRecordedProgramLoadFinished) { + checkAndRemoveEmptySeriesRecording(seriesRecordingIdsToCheck); + } notifyScheduledRecordingRemoved(schedules); } + Iterator iterator = schedulesNotToDelete.iterator(); + while (iterator.hasNext()) { + ScheduledRecording r = iterator.next(); + if (!mSeriesRecordings.containsKey(r.getSeriesRecordingId())) { + iterator.remove(); + schedulesToDelete.add(r); + } + } if (!schedulesToDelete.isEmpty()) { new AsyncDeleteScheduleTask(mContext).executeOnDbThread( ScheduledRecording.toArray(schedulesToDelete)); @@ -669,6 +707,7 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { private void updateScheduledRecording(boolean updateDb, final ScheduledRecording... schedules) { List toUpdate = new ArrayList<>(); + Set seriesRecordingIdsToCheck = new HashSet<>(); for (ScheduledRecording r : schedules) { if (!SoftPreconditions.checkState(mScheduledRecordings.containsKey(r.getId()), TAG, "Recording not found for: " + r)) { @@ -691,6 +730,13 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { if (programId != ScheduledRecording.ID_NOT_SET) { mProgramId2ScheduledRecordings.put(programId, r); } + if (r.getState() == ScheduledRecording.STATE_RECORDING_FAILED + && r.getSeriesRecordingId() != SeriesRecording.ID_NOT_SET) { + // If the scheduled recording is failed, it may cause the automatically generated + // series recording for this schedule becomes invalid (with no future schedules and + // past recordings.) We should check and remove these series recordings. + seriesRecordingIdsToCheck.add(r.getSeriesRecordingId()); + } } if (toUpdate.isEmpty()) { return; @@ -702,12 +748,17 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { if (updateDb) { new AsyncUpdateScheduleTask(mContext).executeOnDbThread(scheduleArray); } + checkAndRemoveEmptySeriesRecording(seriesRecordingIdsToCheck); removeDeletedSchedules(schedules); } @Override public void updateSeriesRecording(final SeriesRecording... seriesRecordings) { for (SeriesRecording r : seriesRecordings) { + if (!SoftPreconditions.checkArgument(mSeriesRecordings.containsKey(r.getId()), TAG, + "Non Existing Series ID: " + r)) { + continue; + } SeriesRecording old1 = mSeriesRecordings.put(r.getId(), r); SeriesRecording old2 = mSeriesId2SeriesRecordings.put(r.getSeriesId(), r); SoftPreconditions.checkArgument(old1.equals(old2), TAG, "Series ID cannot be" @@ -769,14 +820,6 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { return r.getInputId().equals(inputId); } }); - List movedSeriesRecordings = - moveElements(mSeriesRecordingsForRemovedInput, mSeriesRecordings, - new Filter() { - @Override - public boolean filter(SeriesRecording r) { - return r.getInputId().equals(inputId); - } - }); List movedRecordedPrograms = moveElements(mRecordedProgramsForRemovedInput, mRecordedPrograms, new Filter() { @@ -785,6 +828,21 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { return r.getInputId().equals(inputId); } }); + List removedSeriesRecordings = new ArrayList<>(); + List movedSeriesRecordings = + moveElements(mSeriesRecordingsForRemovedInput, mSeriesRecordings, + new Filter() { + @Override + public boolean filter(SeriesRecording r) { + if (r.getInputId().equals(inputId)) { + if (!isEmptySeriesRecording(r)) { + return true; + } + removedSeriesRecordings.add(r); + } + return false; + } + }); if (!movedSchedules.isEmpty()) { for (ScheduledRecording schedule : movedSchedules) { mProgramId2ScheduledRecordings.put(schedule.getProgramId(), schedule); @@ -795,6 +853,11 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { mSeriesId2SeriesRecordings.put(seriesRecording.getSeriesId(), seriesRecording); } } + for (SeriesRecording r : removedSeriesRecordings) { + mSeriesRecordingsForRemovedInput.remove(r.getId()); + } + new AsyncDeleteSeriesRecordingTask(mContext).executeOnDbThread( + SeriesRecording.toArray(removedSeriesRecordings)); // Notify after all the data are moved. if (!movedSchedules.isEmpty()) { notifyScheduledRecordingAdded(ScheduledRecording.toArray(movedSchedules)); @@ -811,20 +874,20 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { if (DEBUG) Log.d(TAG, "hideInput " + inputId); List movedSchedules = moveElements(mScheduledRecordings, mScheduledRecordingsForRemovedInput, - new Filter() { - @Override - public boolean filter(ScheduledRecording r) { - return r.getInputId().equals(inputId); - } - }); + new Filter() { + @Override + public boolean filter(ScheduledRecording r) { + return r.getInputId().equals(inputId); + } + }); List movedSeriesRecordings = moveElements(mSeriesRecordings, mSeriesRecordingsForRemovedInput, - new Filter() { - @Override - public boolean filter(SeriesRecording r) { - return r.getInputId().equals(inputId); - } - }); + new Filter() { + @Override + public boolean filter(SeriesRecording r) { + return r.getInputId().equals(inputId); + } + }); List movedRecordedPrograms = moveElements(mRecordedPrograms, mRecordedProgramsForRemovedInput, new Filter() { @@ -855,6 +918,15 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { } } + private void checkAndRemoveEmptySeriesRecording(Set seriesRecordingIds) { + int i = 0; + long[] rIds = new long[seriesRecordingIds.size()]; + for (long rId : seriesRecordingIds) { + rIds[i++] = rId; + } + checkAndRemoveEmptySeriesRecording(rIds); + } + @Override public void forgetStorage(String inputId) { List schedulesToDelete = new ArrayList<>(); @@ -901,6 +973,25 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { }.executeOnDbThread(); } + private void validateSeriesRecordings() { + Iterator iter = mSeriesRecordings.values().iterator(); + List removedSeriesRecordings = new ArrayList<>(); + while (iter.hasNext()) { + SeriesRecording r = iter.next(); + if (isEmptySeriesRecording(r)) { + iter.remove(); + removedSeriesRecordings.add(r); + } + } + if (!removedSeriesRecordings.isEmpty()) { + SeriesRecording[] removed = SeriesRecording.toArray(removedSeriesRecordings); + new AsyncDeleteSeriesRecordingTask(mContext).executeOnDbThread(removed); + if (mDvrLoadFinished) { + notifySeriesRecordingRemoved(removed); + } + } + } + private final class RecordedProgramsQueryTask extends AsyncRecordedProgramQueryTask { private final Uri mUri; diff --git a/src/com/android/tv/dvr/DvrDbSync.java b/src/com/android/tv/dvr/DvrDbSync.java deleted file mode 100644 index df181455..00000000 --- a/src/com/android/tv/dvr/DvrDbSync.java +++ /dev/null @@ -1,363 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.dvr; - -import android.annotation.SuppressLint; -import android.annotation.TargetApi; -import android.content.ContentUris; -import android.content.Context; -import android.database.ContentObserver; -import android.media.tv.TvContract.Programs; -import android.net.Uri; -import android.os.Build; -import android.os.Handler; -import android.os.Looper; -import android.support.annotation.MainThread; -import android.support.annotation.VisibleForTesting; -import android.util.Log; - -import com.android.tv.TvApplication; -import com.android.tv.data.ChannelDataManager; -import com.android.tv.data.Program; -import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; -import com.android.tv.util.AsyncDbTask.AsyncQueryProgramTask; -import com.android.tv.util.TvProviderUriMatcher; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Objects; -import java.util.Queue; -import java.util.Set; - -/** - * A class to synchronizes DVR DB with TvProvider. - * - *

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

This service is responsible for: - *

    - *
  • Send record commands to TV inputs
  • - *
  • Wake up at proper timing for recording
  • - *
  • Deconflict schedule, handling overlapping times etc.
  • - *
  • - * - *
- * - *

The service does not stop it self. - */ -public class DvrRecordingService extends Service { - private static final String TAG = "DvrRecordingService"; - private static final boolean DEBUG = false; - public static final String HANDLER_THREAD_NAME = "DvrRecordingService-handler"; - - public static void startService(Context context) { - Intent dvrSchedulerIntent = new Intent(context, DvrRecordingService.class); - context.startService(dvrSchedulerIntent); - } - - private final Clock mClock = Clock.SYSTEM; - private RecurringRunner mReaperRunner; - - private Scheduler mScheduler; - private HandlerThread mHandlerThread; - - @Override - public void onCreate() { - TvApplication.setCurrentRunningProcess(this, true); - if (DEBUG) Log.d(TAG, "onCreate"); - super.onCreate(); - SoftPreconditions.checkFeatureEnabled(this, CommonFeatures.DVR, TAG); - ApplicationSingletons singletons = TvApplication.getSingletons(this); - WritableDvrDataManager dataManager = (WritableDvrDataManager) singletons.getDvrDataManager(); - - AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); - // mScheduler may have been set for testing. - if (mScheduler == null) { - mHandlerThread = new HandlerThread(HANDLER_THREAD_NAME); - mHandlerThread.start(); - mScheduler = new Scheduler(mHandlerThread.getLooper(), singletons.getDvrManager(), - singletons.getInputSessionManager(), dataManager, - singletons.getChannelDataManager(), singletons.getTvInputManagerHelper(), this, - mClock, alarmManager); - mScheduler.start(); - } - mReaperRunner = new RecurringRunner(this, java.util.concurrent.TimeUnit.DAYS.toMillis(1), - new ScheduledProgramReaper(dataManager, mClock), null); - mReaperRunner.start(); - } - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - if (DEBUG) Log.d(TAG, "onStartCommand (" + intent + "," + flags + "," + startId + ")"); - mScheduler.update(); - return START_STICKY; - } - - @Override - public void onDestroy() { - if (DEBUG) Log.d(TAG, "onDestroy"); - mReaperRunner.stop(); - mScheduler.stop(); - mScheduler = null; - if (mHandlerThread != null) { - mHandlerThread.quitSafely(); - mHandlerThread = null; - } - super.onDestroy(); - } - - @Nullable - @Override - public IBinder onBind(Intent intent) { - return null; - } - - @VisibleForTesting - void setScheduler(Scheduler scheduler) { - Log.i(TAG, "Setting scheduler for tests to " + scheduler); - mScheduler = scheduler; - } -} diff --git a/src/com/android/tv/dvr/DvrScheduleManager.java b/src/com/android/tv/dvr/DvrScheduleManager.java index a5851a75..b72117aa 100644 --- a/src/com/android/tv/dvr/DvrScheduleManager.java +++ b/src/com/android/tv/dvr/DvrScheduleManager.java @@ -24,7 +24,6 @@ import android.support.annotation.MainThread; import android.support.annotation.NonNull; import android.support.annotation.VisibleForTesting; import android.util.ArraySet; -import android.util.LongSparseArray; import android.util.Range; import com.android.tv.ApplicationSingletons; @@ -35,7 +34,10 @@ import com.android.tv.data.ChannelDataManager; import com.android.tv.data.Program; import com.android.tv.dvr.DvrDataManager.OnDvrScheduleLoadFinishedListener; import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; +import com.android.tv.dvr.recorder.InputTaskScheduler; import com.android.tv.util.CompositeComparator; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.data.SeriesRecording; import com.android.tv.util.Utils; import java.util.ArrayList; @@ -88,9 +90,8 @@ public class DvrScheduleManager { private final Map> mInputScheduleMap = new HashMap<>(); // The inner map is a hash map from scheduled recording to its conflicting status, i.e., // the boolean value true denotes the schedule is just partially conflicting, which means - // although there's conflictit, it might still be recorded partially. - private final Map> mInputConflictInfoMap = - new HashMap<>(); + // although there's conflict, it might still be recorded partially. + private final Map> mInputConflictInfoMap = new HashMap<>(); private boolean mInitialized; @@ -171,10 +172,9 @@ public class DvrScheduleManager { mInputScheduleMap.remove(inputId); } } - Map conflictInfo = - mInputConflictInfoMap.get(inputId); + Map conflictInfo = mInputConflictInfoMap.get(inputId); if (conflictInfo != null) { - conflictInfo.remove(schedule); + conflictInfo.remove(schedule.getId()); if (conflictInfo.isEmpty()) { mInputConflictInfoMap.remove(inputId); } @@ -221,21 +221,11 @@ public class DvrScheduleManager { mInputScheduleMap.remove(inputId); } // Update conflict list as well - Map conflictInfo = - mInputConflictInfoMap.get(inputId); + Map conflictInfo = mInputConflictInfoMap.get(inputId); if (conflictInfo != null) { - // Compare ID because ScheduledRecording.equals() doesn't work if the state - // is changed. - ScheduledRecording oldSchedule = null; - for (ScheduledRecording s : conflictInfo.keySet()) { - if (s.getId() == schedule.getId()) { - oldSchedule = s; - break; - } - } - if (oldSchedule != null) { - conflictInfo.put(schedule, conflictInfo.get(oldSchedule)); - conflictInfo.remove(oldSchedule); + ConflictInfo oldConflictInfo = conflictInfo.get(schedule.getId()); + if (oldConflictInfo != null) { + oldConflictInfo.schedule = schedule; } } } @@ -317,24 +307,25 @@ public class DvrScheduleManager { List addedConflicts = new ArrayList<>(); List removedConflicts = new ArrayList<>(); for (String inputId : mInputScheduleMap.keySet()) { - Map oldConflictsInfo = mInputConflictInfoMap.get(inputId); + Map oldConflictInfo = mInputConflictInfoMap.get(inputId); Map oldConflictMap = new HashMap<>(); - if (oldConflictsInfo != null) { - for (ScheduledRecording r : oldConflictsInfo.keySet()) { - oldConflictMap.put(r.getId(), r); + if (oldConflictInfo != null) { + for (ConflictInfo conflictInfo : oldConflictInfo.values()) { + oldConflictMap.put(conflictInfo.schedule.getId(), conflictInfo.schedule); } } - Map conflictInfo = getConflictingSchedulesInfo(inputId); - if (conflictInfo.isEmpty()) { + List conflicts = getConflictingSchedulesInfo(inputId); + if (conflicts.isEmpty()) { mInputConflictInfoMap.remove(inputId); } else { - mInputConflictInfoMap.put(inputId, conflictInfo); - List conflicts = new ArrayList<>(conflictInfo.keySet()); - for (ScheduledRecording r : conflicts) { - if (oldConflictMap.remove(r.getId()) == null) { - addedConflicts.add(r); + Map conflictInfos = new HashMap<>(); + for (ConflictInfo conflictInfo : conflicts) { + conflictInfos.put(conflictInfo.schedule.getId(), conflictInfo); + if (oldConflictMap.remove(conflictInfo.schedule.getId()) == null) { + addedConflicts.add(conflictInfo.schedule); } } + mInputConflictInfoMap.put(inputId, conflictInfos); } removedConflicts.addAll(oldConflictMap.values()); } @@ -565,8 +556,7 @@ public class DvrScheduleManager { } /** - * Returns list of all conflicting scheduled recordings with schedules belonging to {@code - * seriesRecording} + * Returns list of all conflicting scheduled recordings for the given {@code seriesRecording} * recording. *

* Any empty list means there is no conflicts. @@ -581,9 +571,18 @@ public class DvrScheduleManager { if (input == null || !input.canRecord() || input.getTunerCount() <= 0) { return Collections.emptyList(); } - List schedulesForSeries = mDataManager.getScheduledRecordings( + List scheduledRecordingForSeries = mDataManager.getScheduledRecordings( seriesRecording.getId()); - return getConflictingSchedules(input, schedulesForSeries); + List availableScheduledRecordingForSeries = new ArrayList<>(); + for (ScheduledRecording scheduledRecording : scheduledRecordingForSeries) { + if (scheduledRecording.isNotStarted() || scheduledRecording.isInProgress()) { + availableScheduledRecordingForSeries.add(scheduledRecording); + } + } + if (availableScheduledRecordingForSeries.isEmpty()) { + return Collections.emptyList(); + } + return getConflictingSchedules(input, availableScheduledRecordingForSeries); } /** @@ -617,16 +616,16 @@ public class DvrScheduleManager { * the given input. */ @NonNull - private Map getConflictingSchedulesInfo(String inputId) { + private List getConflictingSchedulesInfo(String inputId) { SoftPreconditions.checkState(mInitialized, TAG, "Not initialized yet"); TvInputInfo input = Utils.getTvInputInfoForInputId(mContext, inputId); SoftPreconditions.checkState(input != null, TAG, "Can't find input for : " + inputId); if (!mInitialized || input == null) { - return Collections.emptyMap(); + return Collections.emptyList(); } List schedules = mInputScheduleMap.get(input.getId()); if (schedules == null || schedules.isEmpty()) { - return Collections.emptyMap(); + return Collections.emptyList(); } return getConflictingSchedulesInfo(schedules, input.getTunerCount()); } @@ -645,8 +644,8 @@ public class DvrScheduleManager { if (!mInitialized || input == null) { return false; } - Map conflicts = mInputConflictInfoMap.get(input.getId()); - return conflicts != null && conflicts.containsKey(schedule); + Map conflicts = mInputConflictInfoMap.get(input.getId()); + return conflicts != null && conflicts.containsKey(schedule.getId()); } /** @@ -664,8 +663,12 @@ public class DvrScheduleManager { if (!mInitialized || input == null) { return false; } - Map conflicts = mInputConflictInfoMap.get(input.getId()); - return conflicts != null && conflicts.getOrDefault(schedule, false); + Map conflicts = mInputConflictInfoMap.get(input.getId()); + if (conflicts != null) { + ConflictInfo conflictInfo = conflicts.get(schedule.getId()); + return conflictInfo != null && conflictInfo.partialConflict; + } + return false; } /** @@ -813,15 +816,17 @@ public class DvrScheduleManager { @VisibleForTesting static List getConflictingSchedules( List schedules, int tunerCount, List> periods) { - List result = new ArrayList<>( - getConflictingSchedulesInfo(schedules, tunerCount, periods).keySet()); - Collections.sort(result, RESULT_COMPARATOR); + List result = new ArrayList<>(); + for (ConflictInfo conflictInfo : + getConflictingSchedulesInfo(schedules, tunerCount, periods)) { + result.add(conflictInfo.schedule); + } return result; } @VisibleForTesting - static Map getConflictingSchedulesInfo( - List schedules, int tunerCount) { + static List getConflictingSchedulesInfo(List schedules, + int tunerCount) { return getConflictingSchedulesInfo(schedules, tunerCount, null); } @@ -836,13 +841,13 @@ public class DvrScheduleManager { * to be partially recorded under the given schedules and tuner count {@code true}, * or not {@code false}. */ - private static Map getConflictingSchedulesInfo( + private static List getConflictingSchedulesInfo( List schedules, int tunerCount, List> periods) { List schedulesToCheck = new ArrayList<>(schedules); // Sort by the same order as that in InputTaskScheduler. Collections.sort(schedulesToCheck, InputTaskScheduler.getRecordingOrderComparator()); List recordings = new ArrayList<>(); - Map conflicts = new HashMap<>(); + Map conflicts = new HashMap<>(); Map modified2OriginalSchedules = new HashMap<>(); // Simulate InputTaskScheduler. while (!schedulesToCheck.isEmpty()) { @@ -853,26 +858,29 @@ public class DvrScheduleManager { if (modified2OriginalSchedules.containsKey(schedule)) { // Schedule has been modified, which means it's already conflicted. // Modify its state to partially conflicted. - conflicts.put(modified2OriginalSchedules.get(schedule), true); + ScheduledRecording originalSchedule = modified2OriginalSchedules.get(schedule); + conflicts.put(originalSchedule, new ConflictInfo(originalSchedule, true)); } } else { ScheduledRecording candidate = findReplaceableRecording(recordings, schedule); if (candidate != null) { if (!modified2OriginalSchedules.containsKey(candidate)) { - conflicts.put(candidate, true); + conflicts.put(candidate, new ConflictInfo(candidate, true)); } recordings.remove(candidate); recordings.add(schedule); if (modified2OriginalSchedules.containsKey(schedule)) { // Schedule has been modified, which means it's already conflicted. // Modify its state to partially conflicted. - conflicts.put(modified2OriginalSchedules.get(schedule), true); + ScheduledRecording originalSchedule = + modified2OriginalSchedules.get(schedule); + conflicts.put(originalSchedule, new ConflictInfo(originalSchedule, true)); } } else { if (!modified2OriginalSchedules.containsKey(schedule)) { // if schedule has been modified, it's already conflicted. // No need to add it again. - conflicts.put(schedule, false); + conflicts.put(schedule, new ConflictInfo(schedule, false)); } long earliestEndTime = getEarliestEndTime(recordings); if (earliestEndTime < schedule.getEndTimeMs()) { @@ -912,7 +920,14 @@ public class DvrScheduleManager { } } } - return conflicts; + List result = new ArrayList<>(conflicts.values()); + Collections.sort(result, new Comparator() { + @Override + public int compare(ConflictInfo lhs, ConflictInfo rhs) { + return RESULT_COMPARATOR.compare(lhs.schedule, rhs.schedule); + } + }); + return result; } private static void removeFinishedRecordings(List recordings, @@ -954,6 +969,17 @@ public class DvrScheduleManager { return earliest; } + @VisibleForTesting + static class ConflictInfo { + public ScheduledRecording schedule; + public boolean partialConflict; + + ConflictInfo(ScheduledRecording schedule, boolean partialConflict) { + this.schedule = schedule; + this.partialConflict = partialConflict; + } + } + /** * A listener which is notified the initialization of schedule manager. */ @@ -970,6 +996,9 @@ public class DvrScheduleManager { public interface OnConflictStateChangeListener { /** * Called when the conflicting schedules change. + *

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

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

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

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

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

SeriesId is an opaque but stable string. - */ - public String getSeriesId() { - return mSeriesId; - } - - /** - * If not == {@link SeriesRecordings#THE_BEGINNING} and seasonNumber == startFromSeason then - * only record episodes with a episodeNumber >= this - */ - public int getStartFromEpisode() { - return mStartFromEpisode; - } - - /** - * If not == {@link SeriesRecordings#THE_BEGINNING} then only record episodes with a - * seasonNumber >= this - */ - public int getStartFromSeason() { - return mStartFromSeason; - } - - /** - * Returns the channel recording option. - */ - @ChannelOption public int getChannelOption() { - return mChannelOption; - } - - /** - * Returns the canonical genre ID's. - */ - public int[] getCanonicalGenreIds() { - return mCanonicalGenreIds; - } - - /** - * Returns the poster URI. - */ - public String getPosterUri() { - return mPosterUri; - } - - /** - * Returns the photo URI. - */ - public String getPhotoUri() { - return mPhotoUri; - } - - /** - * Returns the state of series recording. - */ - @SeriesState public int getState() { - return mState; - } - - /** - * Checks whether the series recording is stopped or not. - */ - public boolean isStopped() { - return mState == STATE_SERIES_STOPPED; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof SeriesRecording)) return false; - SeriesRecording that = (SeriesRecording) o; - return mPriority == that.mPriority - && mChannelId == that.mChannelId - && mStartFromSeason == that.mStartFromSeason - && mStartFromEpisode == that.mStartFromEpisode - && Objects.equals(mId, that.mId) - && Objects.equals(mTitle, that.mTitle) - && Objects.equals(mDescription, that.mDescription) - && Objects.equals(mLongDescription, that.mLongDescription) - && Objects.equals(mSeriesId, that.mSeriesId) - && mChannelOption == that.mChannelOption - && Arrays.equals(mCanonicalGenreIds, that.mCanonicalGenreIds) - && Objects.equals(mPosterUri, that.mPosterUri) - && Objects.equals(mPhotoUri, that.mPhotoUri) - && mState == that.mState; - } - - @Override - public int hashCode() { - return Objects.hash(mPriority, mChannelId, mStartFromSeason, mStartFromEpisode, mId, - mTitle, mDescription, mLongDescription, mSeriesId, mChannelOption, - mCanonicalGenreIds, mPosterUri, mPhotoUri, mState); - } - - @Override - public String toString() { - return "SeriesRecording{" + - "inputId=" + mInputId + - ", channelId=" + mChannelId + - ", id='" + mId + '\'' + - ", priority=" + mPriority + - ", title='" + mTitle + '\'' + - ", description='" + mDescription + '\'' + - ", longDescription='" + mLongDescription + '\'' + - ", startFromSeason=" + mStartFromSeason + - ", startFromEpisode=" + mStartFromEpisode + - ", channelOption=" + mChannelOption + - ", canonicalGenreIds=" + Arrays.toString(mCanonicalGenreIds) + - ", posterUri=" + mPosterUri + - ", photoUri=" + mPhotoUri + - ", state=" + mState + - '}'; - } - - private SeriesRecording(long id, long priority, String title, String description, - String longDescription, String inputId, long channelId, String seriesId, - int startFromSeason, int startFromEpisode, int channelOption, int[] canonicalGenreIds, - String posterUri, String photoUri, int state) { - this.mId = id; - this.mPriority = priority; - this.mTitle = title; - this.mDescription = description; - this.mLongDescription = longDescription; - this.mInputId = inputId; - this.mChannelId = channelId; - this.mSeriesId = seriesId; - this.mStartFromSeason = startFromSeason; - this.mStartFromEpisode = startFromEpisode; - this.mChannelOption = channelOption; - this.mCanonicalGenreIds = canonicalGenreIds; - this.mPosterUri = posterUri; - this.mPhotoUri = photoUri; - this.mState = state; - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel out, int paramInt) { - out.writeLong(mId); - out.writeLong(mPriority); - out.writeString(mTitle); - out.writeString(mDescription); - out.writeString(mLongDescription); - out.writeString(mInputId); - out.writeLong(mChannelId); - out.writeString(mSeriesId); - out.writeInt(mStartFromSeason); - out.writeInt(mStartFromEpisode); - out.writeInt(mChannelOption); - out.writeIntArray(mCanonicalGenreIds); - out.writeString(mPosterUri); - out.writeString(mPhotoUri); - out.writeInt(mState); - } - - /** - * Returns an array containing all of the elements in the list. - */ - public static SeriesRecording[] toArray(Collection series) { - return series.toArray(new SeriesRecording[series.size()]); - } - - /** - * Returns {@code true} if the {@code program} is part of the series and meets the season and - * episode constraints. - */ - public boolean matchProgram(Program program) { - return matchProgram(program, mChannelOption); - } - - /** - * Returns {@code true} if the {@code program} is part of the series and meets the season and - * episode constraints. It checks the channel option only if {@code checkChannelOption} is - * {@code true}. - */ - public boolean matchProgram(Program program, @ChannelOption int channelOption) { - String seriesId = program.getSeriesId(); - long channelId = program.getChannelId(); - String seasonNumber = program.getSeasonNumber(); - String episodeNumber = program.getEpisodeNumber(); - if (!mSeriesId.equals(seriesId) || (channelOption == SeriesRecording.OPTION_CHANNEL_ONE - && mChannelId != channelId)) { - return false; - } - // Season number and episode number matches if - // start_season_number < program_season_number - // || (start_season_number == program_season_number - // && start_episode_number <= program_episode_number). - if (mStartFromSeason == SeriesRecordings.THE_BEGINNING - || TextUtils.isEmpty(seasonNumber)) { - return true; - } else { - int intSeasonNumber; - try { - intSeasonNumber = Integer.valueOf(seasonNumber); - } catch (NumberFormatException e) { - return true; - } - if (intSeasonNumber > mStartFromSeason) { - return true; - } else if (intSeasonNumber < mStartFromSeason) { - return false; - } - } - if (mStartFromEpisode == SeriesRecordings.THE_BEGINNING - || TextUtils.isEmpty(episodeNumber)) { - return true; - } else { - int intEpisodeNumber; - try { - intEpisodeNumber = Integer.valueOf(episodeNumber); - } catch (NumberFormatException e) { - return true; - } - return intEpisodeNumber >= mStartFromEpisode; - } - } -} diff --git a/src/com/android/tv/dvr/SeriesRecordingScheduler.java b/src/com/android/tv/dvr/SeriesRecordingScheduler.java deleted file mode 100644 index 5ed12ce8..00000000 --- a/src/com/android/tv/dvr/SeriesRecordingScheduler.java +++ /dev/null @@ -1,579 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.dvr; - -import android.annotation.SuppressLint; -import android.annotation.TargetApi; -import android.content.Context; -import android.content.SharedPreferences; -import android.os.AsyncTask; -import android.os.Build; -import android.support.annotation.MainThread; -import android.support.annotation.VisibleForTesting; -import android.text.TextUtils; -import android.util.ArraySet; -import android.util.Log; -import android.util.LongSparseArray; - -import com.android.tv.ApplicationSingletons; -import com.android.tv.TvApplication; -import com.android.tv.common.CollectionUtils; -import com.android.tv.common.SharedPreferencesUtils; -import com.android.tv.common.SoftPreconditions; -import com.android.tv.data.Program; -import com.android.tv.data.epg.EpgFetcher; -import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; -import com.android.tv.dvr.DvrDataManager.SeriesRecordingListener; -import com.android.tv.dvr.EpisodicProgramLoadTask.ScheduledEpisode; -import com.android.tv.experiments.Experiments; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.concurrent.CopyOnWriteArraySet; -import java.util.Set; - -/** - * Creates the {@link ScheduledRecording}s for the {@link SeriesRecording}. - *

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

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

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

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

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

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

SeriesId is an opaque but stable string. + */ + public String getSeriesId() { + return mSeriesId; + } + + /** + * If not == {@link SeriesRecordings#THE_BEGINNING} and seasonNumber == startFromSeason then + * only record episodes with a episodeNumber >= this + */ + public int getStartFromEpisode() { + return mStartFromEpisode; + } + + /** + * If not == {@link SeriesRecordings#THE_BEGINNING} then only record episodes with a + * seasonNumber >= this + */ + public int getStartFromSeason() { + return mStartFromSeason; + } + + /** + * Returns the channel recording option. + */ + @ChannelOption public int getChannelOption() { + return mChannelOption; + } + + /** + * Returns the canonical genre ID's. + */ + public int[] getCanonicalGenreIds() { + return mCanonicalGenreIds; + } + + /** + * Returns the poster URI. + */ + public String getPosterUri() { + return mPosterUri; + } + + /** + * Returns the photo URI. + */ + public String getPhotoUri() { + return mPhotoUri; + } + + /** + * Returns the state of series recording. + */ + @SeriesState public int getState() { + return mState; + } + + /** + * Checks whether the series recording is stopped or not. + */ + public boolean isStopped() { + return mState == STATE_SERIES_STOPPED; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof SeriesRecording)) return false; + SeriesRecording that = (SeriesRecording) o; + return mPriority == that.mPriority + && mChannelId == that.mChannelId + && mStartFromSeason == that.mStartFromSeason + && mStartFromEpisode == that.mStartFromEpisode + && Objects.equals(mId, that.mId) + && Objects.equals(mTitle, that.mTitle) + && Objects.equals(mDescription, that.mDescription) + && Objects.equals(mLongDescription, that.mLongDescription) + && Objects.equals(mSeriesId, that.mSeriesId) + && mChannelOption == that.mChannelOption + && Arrays.equals(mCanonicalGenreIds, that.mCanonicalGenreIds) + && Objects.equals(mPosterUri, that.mPosterUri) + && Objects.equals(mPhotoUri, that.mPhotoUri) + && mState == that.mState; + } + + @Override + public int hashCode() { + return Objects.hash(mPriority, mChannelId, mStartFromSeason, mStartFromEpisode, mId, + mTitle, mDescription, mLongDescription, mSeriesId, mChannelOption, + mCanonicalGenreIds, mPosterUri, mPhotoUri, mState); + } + + @Override + public String toString() { + return "SeriesRecording{" + + "inputId=" + mInputId + + ", channelId=" + mChannelId + + ", id='" + mId + '\'' + + ", priority=" + mPriority + + ", title='" + mTitle + '\'' + + ", description='" + mDescription + '\'' + + ", longDescription='" + mLongDescription + '\'' + + ", startFromSeason=" + mStartFromSeason + + ", startFromEpisode=" + mStartFromEpisode + + ", channelOption=" + mChannelOption + + ", canonicalGenreIds=" + Arrays.toString(mCanonicalGenreIds) + + ", posterUri=" + mPosterUri + + ", photoUri=" + mPhotoUri + + ", state=" + mState + + '}'; + } + + private SeriesRecording(long id, long priority, String title, String description, + String longDescription, String inputId, long channelId, String seriesId, + int startFromSeason, int startFromEpisode, int channelOption, int[] canonicalGenreIds, + String posterUri, String photoUri, int state) { + this.mId = id; + this.mPriority = priority; + this.mTitle = title; + this.mDescription = description; + this.mLongDescription = longDescription; + this.mInputId = inputId; + this.mChannelId = channelId; + this.mSeriesId = seriesId; + this.mStartFromSeason = startFromSeason; + this.mStartFromEpisode = startFromEpisode; + this.mChannelOption = channelOption; + this.mCanonicalGenreIds = canonicalGenreIds; + this.mPosterUri = posterUri; + this.mPhotoUri = photoUri; + this.mState = state; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel out, int paramInt) { + out.writeLong(mId); + out.writeLong(mPriority); + out.writeString(mTitle); + out.writeString(mDescription); + out.writeString(mLongDescription); + out.writeString(mInputId); + out.writeLong(mChannelId); + out.writeString(mSeriesId); + out.writeInt(mStartFromSeason); + out.writeInt(mStartFromEpisode); + out.writeInt(mChannelOption); + out.writeIntArray(mCanonicalGenreIds); + out.writeString(mPosterUri); + out.writeString(mPhotoUri); + out.writeInt(mState); + } + + /** + * Returns an array containing all of the elements in the list. + */ + public static SeriesRecording[] toArray(Collection series) { + return series.toArray(new SeriesRecording[series.size()]); + } + + /** + * Returns {@code true} if the {@code program} is part of the series and meets the season and + * episode constraints. + */ + public boolean matchProgram(Program program) { + return matchProgram(program, mChannelOption); + } + + /** + * Returns {@code true} if the {@code program} is part of the series and meets the season and + * episode constraints. It checks the channel option only if {@code checkChannelOption} is + * {@code true}. + */ + public boolean matchProgram(Program program, @ChannelOption int channelOption) { + String seriesId = program.getSeriesId(); + long channelId = program.getChannelId(); + String seasonNumber = program.getSeasonNumber(); + String episodeNumber = program.getEpisodeNumber(); + if (!mSeriesId.equals(seriesId) || (channelOption == SeriesRecording.OPTION_CHANNEL_ONE + && mChannelId != channelId)) { + return false; + } + // Season number and episode number matches if + // start_season_number < program_season_number + // || (start_season_number == program_season_number + // && start_episode_number <= program_episode_number). + if (mStartFromSeason == SeriesRecordings.THE_BEGINNING + || TextUtils.isEmpty(seasonNumber)) { + return true; + } else { + int intSeasonNumber; + try { + intSeasonNumber = Integer.valueOf(seasonNumber); + } catch (NumberFormatException e) { + return true; + } + if (intSeasonNumber > mStartFromSeason) { + return true; + } else if (intSeasonNumber < mStartFromSeason) { + return false; + } + } + if (mStartFromEpisode == SeriesRecordings.THE_BEGINNING + || TextUtils.isEmpty(episodeNumber)) { + return true; + } else { + int intEpisodeNumber; + try { + intEpisodeNumber = Integer.valueOf(episodeNumber); + } catch (NumberFormatException e) { + return true; + } + return intEpisodeNumber >= mStartFromEpisode; + } + } +} diff --git a/src/com/android/tv/dvr/provider/AsyncDvrDbTask.java b/src/com/android/tv/dvr/provider/AsyncDvrDbTask.java index 1a12fb23..c5383d02 100644 --- a/src/com/android/tv/dvr/provider/AsyncDvrDbTask.java +++ b/src/com/android/tv/dvr/provider/AsyncDvrDbTask.java @@ -21,8 +21,8 @@ import android.database.Cursor; import android.os.AsyncTask; import android.support.annotation.Nullable; -import com.android.tv.dvr.ScheduledRecording; -import com.android.tv.dvr.SeriesRecording; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.data.SeriesRecording; import com.android.tv.dvr.provider.DvrContract.Schedules; import com.android.tv.dvr.provider.DvrContract.SeriesRecordings; import com.android.tv.util.NamedThreadFactory; diff --git a/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java b/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java index 2f16ba5d..8b9481a9 100644 --- a/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java +++ b/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java @@ -27,8 +27,8 @@ import android.provider.BaseColumns; import android.text.TextUtils; import android.util.Log; -import com.android.tv.dvr.ScheduledRecording; -import com.android.tv.dvr.SeriesRecording; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.data.SeriesRecording; import com.android.tv.dvr.provider.DvrContract.Schedules; import com.android.tv.dvr.provider.DvrContract.SeriesRecordings; diff --git a/src/com/android/tv/dvr/provider/DvrDbSync.java b/src/com/android/tv/dvr/provider/DvrDbSync.java new file mode 100644 index 00000000..8a0c2d19 --- /dev/null +++ b/src/com/android/tv/dvr/provider/DvrDbSync.java @@ -0,0 +1,373 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.dvr.provider; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.content.ContentUris; +import android.content.Context; +import android.database.ContentObserver; +import android.media.tv.TvContract.Programs; +import android.net.Uri; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; +import android.support.annotation.MainThread; +import android.support.annotation.VisibleForTesting; +import android.util.Log; + +import com.android.tv.TvApplication; +import com.android.tv.data.ChannelDataManager; +import com.android.tv.data.Program; +import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; +import com.android.tv.dvr.DvrDataManagerImpl; +import com.android.tv.dvr.DvrManager; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.data.SeriesRecording; +import com.android.tv.dvr.recorder.SeriesRecordingScheduler; +import com.android.tv.util.AsyncDbTask.AsyncQueryProgramTask; +import com.android.tv.util.TvProviderUriMatcher; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; +import java.util.Queue; +import java.util.Set; + +/** + * A class to synchronizes DVR DB with TvProvider. + * + *

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

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

This service is responsible for: + *

    + *
  • Send record commands to TV inputs
  • + *
  • Wake up at proper timing for recording
  • + *
  • Deconflict schedule, handling overlapping times etc.
  • + *
  • + * + *
+ * + *

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

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

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

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

If there are no existing schedules for an episode, one program which starts earlier is + * picked. + */ + private LongSparseArray> pickOneProgramPerEpisode( + List seriesRecordings, List programs) { + return pickOneProgramPerEpisode(mDataManager, seriesRecordings, programs); + } + + /** + * @see #pickOneProgramPerEpisode(List, List) + */ + public static LongSparseArray> pickOneProgramPerEpisode( + DvrDataManager dataManager, List seriesRecordings, + List programs) { + // Initialize. + LongSparseArray> result = new LongSparseArray<>(); + Map seriesRecordingIds = new HashMap<>(); + for (SeriesRecording seriesRecording : seriesRecordings) { + result.put(seriesRecording.getId(), new ArrayList<>()); + seriesRecordingIds.put(seriesRecording.getSeriesId(), seriesRecording.getId()); + } + // Group programs by the episode. + Map> programsForEpisodeMap = new HashMap<>(); + for (Program program : programs) { + long seriesRecordingId = seriesRecordingIds.get(program.getSeriesId()); + if (TextUtils.isEmpty(program.getSeasonNumber()) + || TextUtils.isEmpty(program.getEpisodeNumber())) { + // Add all the programs if it doesn't have season number or episode number. + result.get(seriesRecordingId).add(program); + continue; + } + SeasonEpisodeNumber seasonEpisodeNumber = new SeasonEpisodeNumber(seriesRecordingId, + program.getSeasonNumber(), program.getEpisodeNumber()); + List programsForEpisode = programsForEpisodeMap.get(seasonEpisodeNumber); + if (programsForEpisode == null) { + programsForEpisode = new ArrayList<>(); + programsForEpisodeMap.put(seasonEpisodeNumber, programsForEpisode); + } + programsForEpisode.add(program); + } + // Pick one program. + for (Entry> entry : programsForEpisodeMap.entrySet()) { + List programsForEpisode = entry.getValue(); + Collections.sort(programsForEpisode, new Comparator() { + @Override + public int compare(Program lhs, Program rhs) { + // Place the existing schedule first. + boolean lhsScheduled = isProgramScheduled(dataManager, lhs); + boolean rhsScheduled = isProgramScheduled(dataManager, rhs); + if (lhsScheduled && !rhsScheduled) { + return -1; + } + if (!lhsScheduled && rhsScheduled) { + return 1; + } + // Sort by the start time in ascending order. + return lhs.compareTo(rhs); + } + }); + boolean added = false; + // Add all the scheduled programs + List programsForSeries = result.get(entry.getKey().seriesRecordingId); + for (Program program : programsForEpisode) { + if (isProgramScheduled(dataManager, program)) { + programsForSeries.add(program); + added = true; + } else if (!added) { + programsForSeries.add(program); + break; + } + } + } + return result; + } + + private static boolean isProgramScheduled(DvrDataManager dataManager, Program program) { + ScheduledRecording schedule = + dataManager.getScheduledRecordingForProgramId(program.getId()); + return schedule != null && schedule.getState() + == ScheduledRecording.STATE_RECORDING_NOT_STARTED; + } + + private void updateFetchedSeries() { + mSharedPreferences.edit().putStringSet(KEY_FETCHED_SERIES_IDS, mFetchedSeriesIds).apply(); + } + + /** + * This works only for the existing series recordings. Do not use this task for the + * "adding series recording" UI. + */ + private class SeriesRecordingUpdateTask extends EpisodicProgramLoadTask { + SeriesRecordingUpdateTask(List seriesRecordings) { + super(mContext, seriesRecordings); + } + + @Override + protected void onPostExecute(List programs) { + if (DEBUG) Log.d(TAG, "onPostExecute: updating schedules with programs:" + programs); + mScheduleTasks.remove(this); + if (programs == null) { + Log.e(TAG, "Creating schedules for series recording failed: " + + getSeriesRecordings()); + return; + } + LongSparseArray> seriesProgramMap = pickOneProgramPerEpisode( + getSeriesRecordings(), programs); + for (SeriesRecording seriesRecording : getSeriesRecordings()) { + // Check the series recording is still valid. + SeriesRecording actualSeriesRecording = mDataManager.getSeriesRecording( + seriesRecording.getId()); + if (actualSeriesRecording == null || actualSeriesRecording.isStopped()) { + continue; + } + List programsToSchedule = seriesProgramMap.get(seriesRecording.getId()); + if (mDataManager.getSeriesRecording(seriesRecording.getId()) != null + && !programsToSchedule.isEmpty()) { + mDvrManager.addScheduleToSeriesRecording(seriesRecording, programsToSchedule); + } + } + } + + @Override + protected void onCancelled(List programs) { + mScheduleTasks.remove(this); + } + + @Override + public String toString() { + return "SeriesRecordingUpdateTask:{" + + "series_recordings=" + getSeriesRecordings() + + "}"; + } + } + + private class FetchSeriesInfoTask extends AsyncTask { + private SeriesRecording mSeriesRecording; + + FetchSeriesInfoTask(SeriesRecording seriesRecording) { + mSeriesRecording = seriesRecording; + } + + @Override + protected SeriesInfo doInBackground(Void... voids) { + return EpgFetcher.createEpgReader(mContext) + .getSeriesInfo(mSeriesRecording.getSeriesId()); + } + + @Override + protected void onPostExecute(SeriesInfo seriesInfo) { + if (seriesInfo != null) { + mDataManager.updateSeriesRecording(SeriesRecording.buildFrom(mSeriesRecording) + .setTitle(seriesInfo.getTitle()) + .setDescription(seriesInfo.getDescription()) + .setLongDescription(seriesInfo.getLongDescription()) + .setCanonicalGenreIds(seriesInfo.getCanonicalGenreIds()) + .setPosterUri(seriesInfo.getPosterUri()) + .setPhotoUri(seriesInfo.getPhotoUri()) + .build()); + mFetchedSeriesIds.add(seriesInfo.getId()); + updateFetchedSeries(); + } + mFetchSeriesInfoTasks.remove(mSeriesRecording.getId()); + } + + @Override + protected void onCancelled(SeriesInfo seriesInfo) { + mFetchSeriesInfoTasks.remove(mSeriesRecording.getId()); + } + } +} diff --git a/src/com/android/tv/dvr/ui/ActionPresenterSelector.java b/src/com/android/tv/dvr/ui/ActionPresenterSelector.java deleted file mode 100644 index 8b8cd5c5..00000000 --- a/src/com/android/tv/dvr/ui/ActionPresenterSelector.java +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.tv.dvr.ui; - -import android.graphics.drawable.Drawable; -import android.support.v17.leanback.R; -import android.support.v17.leanback.widget.Action; -import android.support.v17.leanback.widget.Presenter; -import android.support.v17.leanback.widget.PresenterSelector; -import android.text.TextUtils; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Button; - -// This class is adapted from Leanback's library, which does not support action icon with one-line -// label. This class modified its getPresenter method to support the above situation. -class ActionPresenterSelector extends PresenterSelector { - private final Presenter mOneLineActionPresenter = new OneLineActionPresenter(); - private final Presenter mTwoLineActionPresenter = new TwoLineActionPresenter(); - private final Presenter[] mPresenters = new Presenter[] { - mOneLineActionPresenter, mTwoLineActionPresenter}; - - @Override - public Presenter getPresenter(Object item) { - Action action = (Action) item; - if (TextUtils.isEmpty(action.getLabel2()) && action.getIcon() == null) { - return mOneLineActionPresenter; - } else { - return mTwoLineActionPresenter; - } - } - - @Override - public Presenter[] getPresenters() { - return mPresenters; - } - - static class ActionViewHolder extends Presenter.ViewHolder { - Action mAction; - Button mButton; - int mLayoutDirection; - - public ActionViewHolder(View view, int layoutDirection) { - super(view); - mButton = (Button) view.findViewById(R.id.lb_action_button); - mLayoutDirection = layoutDirection; - } - } - - class OneLineActionPresenter extends Presenter { - @Override - public ViewHolder onCreateViewHolder(ViewGroup parent) { - View v = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.lb_action_1_line, parent, false); - return new ActionViewHolder(v, parent.getLayoutDirection()); - } - - @Override - public void onBindViewHolder(Presenter.ViewHolder viewHolder, Object item) { - Action action = (Action) item; - ActionViewHolder vh = (ActionViewHolder) viewHolder; - vh.mAction = action; - vh.mButton.setText(action.getLabel1()); - } - - @Override - public void onUnbindViewHolder(Presenter.ViewHolder viewHolder) { - ((ActionViewHolder) viewHolder).mAction = null; - } - } - - class TwoLineActionPresenter extends Presenter { - @Override - public ViewHolder onCreateViewHolder(ViewGroup parent) { - View v = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.lb_action_2_lines, parent, false); - return new ActionViewHolder(v, parent.getLayoutDirection()); - } - - @Override - public void onBindViewHolder(Presenter.ViewHolder viewHolder, Object item) { - Action action = (Action) item; - ActionViewHolder vh = (ActionViewHolder) viewHolder; - Drawable icon = action.getIcon(); - vh.mAction = action; - - if (icon != null) { - final int startPadding = vh.view.getResources() - .getDimensionPixelSize(R.dimen.lb_action_with_icon_padding_start); - final int endPadding = vh.view.getResources() - .getDimensionPixelSize(R.dimen.lb_action_with_icon_padding_end); - vh.view.setPaddingRelative(startPadding, 0, endPadding, 0); - } else { - final int padding = vh.view.getResources() - .getDimensionPixelSize(R.dimen.lb_action_padding_horizontal); - vh.view.setPaddingRelative(padding, 0, padding, 0); - } - if (vh.mLayoutDirection == View.LAYOUT_DIRECTION_RTL) { - vh.mButton.setCompoundDrawablesWithIntrinsicBounds(null, null, icon, null); - } else { - vh.mButton.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null); - } - - CharSequence line1 = action.getLabel1(); - CharSequence line2 = action.getLabel2(); - if (TextUtils.isEmpty(line1)) { - vh.mButton.setText(line2); - } else if (TextUtils.isEmpty(line2)) { - vh.mButton.setText(line1); - } else { - vh.mButton.setText(line1 + "\n" + line2); - } - } - - @Override - public void onUnbindViewHolder(Presenter.ViewHolder viewHolder) { - ActionViewHolder vh = (ActionViewHolder) viewHolder; - vh.mButton.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null); - vh.view.setPadding(0, 0, 0, 0); - vh.mAction = null; - } - } -} \ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/BigArguments.java b/src/com/android/tv/dvr/ui/BigArguments.java new file mode 100644 index 00000000..ec3b5065 --- /dev/null +++ b/src/com/android/tv/dvr/ui/BigArguments.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.dvr.ui; + +import android.support.annotation.NonNull; + +import com.android.tv.common.SoftPreconditions; + +import java.util.HashMap; +import java.util.Map; + +/** + * Stores the object to pass through activities/fragments. + */ +public class BigArguments { + private final static String TAG = "BigArguments"; + private static Map sBigArgumentMap = new HashMap<>(); + + /** + * Sets the argument. + */ + public static void setArgument(String name, @NonNull Object value) { + SoftPreconditions.checkState(value != null, TAG, "Set argument, but value is null"); + sBigArgumentMap.put(name, value); + } + + /** + * Returns the argument which is associated to the name. + */ + public static Object getArgument(String name) { + return sBigArgumentMap.get(name); + } + + /** + * Resets the arguments. + */ + public static void reset() { + sBigArgumentMap.clear(); + } +} diff --git a/src/com/android/tv/dvr/ui/ChangeImageTransformWithScaledParent.java b/src/com/android/tv/dvr/ui/ChangeImageTransformWithScaledParent.java new file mode 100644 index 00000000..cddece73 --- /dev/null +++ b/src/com/android/tv/dvr/ui/ChangeImageTransformWithScaledParent.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.dvr.ui; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Matrix; +import android.graphics.drawable.BitmapDrawable; +import android.transition.ChangeImageTransform; +import android.transition.TransitionValues; +import android.util.AttributeSet; +import android.view.View; +import android.widget.ImageView; +import android.widget.ImageView.ScaleType; + +import com.android.tv.R; + +import java.util.Map; + +/** + * TODO: Remove this class once b/32405620 is fixed. + * This class is for the workaround of b/32405620 and only for the shared element transition between + * {@link com.android.tv.dvr.ui.browse.RecordingCardView} and + * {@link com.android.tv.dvr.ui.browse.DvrDetailsActivity}. + */ +public class ChangeImageTransformWithScaledParent extends ChangeImageTransform { + private static final String PROPNAME_MATRIX = "android:changeImageTransform:matrix"; + + public ChangeImageTransformWithScaledParent(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public void captureStartValues(TransitionValues transitionValues) { + super.captureStartValues(transitionValues); + applyParentScale(transitionValues); + } + + @Override + public void captureEndValues(TransitionValues transitionValues) { + super.captureEndValues(transitionValues); + applyParentScale(transitionValues); + } + + private void applyParentScale(TransitionValues transitionValues) { + View view = transitionValues.view; + Map values = transitionValues.values; + Matrix matrix = (Matrix) values.get(PROPNAME_MATRIX); + if (matrix != null && view.getId() == R.id.details_overview_image + && view instanceof ImageView) { + ImageView imageView = (ImageView) view; + if (imageView.getScaleType() == ScaleType.CENTER_INSIDE + && imageView.getDrawable() instanceof BitmapDrawable) { + Bitmap bitmap = ((BitmapDrawable) imageView.getDrawable()).getBitmap(); + if (bitmap.getWidth() < imageView.getWidth() + && bitmap.getHeight() < imageView.getHeight()) { + float scale = imageView.getContext().getResources().getFraction( + R.fraction.lb_focus_zoom_factor_medium, 1, 1); + matrix.postScale(scale, scale, imageView.getWidth() / 2, + imageView.getHeight() / 2); + } + } + } + } +} diff --git a/src/com/android/tv/dvr/ui/CurrentRecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/CurrentRecordingDetailsFragment.java deleted file mode 100644 index 5d8e20ff..00000000 --- a/src/com/android/tv/dvr/ui/CurrentRecordingDetailsFragment.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.tv.dvr.ui; - -import android.content.res.Resources; -import android.support.v17.leanback.widget.Action; -import android.support.v17.leanback.widget.OnActionClickedListener; -import android.support.v17.leanback.widget.SparseArrayObjectAdapter; - -import com.android.tv.R; -import com.android.tv.TvApplication; -import com.android.tv.dvr.DvrManager; - -/** - * {@link RecordingDetailsFragment} for current recording in DVR. - */ -public class CurrentRecordingDetailsFragment extends RecordingDetailsFragment { - private static final int ACTION_STOP_RECORDING = 1; - - @Override - protected SparseArrayObjectAdapter onCreateActionsAdapter() { - SparseArrayObjectAdapter adapter = - new SparseArrayObjectAdapter(new ActionPresenterSelector()); - Resources res = getResources(); - adapter.set(ACTION_STOP_RECORDING, new Action(ACTION_STOP_RECORDING, - res.getString(R.string.epg_dvr_dialog_message_stop_recording), null, - res.getDrawable(R.drawable.lb_ic_stop))); - return adapter; - } - - @Override - protected OnActionClickedListener onCreateOnActionClickedListener() { - return new OnActionClickedListener() { - @Override - public void onActionClicked(Action action) { - if (action.getId() == ACTION_STOP_RECORDING) { - DvrManager dvrManager = TvApplication.getSingletons(getActivity()) - .getDvrManager(); - dvrManager.stopRecording(getRecording()); - } - getActivity().finish(); - } - }; - } -} diff --git a/src/com/android/tv/dvr/ui/DetailsContent.java b/src/com/android/tv/dvr/ui/DetailsContent.java deleted file mode 100644 index 19521fca..00000000 --- a/src/com/android/tv/dvr/ui/DetailsContent.java +++ /dev/null @@ -1,207 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.tv.dvr.ui; - -import android.media.tv.TvContract; -import android.support.annotation.Nullable; -import android.text.TextUtils; - -import com.android.tv.data.BaseProgram; -import com.android.tv.data.Channel; - -/** - * A class for details content. - */ -public class DetailsContent { - /** Constant for invalid time. */ - public static final long INVALID_TIME = -1; - - private CharSequence mTitle; - private long mStartTimeUtcMillis; - private long mEndTimeUtcMillis; - private String mDescription; - private String mLogoImageUri; - private String mBackgroundImageUri; - - private DetailsContent() { } - - /** - * Returns title. - */ - public CharSequence getTitle() { - return mTitle; - } - - /** - * Returns start time. - */ - public long getStartTimeUtcMillis() { - return mStartTimeUtcMillis; - } - - /** - * Returns end time. - */ - public long getEndTimeUtcMillis() { - return mEndTimeUtcMillis; - } - - /** - * Returns description. - */ - public String getDescription() { - return mDescription; - } - - /** - * Returns Logo image URI as a String. - */ - public String getLogoImageUri() { - return mLogoImageUri; - } - - /** - * Returns background image URI as a String. - */ - public String getBackgroundImageUri() { - return mBackgroundImageUri; - } - - /** - * Copies other details content. - */ - public void copyFrom(DetailsContent other) { - if (this == other) { - return; - } - mTitle = other.mTitle; - mStartTimeUtcMillis = other.mStartTimeUtcMillis; - mEndTimeUtcMillis = other.mEndTimeUtcMillis; - mDescription = other.mDescription; - mLogoImageUri = other.mLogoImageUri; - mBackgroundImageUri = other.mBackgroundImageUri; - } - - /** - * A class for building details content. - */ - public static final class Builder { - private final DetailsContent mDetailsContent; - - public Builder() { - mDetailsContent = new DetailsContent(); - mDetailsContent.mStartTimeUtcMillis = INVALID_TIME; - mDetailsContent.mEndTimeUtcMillis = INVALID_TIME; - } - - /** - * Sets title. - */ - public Builder setTitle(CharSequence title) { - mDetailsContent.mTitle = title; - return this; - } - - /** - * Sets start time. - */ - public Builder setStartTimeUtcMillis(long startTimeUtcMillis) { - mDetailsContent.mStartTimeUtcMillis = startTimeUtcMillis; - return this; - } - - /** - * Sets end time. - */ - public Builder setEndTimeUtcMillis(long endTimeUtcMillis) { - mDetailsContent.mEndTimeUtcMillis = endTimeUtcMillis; - return this; - } - - /** - * Sets description. - */ - public Builder setDescription(String description) { - mDetailsContent.mDescription = description; - return this; - } - - /** - * Sets logo image URI as a String. - */ - public Builder setLogoImageUri(String logoImageUri) { - mDetailsContent.mLogoImageUri = logoImageUri; - return this; - } - - /** - * Sets background image URI as a String. - */ - public Builder setBackgroundImageUri(String backgroundImageUri) { - mDetailsContent.mBackgroundImageUri = backgroundImageUri; - return this; - } - - /** - * Sets background image and logo image URI from program and channel. - */ - public Builder setImageUris(@Nullable BaseProgram program, @Nullable Channel channel) { - if (program != null) { - return setImageUris(program.getPosterArtUri(), program.getThumbnailUri(), channel); - } else { - return setImageUris(null, null, channel); - } - } - - /** - * Sets background image and logo image URI and channel is used for fallback images. - */ - public Builder setImageUris(@Nullable String posterArtUri, - @Nullable String thumbnailUri, @Nullable Channel channel) { - mDetailsContent.mLogoImageUri = null; - mDetailsContent.mBackgroundImageUri = null; - if (!TextUtils.isEmpty(posterArtUri) && !TextUtils.isEmpty(thumbnailUri)) { - mDetailsContent.mLogoImageUri = posterArtUri; - mDetailsContent.mBackgroundImageUri = thumbnailUri; - } else if (!TextUtils.isEmpty(posterArtUri)) { - // thumbnailUri is empty - mDetailsContent.mLogoImageUri = posterArtUri; - mDetailsContent.mBackgroundImageUri = posterArtUri; - } else if (!TextUtils.isEmpty(thumbnailUri)) { - // posterArtUri is empty - mDetailsContent.mLogoImageUri = thumbnailUri; - mDetailsContent.mBackgroundImageUri = thumbnailUri; - } - if (TextUtils.isEmpty(mDetailsContent.mLogoImageUri) && channel != null) { - String channelLogoUri = TvContract.buildChannelLogoUri(channel.getId()) - .toString(); - mDetailsContent.mLogoImageUri = channelLogoUri; - mDetailsContent.mBackgroundImageUri = channelLogoUri; - } - return this; - } - - /** - * Builds details content. - */ - public DetailsContent build() { - DetailsContent detailsContent = new DetailsContent(); - detailsContent.copyFrom(mDetailsContent); - return detailsContent; - } - } -} \ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/DetailsContentPresenter.java b/src/com/android/tv/dvr/ui/DetailsContentPresenter.java deleted file mode 100644 index 175f05bc..00000000 --- a/src/com/android/tv/dvr/ui/DetailsContentPresenter.java +++ /dev/null @@ -1,300 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.tv.dvr.ui; - -import android.app.Activity; -import android.animation.Animator; -import android.animation.AnimatorSet; -import android.animation.ObjectAnimator; -import android.animation.PropertyValuesHolder; -import android.graphics.Paint; -import android.graphics.Paint.FontMetricsInt; -import android.support.v17.leanback.widget.Presenter; -import android.text.TextUtils; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.view.ViewTreeObserver; -import android.widget.LinearLayout; -import android.widget.TextView; - -import com.android.tv.R; -import com.android.tv.ui.ViewUtils; -import com.android.tv.util.Utils; - -/** - * An {@link Presenter} for rendering a detailed description of an DVR item. - * Typically this Presenter will be used in a {@link DetailsOverviewRowPresenter}. - * Most codes of this class is originated from - * {@link android.support.v17.leanback.widget.AbstractDetailsDescriptionPresenter}. - * The latter class are re-used to provide a customized version of - * {@link android.support.v17.leanback.widget.DetailsOverviewRow}. - */ -public class DetailsContentPresenter extends Presenter { - /** - * The ViewHolder for the {@link DetailsContentPresenter}. - */ - public static class ViewHolder extends Presenter.ViewHolder { - final TextView mTitle; - final TextView mSubtitle; - final LinearLayout mDescriptionContainer; - final TextView mBody; - final TextView mReadMoreView; - final int mTitleMargin; - final int mUnderTitleBaselineMargin; - final int mUnderSubtitleBaselineMargin; - final int mTitleLineSpacing; - final int mBodyLineSpacing; - final int mBodyMaxLines; - final int mBodyMinLines; - final FontMetricsInt mTitleFontMetricsInt; - final FontMetricsInt mSubtitleFontMetricsInt; - final FontMetricsInt mBodyFontMetricsInt; - final int mTitleMaxLines; - - private Activity mActivity; - private boolean mFullTextMode; - private int mFullTextAnimationDuration; - private boolean mIsListeningToPreDraw; - - private ViewTreeObserver.OnPreDrawListener mPreDrawListener = - new ViewTreeObserver.OnPreDrawListener() { - @Override - public boolean onPreDraw() { - if (mSubtitle.getVisibility() == View.VISIBLE - && mSubtitle.getTop() > view.getHeight() - && mTitle.getLineCount() > 1) { - mTitle.setMaxLines(mTitle.getLineCount() - 1); - return false; - } - final int bodyLines = mBody.getLineCount(); - final int maxLines = mFullTextMode ? bodyLines : - (mTitle.getLineCount() > 1 ? mBodyMinLines : mBodyMaxLines); - if (bodyLines > maxLines) { - mReadMoreView.setVisibility(View.VISIBLE); - mDescriptionContainer.setFocusable(true); - mDescriptionContainer.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - mFullTextMode = true; - mReadMoreView.setVisibility(View.GONE); - mDescriptionContainer.setFocusable(false); - mDescriptionContainer.setOnClickListener(null); - mBody.setMaxLines(bodyLines); - // Minus 1 from line difference to eliminate the space - // originally occupied by "READ MORE" - showFullText((bodyLines - maxLines - 1) * mBodyLineSpacing); - } - }); - } - if (mBody.getMaxLines() != maxLines) { - mBody.setMaxLines(maxLines); - return false; - } else { - removePreDrawListener(); - return true; - } - } - }; - - public ViewHolder(final View view) { - super(view); - mTitle = (TextView) view.findViewById(R.id.dvr_details_description_title); - mSubtitle = (TextView) view.findViewById(R.id.dvr_details_description_subtitle); - mBody = (TextView) view.findViewById(R.id.dvr_details_description_body); - mDescriptionContainer = - (LinearLayout) view.findViewById(R.id.dvr_details_description_container); - mReadMoreView = (TextView) view.findViewById(R.id.dvr_details_description_read_more); - - FontMetricsInt titleFontMetricsInt = getFontMetricsInt(mTitle); - final int titleAscent = view.getResources().getDimensionPixelSize( - R.dimen.lb_details_description_title_baseline); - // Ascent is negative - mTitleMargin = titleAscent + titleFontMetricsInt.ascent; - - mUnderTitleBaselineMargin = view.getResources().getDimensionPixelSize( - R.dimen.lb_details_description_under_title_baseline_margin); - mUnderSubtitleBaselineMargin = view.getResources().getDimensionPixelSize( - R.dimen.lb_details_description_under_subtitle_baseline_margin); - - mTitleLineSpacing = view.getResources().getDimensionPixelSize( - R.dimen.lb_details_description_title_line_spacing); - mBodyLineSpacing = view.getResources().getDimensionPixelSize( - R.dimen.lb_details_description_body_line_spacing); - - mBodyMaxLines = view.getResources().getInteger( - R.integer.lb_details_description_body_max_lines); - mBodyMinLines = view.getResources().getInteger( - R.integer.lb_details_description_body_min_lines); - mTitleMaxLines = mTitle.getMaxLines(); - - mTitleFontMetricsInt = getFontMetricsInt(mTitle); - mSubtitleFontMetricsInt = getFontMetricsInt(mSubtitle); - mBodyFontMetricsInt = getFontMetricsInt(mBody); - } - - void addPreDrawListener() { - if (!mIsListeningToPreDraw) { - mIsListeningToPreDraw = true; - view.getViewTreeObserver().addOnPreDrawListener(mPreDrawListener); - } - } - - void removePreDrawListener() { - if (mIsListeningToPreDraw) { - view.getViewTreeObserver().removeOnPreDrawListener(mPreDrawListener); - mIsListeningToPreDraw = false; - } - } - - public TextView getTitle() { - return mTitle; - } - - public TextView getSubtitle() { - return mSubtitle; - } - - public TextView getBody() { - return mBody; - } - - private FontMetricsInt getFontMetricsInt(TextView textView) { - Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); - paint.setTextSize(textView.getTextSize()); - paint.setTypeface(textView.getTypeface()); - return paint.getFontMetricsInt(); - } - - private void showFullText(int heightDiff) { - final ViewGroup detailsFrame = (ViewGroup) mActivity.findViewById(R.id.details_frame); - int nowHeight = ViewUtils.getLayoutHeight(detailsFrame); - Animator expandAnimator = ViewUtils.createHeightAnimator( - detailsFrame, nowHeight, nowHeight + heightDiff); - expandAnimator.setDuration(mFullTextAnimationDuration); - Animator shiftAnimator = ObjectAnimator.ofPropertyValuesHolder(detailsFrame, - PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, - 0f, -(heightDiff / 2))); - shiftAnimator.setDuration(mFullTextAnimationDuration); - AnimatorSet fullTextAnimator = new AnimatorSet(); - fullTextAnimator.playTogether(expandAnimator, shiftAnimator); - fullTextAnimator.start(); - } - } - - private final Activity mActivity; - private final int mFullTextAnimationDuration; - - public DetailsContentPresenter(Activity activity) { - super(); - mActivity = activity; - mFullTextAnimationDuration = mActivity.getResources() - .getInteger(R.integer.dvr_details_full_text_animation_duration); - } - - @Override - public final ViewHolder onCreateViewHolder(ViewGroup parent) { - View v = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.dvr_details_description, parent, false); - return new ViewHolder(v); - } - - @Override - public final void onBindViewHolder(Presenter.ViewHolder viewHolder, Object item) { - final ViewHolder vh = (ViewHolder) viewHolder; - final DetailsContent detailsContent = (DetailsContent) item; - - vh.mActivity = mActivity; - vh.mFullTextAnimationDuration = mFullTextAnimationDuration; - - boolean hasTitle = true; - if (TextUtils.isEmpty(detailsContent.getTitle())) { - vh.mTitle.setVisibility(View.GONE); - hasTitle = false; - } else { - vh.mTitle.setText(detailsContent.getTitle()); - vh.mTitle.setVisibility(View.VISIBLE); - vh.mTitle.setLineSpacing(vh.mTitleLineSpacing - vh.mTitle.getLineHeight() - + vh.mTitle.getLineSpacingExtra(), vh.mTitle.getLineSpacingMultiplier()); - vh.mTitle.setMaxLines(vh.mTitleMaxLines); - } - setTopMargin(vh.mTitle, vh.mTitleMargin); - - boolean hasSubtitle = true; - if (detailsContent.getStartTimeUtcMillis() != DetailsContent.INVALID_TIME - && detailsContent.getEndTimeUtcMillis() != DetailsContent.INVALID_TIME) { - vh.mSubtitle.setText(Utils.getDurationString(viewHolder.view.getContext(), - detailsContent.getStartTimeUtcMillis(), - detailsContent.getEndTimeUtcMillis(), false)); - vh.mSubtitle.setVisibility(View.VISIBLE); - if (hasTitle) { - setTopMargin(vh.mSubtitle, vh.mUnderTitleBaselineMargin - + vh.mSubtitleFontMetricsInt.ascent - vh.mTitleFontMetricsInt.descent); - } else { - setTopMargin(vh.mSubtitle, 0); - } - } else { - vh.mSubtitle.setVisibility(View.GONE); - hasSubtitle = false; - } - - if (TextUtils.isEmpty(detailsContent.getDescription())) { - vh.mBody.setVisibility(View.GONE); - } else { - vh.mBody.setText(detailsContent.getDescription()); - vh.mBody.setVisibility(View.VISIBLE); - vh.mBody.setLineSpacing(vh.mBodyLineSpacing - vh.mBody.getLineHeight() - + vh.mBody.getLineSpacingExtra(), vh.mBody.getLineSpacingMultiplier()); - if (hasSubtitle) { - setTopMargin(vh.mDescriptionContainer, vh.mUnderSubtitleBaselineMargin - + vh.mBodyFontMetricsInt.ascent - vh.mSubtitleFontMetricsInt.descent - - vh.mBody.getPaddingTop()); - } else if (hasTitle) { - setTopMargin(vh.mDescriptionContainer, vh.mUnderTitleBaselineMargin - + vh.mBodyFontMetricsInt.ascent - vh.mTitleFontMetricsInt.descent - - vh.mBody.getPaddingTop()); - } else { - setTopMargin(vh.mDescriptionContainer, 0); - } - } - } - - @Override - public void onUnbindViewHolder(Presenter.ViewHolder viewHolder) { } - - @Override - public void onViewAttachedToWindow(Presenter.ViewHolder holder) { - // In case predraw listener was removed in detach, make sure - // we have the proper layout. - ViewHolder vh = (ViewHolder) holder; - vh.addPreDrawListener(); - super.onViewAttachedToWindow(holder); - } - - @Override - public void onViewDetachedFromWindow(Presenter.ViewHolder holder) { - ViewHolder vh = (ViewHolder) holder; - vh.removePreDrawListener(); - super.onViewDetachedFromWindow(holder); - } - - private void setTopMargin(View view, int topMargin) { - ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) view.getLayoutParams(); - lp.topMargin = topMargin; - view.setLayoutParams(lp); - } -} \ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/DetailsViewBackgroundHelper.java b/src/com/android/tv/dvr/ui/DetailsViewBackgroundHelper.java deleted file mode 100644 index 6714ecd3..00000000 --- a/src/com/android/tv/dvr/ui/DetailsViewBackgroundHelper.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.tv.dvr.ui; - -import android.app.Activity; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.ColorDrawable; -import android.graphics.drawable.Drawable; -import android.os.Handler; -import android.support.v17.leanback.app.BackgroundManager; - -/** - * The Background Helper. - */ -public class DetailsViewBackgroundHelper { - // Background delay serves to avoid kicking off expensive bitmap loading - // in case multiple backgrounds are set in quick succession. - private static final int SET_BACKGROUND_DELAY_MS = 100; - - private final BackgroundManager mBackgroundManager; - - class LoadBackgroundRunnable implements Runnable { - final Drawable mBackGround; - - LoadBackgroundRunnable(Drawable background) { - mBackGround = background; - } - - @Override - public void run() { - if (!mBackgroundManager.isAttached()) { - return; - } - if (mBackGround instanceof BitmapDrawable) { - mBackgroundManager.setBitmap(((BitmapDrawable) mBackGround).getBitmap()); - } - mRunnable = null; - } - } - - private LoadBackgroundRunnable mRunnable; - - private final Handler mHandler = new Handler(); - - public DetailsViewBackgroundHelper(Activity activity) { - mBackgroundManager = BackgroundManager.getInstance(activity); - mBackgroundManager.attach(activity.getWindow()); - } - - /** - * Sets the given image to background. - */ - public void setBackground(Drawable background) { - if (mRunnable != null) { - mHandler.removeCallbacks(mRunnable); - } - mRunnable = new LoadBackgroundRunnable(background); - mHandler.postDelayed(mRunnable, SET_BACKGROUND_DELAY_MS); - } - - /** - * Sets the background color. - */ - public void setBackgroundColor(int color) { - if (mBackgroundManager.isAttached()) { - mBackgroundManager.setColor(color); - } - } - - /** - * Sets the background scrim. - */ - public void setScrim(int color) { - if (mBackgroundManager.isAttached()) { - mBackgroundManager.setDimLayer(new ColorDrawable(color)); - } - } -} diff --git a/src/com/android/tv/dvr/ui/DvrActivity.java b/src/com/android/tv/dvr/ui/DvrActivity.java deleted file mode 100644 index 45fb1cf1..00000000 --- a/src/com/android/tv/dvr/ui/DvrActivity.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.tv.dvr.ui; - -import android.app.Activity; -import android.os.Bundle; - -import com.android.tv.R; -import com.android.tv.TvApplication; - -/** - * {@link android.app.Activity} for DVR UI. - */ -public class DvrActivity extends Activity { - @Override - public void onCreate(Bundle savedInstanceState) { - TvApplication.setCurrentRunningProcess(this, true); - super.onCreate(savedInstanceState); - setContentView(R.layout.dvr_main); - } -} diff --git a/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java b/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java index 9df228d1..936e9c31 100644 --- a/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java +++ b/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java @@ -28,11 +28,9 @@ import android.widget.Toast; import com.android.tv.R; import com.android.tv.TvApplication; -import com.android.tv.dvr.RecordedProgram; import com.android.tv.data.Program; import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.DvrUiHelper; -import com.android.tv.util.Utils; +import com.android.tv.dvr.data.RecordedProgram; import java.util.List; diff --git a/src/com/android/tv/dvr/ui/DvrAlreadyScheduledFragment.java b/src/com/android/tv/dvr/ui/DvrAlreadyScheduledFragment.java index 78f21784..3c73cb47 100644 --- a/src/com/android/tv/dvr/ui/DvrAlreadyScheduledFragment.java +++ b/src/com/android/tv/dvr/ui/DvrAlreadyScheduledFragment.java @@ -25,15 +25,12 @@ import android.support.annotation.NonNull; import android.support.v17.leanback.widget.GuidanceStylist.Guidance; import android.support.v17.leanback.widget.GuidedAction; import android.text.format.DateUtils; -import android.widget.Toast; import com.android.tv.R; import com.android.tv.TvApplication; import com.android.tv.data.Program; import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.DvrUiHelper; -import com.android.tv.dvr.ScheduledRecording; -import com.android.tv.util.Utils; +import com.android.tv.dvr.data.ScheduledRecording; import java.util.List; diff --git a/src/com/android/tv/dvr/ui/DvrBrowseFragment.java b/src/com/android/tv/dvr/ui/DvrBrowseFragment.java deleted file mode 100644 index a6dd31d1..00000000 --- a/src/com/android/tv/dvr/ui/DvrBrowseFragment.java +++ /dev/null @@ -1,601 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.tv.dvr.ui; - -import android.content.Context; -import android.media.tv.TvInputManager.TvInputCallback; -import android.os.Bundle; -import android.os.Handler; -import android.support.v17.leanback.app.BrowseFragment; -import android.support.v17.leanback.widget.ArrayObjectAdapter; -import android.support.v17.leanback.widget.ClassPresenterSelector; -import android.support.v17.leanback.widget.HeaderItem; -import android.support.v17.leanback.widget.ListRow; -import android.support.v17.leanback.widget.ListRowPresenter; -import android.support.v17.leanback.widget.Presenter; -import android.support.v17.leanback.widget.TitleViewAdapter; -import android.text.TextUtils; -import android.util.Log; - -import com.android.tv.ApplicationSingletons; -import com.android.tv.R; -import com.android.tv.TvApplication; -import com.android.tv.data.GenreItems; -import com.android.tv.dvr.DvrDataManager; -import com.android.tv.dvr.DvrDataManager.OnDvrScheduleLoadFinishedListener; -import com.android.tv.dvr.DvrDataManager.OnRecordedProgramLoadFinishedListener; -import com.android.tv.dvr.DvrDataManager.RecordedProgramListener; -import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; -import com.android.tv.dvr.DvrDataManager.SeriesRecordingListener; -import com.android.tv.dvr.DvrScheduleManager; -import com.android.tv.dvr.RecordedProgram; -import com.android.tv.dvr.ScheduledRecording; -import com.android.tv.dvr.SeriesRecording; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Comparator; -import java.util.HashMap; -import java.util.List; - -/** - * {@link BrowseFragment} for DVR functions. - */ -public class DvrBrowseFragment extends BrowseFragment implements - RecordedProgramListener, ScheduledRecordingListener, SeriesRecordingListener, - OnDvrScheduleLoadFinishedListener, OnRecordedProgramLoadFinishedListener { - private static final String TAG = "DvrBrowseFragment"; - private static final boolean DEBUG = false; - - private static final int MAX_RECENT_ITEM_COUNT = 10; - private static final int MAX_SCHEDULED_ITEM_COUNT = 4; - - private RecordedProgramAdapter mRecentAdapter; - private ScheduleAdapter mScheduleAdapter; - private SeriesAdapter mSeriesAdapter; - private RecordedProgramAdapter[] mGenreAdapters = - new RecordedProgramAdapter[GenreItems.getGenreCount() + 1]; - private ListRow mRecentRow; - private ListRow mSeriesRow; - private ListRow[] mGenreRows = new ListRow[GenreItems.getGenreCount() + 1]; - private List mGenreLabels; - private DvrDataManager mDvrDataManager; - private DvrScheduleManager mDvrScheudleManager; - private ArrayObjectAdapter mRowsAdapter; - private ClassPresenterSelector mPresenterSelector; - private final HashMap mSeriesId2LatestProgram = new HashMap<>(); - private final Handler mHandler = new Handler(); - - private final Comparator RECORDED_PROGRAM_COMPARATOR = new Comparator() { - @Override - public int compare(Object lhs, Object rhs) { - if (lhs instanceof SeriesRecording) { - lhs = mSeriesId2LatestProgram.get(((SeriesRecording) lhs).getSeriesId()); - } - if (rhs instanceof SeriesRecording) { - rhs = mSeriesId2LatestProgram.get(((SeriesRecording) rhs).getSeriesId()); - } - if (lhs instanceof RecordedProgram) { - if (rhs instanceof RecordedProgram) { - return RecordedProgram.START_TIME_THEN_ID_COMPARATOR.reversed() - .compare((RecordedProgram) lhs, (RecordedProgram) rhs); - } else { - return -1; - } - } else if (rhs instanceof RecordedProgram) { - return 1; - } else { - return 0; - } - } - }; - - private final Comparator SCHEDULE_COMPARATOR = new Comparator() { - @Override - public int compare(Object lhs, Object rhs) { - if (lhs instanceof ScheduledRecording) { - if (rhs instanceof ScheduledRecording) { - return ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR - .compare((ScheduledRecording) lhs, (ScheduledRecording) rhs); - } else { - return -1; - } - } else if (rhs instanceof ScheduledRecording) { - return 1; - } else { - return 0; - } - } - }; - - private final DvrScheduleManager.OnConflictStateChangeListener mOnConflictStateChangeListener = - new DvrScheduleManager.OnConflictStateChangeListener() { - @Override - public void onConflictStateChange(boolean conflict, ScheduledRecording... schedules) { - if (mScheduleAdapter != null) { - for (ScheduledRecording schedule : schedules) { - onScheduledRecordingStatusChanged(schedule); - } - } - } - }; - - private final Runnable mUpdateRowsRunnable = new Runnable() { - @Override - public void run() { - updateRows(); - } - }; - - @Override - public void onCreate(Bundle savedInstanceState) { - if (DEBUG) Log.d(TAG, "onCreate"); - super.onCreate(savedInstanceState); - Context context = getContext(); - ApplicationSingletons singletons = TvApplication.getSingletons(context); - mDvrDataManager = singletons.getDvrDataManager(); - mDvrScheudleManager = singletons.getDvrScheduleManager(); - mPresenterSelector = new ClassPresenterSelector() - .addClassPresenter(ScheduledRecording.class, - new ScheduledRecordingPresenter(context)) - .addClassPresenter(RecordedProgram.class, new RecordedProgramPresenter(context)) - .addClassPresenter(SeriesRecording.class, new SeriesRecordingPresenter(context)) - .addClassPresenter(FullScheduleCardHolder.class, new FullSchedulesCardPresenter()); - mGenreLabels = new ArrayList<>(Arrays.asList(GenreItems.getLabels(context))); - mGenreLabels.add(getString(R.string.dvr_main_others)); - setupUiElements(); - setupAdapters(); - mDvrScheudleManager.addOnConflictStateChangeListener(mOnConflictStateChangeListener); - prepareEntranceTransition(); - if (mDvrDataManager.isInitialized()) { - startEntranceTransition(); - } else { - if (!mDvrDataManager.isDvrScheduleLoadFinished()) { - mDvrDataManager.addDvrScheduleLoadFinishedListener(this); - } - if (!mDvrDataManager.isRecordedProgramLoadFinished()) { - mDvrDataManager.addRecordedProgramLoadFinishedListener(this); - } - } - } - - @Override - public void onDestroy() { - if (DEBUG) Log.d(TAG, "onDestroy"); - mHandler.removeCallbacks(mUpdateRowsRunnable); - mDvrScheudleManager.removeOnConflictStateChangeListener(mOnConflictStateChangeListener); - mDvrDataManager.removeRecordedProgramListener(this); - mDvrDataManager.removeScheduledRecordingListener(this); - mDvrDataManager.removeSeriesRecordingListener(this); - mDvrDataManager.removeDvrScheduleLoadFinishedListener(this); - mDvrDataManager.removeRecordedProgramLoadFinishedListener(this); - mRowsAdapter.clear(); - mSeriesId2LatestProgram.clear(); - for (Presenter presenter : mPresenterSelector.getPresenters()) { - if (presenter instanceof DvrItemPresenter) { - ((DvrItemPresenter) presenter).unbindAllViewHolders(); - } - } - super.onDestroy(); - } - - @Override - public void onDvrScheduleLoadFinished() { - List scheduledRecordings = mDvrDataManager.getAllScheduledRecordings(); - onScheduledRecordingAdded(ScheduledRecording.toArray(scheduledRecordings)); - List seriesRecordings = mDvrDataManager.getSeriesRecordings(); - onSeriesRecordingAdded(SeriesRecording.toArray(seriesRecordings)); - if (mDvrDataManager.isInitialized()) { - startEntranceTransition(); - } - mDvrDataManager.removeDvrScheduleLoadFinishedListener(this); - } - - @Override - public void onRecordedProgramLoadFinished() { - for (RecordedProgram recordedProgram : mDvrDataManager.getRecordedPrograms()) { - handleRecordedProgramAdded(recordedProgram, true); - } - updateRows(); - if (mDvrDataManager.isInitialized()) { - startEntranceTransition(); - } - mDvrDataManager.removeRecordedProgramLoadFinishedListener(this); - } - - @Override - public void onRecordedProgramsAdded(RecordedProgram... recordedPrograms) { - for (RecordedProgram recordedProgram : recordedPrograms) { - handleRecordedProgramAdded(recordedProgram, true); - } - postUpdateRows(); - } - - @Override - public void onRecordedProgramsChanged(RecordedProgram... recordedPrograms) { - for (RecordedProgram recordedProgram : recordedPrograms) { - handleRecordedProgramChanged(recordedProgram); - } - postUpdateRows(); - } - - @Override - public void onRecordedProgramsRemoved(RecordedProgram... recordedPrograms) { - for (RecordedProgram recordedProgram : recordedPrograms) { - handleRecordedProgramRemoved(recordedProgram); - } - postUpdateRows(); - } - - // No need to call updateRows() during ScheduledRecordings' change because - // the row for ScheduledRecordings is always displayed. - @Override - public void onScheduledRecordingAdded(ScheduledRecording... scheduledRecordings) { - for (ScheduledRecording scheduleRecording : scheduledRecordings) { - if (needToShowScheduledRecording(scheduleRecording)) { - mScheduleAdapter.add(scheduleRecording); - } - } - } - - @Override - public void onScheduledRecordingRemoved(ScheduledRecording... scheduledRecordings) { - for (ScheduledRecording scheduleRecording : scheduledRecordings) { - mScheduleAdapter.remove(scheduleRecording); - } - } - - @Override - public void onScheduledRecordingStatusChanged(ScheduledRecording... scheduledRecordings) { - for (ScheduledRecording scheduleRecording : scheduledRecordings) { - if (needToShowScheduledRecording(scheduleRecording)) { - mScheduleAdapter.change(scheduleRecording); - } else { - mScheduleAdapter.removeWithId(scheduleRecording); - } - } - } - - @Override - public void onSeriesRecordingAdded(SeriesRecording... seriesRecordings) { - handleSeriesRecordingsAdded(Arrays.asList(seriesRecordings)); - postUpdateRows(); - } - - @Override - public void onSeriesRecordingRemoved(SeriesRecording... seriesRecordings) { - handleSeriesRecordingsRemoved(Arrays.asList(seriesRecordings)); - postUpdateRows(); - } - - @Override - public void onSeriesRecordingChanged(SeriesRecording... seriesRecordings) { - handleSeriesRecordingsChanged(Arrays.asList(seriesRecordings)); - postUpdateRows(); - } - - // Workaround of b/29108300 - @Override - public void showTitle(int flags) { - flags &= ~TitleViewAdapter.SEARCH_VIEW_VISIBLE; - super.showTitle(flags); - } - - private void setupUiElements() { - setBadgeDrawable(getActivity().getDrawable(R.drawable.ic_dvr_badge)); - setHeadersState(HEADERS_ENABLED); - setHeadersTransitionOnBackEnabled(false); - setBrandColor(getResources().getColor(R.color.program_guide_side_panel_background, null)); - } - - private void setupAdapters() { - mRecentAdapter = new RecordedProgramAdapter(MAX_RECENT_ITEM_COUNT); - mScheduleAdapter = new ScheduleAdapter(MAX_SCHEDULED_ITEM_COUNT); - mSeriesAdapter = new SeriesAdapter(); - for (int i = 0; i < mGenreAdapters.length; i++) { - mGenreAdapters[i] = new RecordedProgramAdapter(); - } - // Schedule Recordings. - List schedules = mDvrDataManager.getAllScheduledRecordings(); - onScheduledRecordingAdded(ScheduledRecording.toArray(schedules)); - mScheduleAdapter.addExtraItem(FullScheduleCardHolder.FULL_SCHEDULE_CARD_HOLDER); - // Recorded Programs. - for (RecordedProgram recordedProgram : mDvrDataManager.getRecordedPrograms()) { - handleRecordedProgramAdded(recordedProgram, false); - } - // Series Recordings. Series recordings should be added after recorded programs, because - // we build series recordings' latest program information while adding recorded programs. - List recordings = mDvrDataManager.getSeriesRecordings(); - handleSeriesRecordingsAdded(recordings); - mRowsAdapter = new ArrayObjectAdapter(new ListRowPresenter()); - mRecentRow = new ListRow(new HeaderItem( - getString(R.string.dvr_main_recent)), mRecentAdapter); - mRowsAdapter.add(new ListRow(new HeaderItem( - getString(R.string.dvr_main_scheduled)), mScheduleAdapter)); - mSeriesRow = new ListRow(new HeaderItem( - getString(R.string.dvr_main_series)), mSeriesAdapter); - updateRows(); - mDvrDataManager.addRecordedProgramListener(this); - mDvrDataManager.addScheduledRecordingListener(this); - mDvrDataManager.addSeriesRecordingListener(this); - setAdapter(mRowsAdapter); - } - - private void handleRecordedProgramAdded(RecordedProgram recordedProgram, - boolean updateSeriesRecording) { - mRecentAdapter.add(recordedProgram); - String seriesId = recordedProgram.getSeriesId(); - SeriesRecording seriesRecording = null; - if (seriesId != null) { - seriesRecording = mDvrDataManager.getSeriesRecording(seriesId); - RecordedProgram latestProgram = mSeriesId2LatestProgram.get(seriesId); - if (latestProgram == null || RecordedProgram.START_TIME_THEN_ID_COMPARATOR - .compare(latestProgram, recordedProgram) < 0) { - mSeriesId2LatestProgram.put(seriesId, recordedProgram); - if (updateSeriesRecording && seriesRecording != null) { - onSeriesRecordingChanged(seriesRecording); - } - } - } - if (seriesRecording == null) { - for (RecordedProgramAdapter adapter - : getGenreAdapters(recordedProgram.getCanonicalGenres())) { - adapter.add(recordedProgram); - } - } - } - - private void handleRecordedProgramRemoved(RecordedProgram recordedProgram) { - mRecentAdapter.remove(recordedProgram); - String seriesId = recordedProgram.getSeriesId(); - if (seriesId != null) { - SeriesRecording seriesRecording = mDvrDataManager.getSeriesRecording(seriesId); - RecordedProgram latestProgram = - mSeriesId2LatestProgram.get(recordedProgram.getSeriesId()); - if (latestProgram != null && latestProgram.getId() == recordedProgram.getId()) { - if (seriesRecording != null) { - updateLatestRecordedProgram(seriesRecording); - onSeriesRecordingChanged(seriesRecording); - } - } - } - for (RecordedProgramAdapter adapter - : getGenreAdapters(recordedProgram.getCanonicalGenres())) { - adapter.remove(recordedProgram); - } - } - - private void handleRecordedProgramChanged(RecordedProgram recordedProgram) { - mRecentAdapter.change(recordedProgram); - String seriesId = recordedProgram.getSeriesId(); - SeriesRecording seriesRecording = null; - if (seriesId != null) { - seriesRecording = mDvrDataManager.getSeriesRecording(seriesId); - RecordedProgram latestProgram = mSeriesId2LatestProgram.get(seriesId); - if (latestProgram == null || RecordedProgram.START_TIME_THEN_ID_COMPARATOR - .compare(latestProgram, recordedProgram) <= 0) { - mSeriesId2LatestProgram.put(seriesId, recordedProgram); - if (seriesRecording != null) { - onSeriesRecordingChanged(seriesRecording); - } - } else if (latestProgram.getId() == recordedProgram.getId()) { - if (seriesRecording != null) { - updateLatestRecordedProgram(seriesRecording); - onSeriesRecordingChanged(seriesRecording); - } - } - } - if (seriesRecording == null) { - updateGenreAdapters(getGenreAdapters( - recordedProgram.getCanonicalGenres()), recordedProgram); - } else { - updateGenreAdapters(new ArrayList<>(), recordedProgram); - } - } - - private void handleSeriesRecordingsAdded(List seriesRecordings) { - for (SeriesRecording seriesRecording : seriesRecordings) { - mSeriesAdapter.add(seriesRecording); - if (mSeriesId2LatestProgram.get(seriesRecording.getSeriesId()) != null) { - for (RecordedProgramAdapter adapter - : getGenreAdapters(seriesRecording.getCanonicalGenreIds())) { - adapter.add(seriesRecording); - } - } - } - } - - private void handleSeriesRecordingsRemoved(List seriesRecordings) { - for (SeriesRecording seriesRecording : seriesRecordings) { - mSeriesAdapter.remove(seriesRecording); - for (RecordedProgramAdapter adapter - : getGenreAdapters(seriesRecording.getCanonicalGenreIds())) { - adapter.remove(seriesRecording); - } - } - } - - private void handleSeriesRecordingsChanged(List seriesRecordings) { - for (SeriesRecording seriesRecording : seriesRecordings) { - mSeriesAdapter.change(seriesRecording); - if (mSeriesId2LatestProgram.get(seriesRecording.getSeriesId()) != null) { - updateGenreAdapters(getGenreAdapters( - seriesRecording.getCanonicalGenreIds()), seriesRecording); - } else { - // Remove series recording from all genre rows if it has no recorded program - updateGenreAdapters(new ArrayList<>(), seriesRecording); - } - } - } - - private List getGenreAdapters(String[] genres) { - List result = new ArrayList<>(); - if (genres == null || genres.length == 0) { - result.add(mGenreAdapters[mGenreAdapters.length - 1]); - } else { - for (String genre : genres) { - int genreId = GenreItems.getId(genre); - if(genreId >= mGenreAdapters.length) { - Log.d(TAG, "Wrong Genre ID: " + genreId); - } else { - result.add(mGenreAdapters[genreId]); - } - } - } - return result; - } - - private List getGenreAdapters(int[] genreIds) { - List result = new ArrayList<>(); - if (genreIds == null || genreIds.length == 0) { - result.add(mGenreAdapters[mGenreAdapters.length - 1]); - } else { - for (int genreId : genreIds) { - if(genreId >= mGenreAdapters.length) { - Log.d(TAG, "Wrong Genre ID: " + genreId); - } else { - result.add(mGenreAdapters[genreId]); - } - } - } - return result; - } - - private void updateGenreAdapters(List adapters, Object r) { - for (RecordedProgramAdapter adapter : mGenreAdapters) { - if (adapters.contains(adapter)) { - adapter.change(r); - } else { - adapter.remove(r); - } - } - } - - private void postUpdateRows() { - mHandler.removeCallbacks(mUpdateRowsRunnable); - mHandler.post(mUpdateRowsRunnable); - } - - private void updateRows() { - int visibleRowsCount = 1; // Schedule's Row will never be empty - if (mRecentAdapter.isEmpty()) { - mRowsAdapter.remove(mRecentRow); - } else { - if (mRowsAdapter.indexOf(mRecentRow) < 0) { - mRowsAdapter.add(0, mRecentRow); - } - visibleRowsCount++; - } - if (mSeriesAdapter.isEmpty()) { - mRowsAdapter.remove(mSeriesRow); - } else { - if (mRowsAdapter.indexOf(mSeriesRow) < 0) { - mRowsAdapter.add(visibleRowsCount, mSeriesRow); - } - visibleRowsCount++; - } - for (int i = 0; i < mGenreAdapters.length; i++) { - RecordedProgramAdapter adapter = mGenreAdapters[i]; - if (adapter != null) { - if (adapter.isEmpty()) { - mRowsAdapter.remove(mGenreRows[i]); - } else { - if (mGenreRows[i] == null || mRowsAdapter.indexOf(mGenreRows[i]) < 0) { - mGenreRows[i] = new ListRow(new HeaderItem(mGenreLabels.get(i)), adapter); - mRowsAdapter.add(visibleRowsCount, mGenreRows[i]); - } - visibleRowsCount++; - } - } - } - } - - private boolean needToShowScheduledRecording(ScheduledRecording recording) { - int state = recording.getState(); - return state == ScheduledRecording.STATE_RECORDING_IN_PROGRESS - || state == ScheduledRecording.STATE_RECORDING_NOT_STARTED; - } - - private void updateLatestRecordedProgram(SeriesRecording seriesRecording) { - RecordedProgram latestProgram = null; - for (RecordedProgram program : - mDvrDataManager.getRecordedPrograms(seriesRecording.getId())) { - if (latestProgram == null || RecordedProgram - .START_TIME_THEN_ID_COMPARATOR.compare(latestProgram, program) < 0) { - latestProgram = program; - } - } - mSeriesId2LatestProgram.put(seriesRecording.getSeriesId(), latestProgram); - } - - private class ScheduleAdapter extends SortedArrayAdapter { - ScheduleAdapter(int maxItemCount) { - super(mPresenterSelector, SCHEDULE_COMPARATOR, maxItemCount); - } - - @Override - public long getId(Object item) { - if (item instanceof ScheduledRecording) { - return ((ScheduledRecording) item).getId(); - } else { - return -1; - } - } - } - - private class SeriesAdapter extends SortedArrayAdapter { - SeriesAdapter() { - super(mPresenterSelector, new Comparator() { - @Override - public int compare(SeriesRecording lhs, SeriesRecording rhs) { - if (lhs.isStopped() && !rhs.isStopped()) { - return 1; - } else if (!lhs.isStopped() && rhs.isStopped()) { - return -1; - } - return SeriesRecording.PRIORITY_COMPARATOR.compare(lhs, rhs); - } - }); - } - - @Override - public long getId(SeriesRecording item) { - return item.getId(); - } - } - - private class RecordedProgramAdapter extends SortedArrayAdapter { - RecordedProgramAdapter() { - this(Integer.MAX_VALUE); - } - - RecordedProgramAdapter(int maxItemCount) { - super(mPresenterSelector, RECORDED_PROGRAM_COMPARATOR, maxItemCount); - } - - @Override - public long getId(Object item) { - if (item instanceof SeriesRecording) { - return ((SeriesRecording) item).getId(); - } else if (item instanceof RecordedProgram) { - return ((RecordedProgram) item).getId(); - } else { - return -1; - } - } - } -} \ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/DvrChannelRecordDurationOptionFragment.java b/src/com/android/tv/dvr/ui/DvrChannelRecordDurationOptionFragment.java index 837d8ab2..880dc8ac 100644 --- a/src/com/android/tv/dvr/ui/DvrChannelRecordDurationOptionFragment.java +++ b/src/com/android/tv/dvr/ui/DvrChannelRecordDurationOptionFragment.java @@ -27,7 +27,7 @@ import com.android.tv.TvApplication; import com.android.tv.common.SoftPreconditions; import com.android.tv.data.Channel; import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.ScheduledRecording; +import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.dvr.ui.DvrConflictFragment.DvrChannelRecordConflictFragment; import java.util.ArrayList; diff --git a/src/com/android/tv/dvr/ui/DvrConflictFragment.java b/src/com/android/tv/dvr/ui/DvrConflictFragment.java index e7be4d0a..5985f56f 100644 --- a/src/com/android/tv/dvr/ui/DvrConflictFragment.java +++ b/src/com/android/tv/dvr/ui/DvrConflictFragment.java @@ -34,10 +34,9 @@ import com.android.tv.TvApplication; import com.android.tv.common.SoftPreconditions; import com.android.tv.data.Channel; import com.android.tv.data.Program; -import com.android.tv.dvr.ConflictChecker; -import com.android.tv.dvr.ConflictChecker.OnUpcomingConflictChangeListener; -import com.android.tv.dvr.DvrUiHelper; -import com.android.tv.dvr.ScheduledRecording; +import com.android.tv.dvr.recorder.ConflictChecker; +import com.android.tv.dvr.recorder.ConflictChecker.OnUpcomingConflictChangeListener; +import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.util.Utils; import java.util.ArrayList; diff --git a/src/com/android/tv/dvr/ui/DvrDetailsActivity.java b/src/com/android/tv/dvr/ui/DvrDetailsActivity.java deleted file mode 100644 index 806c775c..00000000 --- a/src/com/android/tv/dvr/ui/DvrDetailsActivity.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.tv.dvr.ui; - -import android.app.Activity; -import android.os.Bundle; -import android.support.v17.leanback.app.DetailsFragment; - -import com.android.tv.R; -import com.android.tv.TvApplication; - -/** - * Activity to show details view in DVR. - */ -public class DvrDetailsActivity extends Activity { - /** - * Name of record id added to the Intent. - */ - public static final String RECORDING_ID = "record_id"; - - /** - * Name of flag added to the Intent to determine if details view should hide "View schedule" - * button. - */ - public static final String HIDE_VIEW_SCHEDULE = "hide_view_schedule"; - - /** - * Name of details view's type added to the intent. - */ - public static final String DETAILS_VIEW_TYPE = "details_view_type"; - - /** - * Name of shared element between activities. - */ - public static final String SHARED_ELEMENT_NAME = "shared_element"; - - /** - * CURRENT_RECORDING_VIEW refers to Current Recordings in DVR. - */ - public static final int CURRENT_RECORDING_VIEW = 1; - - /** - * SCHEDULED_RECORDING_VIEW refers to Scheduled Recordings in DVR. - */ - public static final int SCHEDULED_RECORDING_VIEW = 2; - - /** - * RECORDED_PROGRAM_VIEW refers to Recorded programs in DVR. - */ - public static final int RECORDED_PROGRAM_VIEW = 3; - - /** - * SERIES_RECORDING_VIEW refers to series recording in DVR. - */ - public static final int SERIES_RECORDING_VIEW = 4; - - @Override - public void onCreate(Bundle savedInstanceState) { - TvApplication.setCurrentRunningProcess(this, true); - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_dvr_details); - long recordId = getIntent().getLongExtra(RECORDING_ID, -1); - int detailsViewType = getIntent().getIntExtra(DETAILS_VIEW_TYPE, -1); - boolean hideViewSchedule = getIntent().getBooleanExtra(HIDE_VIEW_SCHEDULE, false); - if (recordId != -1 && detailsViewType != -1 && savedInstanceState == null) { - Bundle args = new Bundle(); - args.putLong(RECORDING_ID, recordId); - DetailsFragment detailsFragment = null; - if (detailsViewType == CURRENT_RECORDING_VIEW) { - detailsFragment = new CurrentRecordingDetailsFragment(); - } else if (detailsViewType == SCHEDULED_RECORDING_VIEW) { - args.putBoolean(HIDE_VIEW_SCHEDULE, hideViewSchedule); - detailsFragment = new ScheduledRecordingDetailsFragment(); - } else if (detailsViewType == RECORDED_PROGRAM_VIEW) { - detailsFragment = new RecordedProgramDetailsFragment(); - } else if (detailsViewType == SERIES_RECORDING_VIEW) { - detailsFragment = new SeriesRecordingDetailsFragment(); - } - detailsFragment.setArguments(args); - getFragmentManager().beginTransaction() - .replace(R.id.dvr_details_view_frame, detailsFragment).commit(); - } - } -} diff --git a/src/com/android/tv/dvr/ui/DvrDetailsFragment.java b/src/com/android/tv/dvr/ui/DvrDetailsFragment.java deleted file mode 100644 index 21f9c4b4..00000000 --- a/src/com/android/tv/dvr/ui/DvrDetailsFragment.java +++ /dev/null @@ -1,344 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.tv.dvr.ui; - -import android.content.Context; -import android.content.Intent; -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.media.tv.TvContentRating; -import android.media.tv.TvInputManager; -import android.net.Uri; -import android.os.Bundle; -import android.support.annotation.Nullable; -import android.support.v17.leanback.app.DetailsFragment; -import android.support.v17.leanback.widget.ArrayObjectAdapter; -import android.support.v17.leanback.widget.ClassPresenterSelector; -import android.support.v17.leanback.widget.DetailsOverviewRow; -import android.support.v17.leanback.widget.DetailsOverviewRowPresenter; -import android.support.v17.leanback.widget.OnActionClickedListener; -import android.support.v17.leanback.widget.PresenterSelector; -import android.support.v17.leanback.widget.SparseArrayObjectAdapter; -import android.support.v17.leanback.widget.VerticalGridView; -import android.text.Spannable; -import android.text.SpannableString; -import android.text.TextUtils; -import android.text.style.TextAppearanceSpan; -import android.widget.Toast; - -import com.android.tv.R; -import com.android.tv.TvApplication; -import com.android.tv.data.BaseProgram; -import com.android.tv.data.Channel; -import com.android.tv.data.ChannelDataManager; -import com.android.tv.dialog.PinDialogFragment; -import com.android.tv.dvr.DvrPlaybackActivity; -import com.android.tv.dvr.RecordedProgram; -import com.android.tv.parental.ParentalControlSettings; -import com.android.tv.util.ImageLoader; -import com.android.tv.util.ToastUtils; -import com.android.tv.util.Utils; - -import java.io.File; - -abstract class DvrDetailsFragment extends DetailsFragment { - private static final int LOAD_LOGO_IMAGE = 1; - private static final int LOAD_BACKGROUND_IMAGE = 2; - - protected DetailsViewBackgroundHelper mBackgroundHelper; - private ArrayObjectAdapter mRowsAdapter; - private DetailsOverviewRow mDetailsOverview; - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - if (!onLoadRecordingDetails(getArguments())) { - getActivity().finish(); - return; - } - mBackgroundHelper = new DetailsViewBackgroundHelper(getActivity()); - setupAdapter(); - onCreateInternal(); - } - - @Override - public void onStart() { - super.onStart(); - // TODO: remove the workaround of b/30401180. - VerticalGridView container = (VerticalGridView) getActivity() - .findViewById(R.id.container_list); - // Need to manually modify offset. Please refer DetailsFragment.setVerticalGridViewLayout. - container.setItemAlignmentOffset(0); - container.setWindowAlignmentOffset( - getResources().getDimensionPixelSize(R.dimen.lb_details_rows_align_top)); - } - - private void setupAdapter() { - DetailsOverviewRowPresenter rowPresenter = new DetailsOverviewRowPresenter( - new DetailsContentPresenter(getActivity())); - rowPresenter.setBackgroundColor(getResources().getColor(R.color.common_tv_background, - null)); - rowPresenter.setSharedElementEnterTransition(getActivity(), - DvrDetailsActivity.SHARED_ELEMENT_NAME); - rowPresenter.setOnActionClickedListener(onCreateOnActionClickedListener()); - mRowsAdapter = new ArrayObjectAdapter(onCreatePresenterSelector(rowPresenter)); - setAdapter(mRowsAdapter); - } - - /** - * Returns details views' rows adapter. - */ - protected ArrayObjectAdapter getRowsAdapter() { - return mRowsAdapter; - } - - /** - * Sets details overview. - */ - protected void setDetailsOverviewRow(DetailsContent detailsContent) { - mDetailsOverview = new DetailsOverviewRow(detailsContent); - mDetailsOverview.setActionsAdapter(onCreateActionsAdapter()); - mRowsAdapter.add(mDetailsOverview); - onLoadLogoAndBackgroundImages(detailsContent); - } - - /** - * Creates and returns presenter selector will be used by rows adaptor. - */ - protected PresenterSelector onCreatePresenterSelector( - DetailsOverviewRowPresenter rowPresenter) { - ClassPresenterSelector presenterSelector = new ClassPresenterSelector(); - presenterSelector.addClassPresenter(DetailsOverviewRow.class, rowPresenter); - return presenterSelector; - } - - /** - * Does customized initialization of subclasses. Since {@link #onCreate(Bundle)} might finish - * activity early when it cannot fetch valid recordings, subclasses' onCreate method should not - * do anything after calling {@link #onCreate(Bundle)}. If there's something subclasses have to - * do after the super class did onCreate, it should override this method and put the codes here. - */ - protected void onCreateInternal() { } - - /** - * Updates actions of details overview. - */ - protected void updateActions() { - mDetailsOverview.setActionsAdapter(onCreateActionsAdapter()); - } - - /** - * Loads recording details according to the arguments the fragment got. - * - * @return false if cannot find valid recordings, else return true. If the return value - * is false, the detail activity and fragment will be ended. - */ - abstract boolean onLoadRecordingDetails(Bundle args); - - /** - * Creates actions users can interact with and their adaptor for this fragment. - */ - abstract SparseArrayObjectAdapter onCreateActionsAdapter(); - - /** - * Creates actions listeners to implement the behavior of the fragment after users click some - * action buttons. - */ - abstract OnActionClickedListener onCreateOnActionClickedListener(); - - /** - * Returns program title with episode number. If the program is null, returns channel name. - */ - protected CharSequence getTitleFromProgram(BaseProgram program, Channel channel) { - String titleWithEpisodeNumber = program.getTitleWithEpisodeNumber(getContext()); - SpannableString title = titleWithEpisodeNumber == null ? null - : new SpannableString(titleWithEpisodeNumber); - if (TextUtils.isEmpty(title)) { - title = new SpannableString(channel != null ? channel.getDisplayName() - : getContext().getResources().getString( - R.string.no_program_information)); - } else { - String programTitle = program.getTitle(); - title.setSpan(new TextAppearanceSpan(getContext(), - R.style.text_appearance_card_view_episode_number), programTitle == null ? 0 - : programTitle.length(), title.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - } - return title; - } - - /** - * Loads logo and background images for detail fragments. - */ - protected void onLoadLogoAndBackgroundImages(DetailsContent detailsContent) { - Drawable logoDrawable = null; - Drawable backgroundDrawable = null; - if (TextUtils.isEmpty(detailsContent.getLogoImageUri())) { - logoDrawable = getContext().getResources() - .getDrawable(R.drawable.dvr_default_poster, null); - mDetailsOverview.setImageDrawable(logoDrawable); - } - if (TextUtils.isEmpty(detailsContent.getBackgroundImageUri())) { - backgroundDrawable = getContext().getResources() - .getDrawable(R.drawable.dvr_default_poster, null); - mBackgroundHelper.setBackground(backgroundDrawable); - } - if (logoDrawable != null && backgroundDrawable != null) { - return; - } - if (logoDrawable == null && backgroundDrawable == null - && detailsContent.getLogoImageUri().equals( - detailsContent.getBackgroundImageUri())) { - ImageLoader.loadBitmap(getContext(), detailsContent.getLogoImageUri(), - new MyImageLoaderCallback(this, LOAD_LOGO_IMAGE | LOAD_BACKGROUND_IMAGE, - getContext())); - return; - } - if (logoDrawable == null) { - int imageWidth = getResources().getDimensionPixelSize(R.dimen.dvr_details_poster_width); - int imageHeight = getResources() - .getDimensionPixelSize(R.dimen.dvr_details_poster_height); - ImageLoader.loadBitmap(getContext(), detailsContent.getLogoImageUri(), - imageWidth, imageHeight, - new MyImageLoaderCallback(this, LOAD_LOGO_IMAGE, getContext())); - } - if (backgroundDrawable == null) { - ImageLoader.loadBitmap(getContext(), detailsContent.getBackgroundImageUri(), - new MyImageLoaderCallback(this, LOAD_BACKGROUND_IMAGE, getContext())); - } - } - - protected void startPlayback(RecordedProgram recordedProgram, long seekTimeMs) { - if (Utils.isInBundledPackageSet(recordedProgram.getPackageName()) && - !isDataUriAccessible(recordedProgram.getDataUri())) { - // Since cleaning RecordedProgram from forgotten storage will take some time, - // ignore playback until cleaning is finished. - ToastUtils.show(getContext(), - getContext().getResources().getString(R.string.dvr_toast_recording_deleted), - Toast.LENGTH_SHORT); - return; - } - ParentalControlSettings parental = TvApplication.getSingletons(getActivity()) - .getTvInputManagerHelper().getParentalControlSettings(); - if (!parental.isParentalControlsEnabled()) { - launchPlaybackActivity(recordedProgram, seekTimeMs, false); - return; - } - ChannelDataManager channelDataManager = - TvApplication.getSingletons(getActivity()).getChannelDataManager(); - Channel channel = channelDataManager.getChannel(recordedProgram.getChannelId()); - if (channel != null && channel.isLocked()) { - checkPinToPlay(recordedProgram, seekTimeMs); - return; - } - String ratingString = recordedProgram.getContentRating(); - if (TextUtils.isEmpty(ratingString)) { - launchPlaybackActivity(recordedProgram, seekTimeMs, false); - return; - } - String[] ratingList = ratingString.split(","); - TvContentRating[] programRatings = new TvContentRating[ratingList.length]; - for (int i = 0; i < ratingList.length; i++) { - programRatings[i] = TvContentRating.unflattenFromString(ratingList[i]); - } - TvContentRating blockRatings = parental.getBlockedRating(programRatings); - if (blockRatings != null) { - checkPinToPlay(recordedProgram, seekTimeMs); - } else { - launchPlaybackActivity(recordedProgram, seekTimeMs, false); - } - } - - private boolean isDataUriAccessible(Uri dataUri) { - if (dataUri == null || dataUri.getPath() == null) { - return false; - } - try { - File recordedProgramPath = new File(dataUri.getPath()); - if (recordedProgramPath.exists()) { - return true; - } - } catch (SecurityException e) { - } - return false; - } - - private void checkPinToPlay(RecordedProgram recordedProgram, long seekTimeMs) { - new PinDialogFragment(PinDialogFragment.PIN_DIALOG_TYPE_UNLOCK_PROGRAM, - new PinDialogFragment.ResultListener() { - @Override - public void done(boolean success) { - if (success) { - launchPlaybackActivity(recordedProgram, seekTimeMs, true); - } - } - }).show(getActivity().getFragmentManager(), PinDialogFragment.DIALOG_TAG); - } - - private void launchPlaybackActivity(RecordedProgram mRecordedProgram, long seekTimeMs, - boolean pinChecked) { - Intent intent = new Intent(getActivity(), DvrPlaybackActivity.class); - intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_ID, mRecordedProgram.getId()); - if (seekTimeMs != TvInputManager.TIME_SHIFT_INVALID_TIME) { - intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_SEEK_TIME, seekTimeMs); - } - intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_PIN_CHECKED, pinChecked); - getActivity().startActivity(intent); - } - - private static class MyImageLoaderCallback extends - ImageLoader.ImageLoaderCallback { - private final Context mContext; - private final int mLoadType; - - public MyImageLoaderCallback(DvrDetailsFragment fragment, - int loadType, Context context) { - super(fragment); - mLoadType = loadType; - mContext = context; - } - - @Override - public void onBitmapLoaded(DvrDetailsFragment fragment, - @Nullable Bitmap bitmap) { - Drawable drawable; - int loadType = mLoadType; - if (bitmap == null) { - Resources res = mContext.getResources(); - drawable = res.getDrawable(R.drawable.dvr_default_poster, null); - if ((loadType & LOAD_BACKGROUND_IMAGE) != 0 && !fragment.isDetached()) { - loadType &= ~LOAD_BACKGROUND_IMAGE; - fragment.mBackgroundHelper.setBackgroundColor( - res.getColor(R.color.dvr_detail_default_background)); - fragment.mBackgroundHelper.setScrim( - res.getColor(R.color.dvr_detail_default_background_scrim)); - } - } else { - drawable = new BitmapDrawable(mContext.getResources(), bitmap); - } - if (!fragment.isDetached()) { - if ((loadType & LOAD_LOGO_IMAGE) != 0) { - fragment.mDetailsOverview.setImageDrawable(drawable); - } - if ((loadType & LOAD_BACKGROUND_IMAGE) != 0) { - fragment.mBackgroundHelper.setBackground(drawable); - } - } - } - } -} diff --git a/src/com/android/tv/dvr/ui/DvrForgetStorageErrorFragment.java b/src/com/android/tv/dvr/ui/DvrForgetStorageErrorFragment.java deleted file mode 100644 index 73ddcdd0..00000000 --- a/src/com/android/tv/dvr/ui/DvrForgetStorageErrorFragment.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.dvr.ui; - -import android.app.Activity; -import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.v17.leanback.widget.GuidanceStylist.Guidance; -import android.support.v17.leanback.widget.GuidedAction; -import android.text.TextUtils; - -import com.android.tv.R; -import com.android.tv.TvApplication; -import com.android.tv.common.SoftPreconditions; -import com.android.tv.dvr.DvrDataManager; -import com.android.tv.dvr.DvrManager; - -import java.util.List; - -public class DvrForgetStorageErrorFragment extends DvrGuidedStepFragment { - private static final int ACTION_CANCEL = 1; - private static final int ACTION_FORGET_STORAGE = 2; - private String mInputId; - - @Override - public void onCreate(Bundle savedInstanceState) { - Bundle args = getArguments(); - if (args != null) { - mInputId = args.getString(DvrHalfSizedDialogFragment.KEY_INPUT_ID); - } - SoftPreconditions.checkArgument(!TextUtils.isEmpty(mInputId)); - super.onCreate(savedInstanceState); - } - - @NonNull - @Override - public Guidance onCreateGuidance(Bundle savedInstanceState) { - String title = getResources().getString(R.string.dvr_error_forget_storage_title); - String description = getResources().getString( - R.string.dvr_error_forget_storage_description); - return new Guidance(title, description, null, null); - } - - @Override - public void onCreateActions(@NonNull List actions, Bundle savedInstanceState) { - Activity activity = getActivity(); - actions.add(new GuidedAction.Builder(activity) - .id(ACTION_CANCEL) - .title(getResources().getString(R.string.dvr_action_error_cancel)) - .build()); - actions.add(new GuidedAction.Builder(activity) - .id(ACTION_FORGET_STORAGE) - .title(getResources().getString(R.string.dvr_action_error_forget_storage)) - .build()); - } - - @Override - public void onGuidedActionClicked(GuidedAction action) { - if (action.getId() != ACTION_FORGET_STORAGE) { - dismissDialog(); - return; - } - DvrManager dvrManager = TvApplication.getSingletons(getContext()).getDvrManager(); - dvrManager.forgetStorage(mInputId); - Activity activity = getActivity(); - if (activity instanceof DvrDetailsActivity) { - // Since we removed everything, just finish the activity. - activity.finish(); - } else { - dismissDialog(); - } - } -} diff --git a/src/com/android/tv/dvr/ui/DvrGuidedStepFragment.java b/src/com/android/tv/dvr/ui/DvrGuidedStepFragment.java index d26e6836..433588da 100644 --- a/src/com/android/tv/dvr/ui/DvrGuidedStepFragment.java +++ b/src/com/android/tv/dvr/ui/DvrGuidedStepFragment.java @@ -16,10 +16,12 @@ package com.android.tv.dvr.ui; +import android.app.Activity; import android.app.DialogFragment; import android.content.Context; import android.os.Bundle; import android.support.v17.leanback.app.GuidedStepFragment; +import android.support.v17.leanback.widget.GuidanceStylist; import android.support.v17.leanback.widget.GuidedAction; import android.support.v17.leanback.widget.VerticalGridView; import android.view.LayoutInflater; @@ -29,11 +31,26 @@ import android.view.ViewGroup; import com.android.tv.MainActivity; import com.android.tv.R; import com.android.tv.TvApplication; +import com.android.tv.dialog.HalfSizedDialogFragment.OnActionClickListener; import com.android.tv.dialog.SafeDismissDialogFragment; import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.ui.HalfSizedDialogFragment.OnActionClickListener; + +import java.util.List; public class DvrGuidedStepFragment extends GuidedStepFragment { + /** + * Action ID for "recording/scheduling the program anyway". + */ + public static final int ACTION_RECORD_ANYWAY = 1; + /** + * Action ID for "deleting existed recordings". + */ + public static final int ACTION_DELETE_RECORDINGS = 2; + /** + * Action ID for "cancelling current recording request". + */ + public static final int ACTION_CANCEL_RECORDING = 3; + private DvrManager mDvrManager; private OnActionClickListener mOnActionClickListener; @@ -86,4 +103,35 @@ public class DvrGuidedStepFragment extends GuidedStepFragment { protected void setOnActionClickListener(OnActionClickListener listener) { mOnActionClickListener = listener; } + + /** + * The inner guided step fragment for + * {@link com.android.tv.dvr.ui.DvrHalfSizedDialogFragment + * .DvrNoFreeSpaceErrorDialogFragment}. + */ + public static class DvrNoFreeSpaceErrorFragment + extends DvrGuidedStepFragment { + @Override + public GuidanceStylist.Guidance onCreateGuidance(Bundle savedInstanceState) { + return new GuidanceStylist.Guidance(getString(R.string.dvr_error_no_free_space_title), + getString(R.string.dvr_error_no_free_space_description), null, null); + } + + @Override + public void onCreateActions(List actions, Bundle savedInstanceState) { + Activity activity = getActivity(); + actions.add(new GuidedAction.Builder(activity) + .id(ACTION_RECORD_ANYWAY) + .title(R.string.dvr_action_record_anyway) + .build()); + actions.add(new GuidedAction.Builder(activity) + .id(ACTION_DELETE_RECORDINGS) + .title(R.string.dvr_action_delete_recordings) + .build()); + actions.add(new GuidedAction.Builder(activity) + .id(ACTION_CANCEL_RECORDING) + .title(R.string.dvr_action_record_cancel) + .build()); + } + } } \ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/DvrHalfSizedDialogFragment.java b/src/com/android/tv/dvr/ui/DvrHalfSizedDialogFragment.java index 2b132db8..9054dd03 100644 --- a/src/com/android/tv/dvr/ui/DvrHalfSizedDialogFragment.java +++ b/src/com/android/tv/dvr/ui/DvrHalfSizedDialogFragment.java @@ -29,6 +29,7 @@ import android.view.ViewGroup; import com.android.tv.MainActivity; import com.android.tv.R; import com.android.tv.dvr.DvrStorageStatusManager; +import com.android.tv.dialog.HalfSizedDialogFragment; import com.android.tv.dvr.ui.DvrConflictFragment.DvrChannelWatchConflictFragment; import com.android.tv.dvr.ui.DvrConflictFragment.DvrProgramConflictFragment; import com.android.tv.guide.ProgramGuide; @@ -165,6 +166,17 @@ public class DvrHalfSizedDialogFragment extends HalfSizedDialogFragment { } } + /** + * A dialog fragment to show error message when there is no enough free space to record. + */ + public static class DvrNoFreeSpaceErrorDialogFragment + extends DvrGuidedStepDialogFragment { + @Override + protected DvrGuidedStepFragment onCreateGuidedStepFragment() { + return new DvrGuidedStepFragment.DvrNoFreeSpaceErrorFragment(); + } + } + /** * A dialog fragment to show error message when the current storage is too small to * support DVR diff --git a/src/com/android/tv/dvr/ui/DvrInsufficientSpaceErrorFragment.java b/src/com/android/tv/dvr/ui/DvrInsufficientSpaceErrorFragment.java index 3b1dbfa0..3c5df1a6 100644 --- a/src/com/android/tv/dvr/ui/DvrInsufficientSpaceErrorFragment.java +++ b/src/com/android/tv/dvr/ui/DvrInsufficientSpaceErrorFragment.java @@ -17,6 +17,7 @@ package com.android.tv.dvr.ui; import android.app.Activity; +import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.support.v17.leanback.widget.GuidanceStylist.Guidance; @@ -24,19 +25,67 @@ import android.support.v17.leanback.widget.GuidedAction; import com.android.tv.R; import com.android.tv.TvApplication; -import com.android.tv.dvr.DvrDataManager; +import com.android.tv.common.SoftPreconditions; +import com.android.tv.dvr.ui.browse.DvrBrowseActivity; +import java.util.ArrayList; import java.util.List; public class DvrInsufficientSpaceErrorFragment extends DvrGuidedStepFragment { - private static final int ACTION_DONE = 1; - private static final int ACTION_OPEN_DVR = 2; + /** + * Key for the failed scheduled recordings information. + */ + public static final String FAILED_SCHEDULED_RECORDING_INFOS = + "failed_scheduled_recording_infos"; + + private static final String TAG = "DvrInsufficientSpaceErrorFragment"; + + private static final int ACTION_VIEW_RECENT_RECORDINGS = 1; + + private ArrayList mFailedScheduledRecordingInfos; + + @Override + public void onAttach(Context context) { + super.onAttach(context); + Bundle args = getArguments(); + if (args != null) { + mFailedScheduledRecordingInfos = + args.getStringArrayList(FAILED_SCHEDULED_RECORDING_INFOS); + } + SoftPreconditions.checkState( + mFailedScheduledRecordingInfos != null && !mFailedScheduledRecordingInfos.isEmpty(), + TAG, "failed scheduled recording is null"); + } @Override public Guidance onCreateGuidance(Bundle savedInstanceState) { - String title = getResources().getString(R.string.dvr_error_insufficient_space_title); - String description = getResources() - .getString(R.string.dvr_error_insufficient_space_description); + String title; + String description; + int failedScheduledRecordingSize = mFailedScheduledRecordingInfos.size(); + if (failedScheduledRecordingSize == 1) { + title = getString( + R.string.dvr_error_insufficient_space_title_one_recording, + mFailedScheduledRecordingInfos.get(0)); + description = getString( + R.string.dvr_error_insufficient_space_description_one_recording, + mFailedScheduledRecordingInfos.get(0)); + } else if (failedScheduledRecordingSize == 2) { + title = getString( + R.string.dvr_error_insufficient_space_title_two_recordings, + mFailedScheduledRecordingInfos.get(0), mFailedScheduledRecordingInfos.get(1)); + description = getString( + R.string.dvr_error_insufficient_space_description_two_recordings, + mFailedScheduledRecordingInfos.get(0), mFailedScheduledRecordingInfos.get(1)); + } else { + title = getString( + R.string.dvr_error_insufficient_space_title_three_or_more_recordings, + mFailedScheduledRecordingInfos.get(0), mFailedScheduledRecordingInfos.get(1), + mFailedScheduledRecordingInfos.get(2)); + description = getString( + R.string.dvr_error_insufficient_space_description_three_or_more_recordings, + mFailedScheduledRecordingInfos.get(0), mFailedScheduledRecordingInfos.get(1), + mFailedScheduledRecordingInfos.get(2)); + } return new Guidance(title, description, null, null); } @@ -44,26 +93,21 @@ public class DvrInsufficientSpaceErrorFragment extends DvrGuidedStepFragment { public void onCreateActions(List actions, Bundle savedInstanceState) { Activity activity = getActivity(); actions.add(new GuidedAction.Builder(activity) - .id(ACTION_DONE) - .title(getResources().getString(R.string.dvr_action_error_done)) + .clickAction(GuidedAction.ACTION_ID_OK) .build()); - DvrDataManager dvrDataManager = TvApplication.getSingletons(getContext()) - .getDvrDataManager(); - if (!(dvrDataManager.getRecordedPrograms().isEmpty() - && dvrDataManager.getStartedRecordings().isEmpty() - && dvrDataManager.getNonStartedScheduledRecordings().isEmpty() - && dvrDataManager.getSeriesRecordings().isEmpty())) { - actions.add(new GuidedAction.Builder(activity) - .id(ACTION_OPEN_DVR) - .title(getResources().getString(R.string.dvr_action_error_open_dvr)) - .build()); + if (TvApplication.getSingletons(getContext()).getDvrManager().hasValidItems()) { + actions.add(new GuidedAction.Builder(activity) + .id(ACTION_VIEW_RECENT_RECORDINGS) + .title(getResources().getString( + R.string.dvr_error_insufficient_space_action_view_recent_recordings)) + .build()); } } @Override public void onGuidedActionClicked(GuidedAction action) { - if (action.getId() == ACTION_OPEN_DVR) { - Intent intent = new Intent(getActivity(), DvrActivity.class); + if (action.getId() == ACTION_VIEW_RECENT_RECORDINGS) { + Intent intent = new Intent(getActivity(), DvrBrowseActivity.class); getActivity().startActivity(intent); } dismissDialog(); diff --git a/src/com/android/tv/dvr/ui/DvrItemPresenter.java b/src/com/android/tv/dvr/ui/DvrItemPresenter.java deleted file mode 100644 index 339e5d2f..00000000 --- a/src/com/android/tv/dvr/ui/DvrItemPresenter.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.dvr.ui; - -import android.app.Activity; -import android.support.annotation.CallSuper; -import android.support.v17.leanback.widget.Presenter; -import android.view.View; -import android.view.View.OnClickListener; - -import com.android.tv.dvr.DvrUiHelper; - -import java.util.HashSet; -import java.util.Iterator; -import java.util.Set; - -/** - * An abstract class to present DVR items in {@link RecordingCardView}, which is mainly used in - * {@link DvrBrowseFragment}. DVR items might include: {@link ScheduledRecording}, - * {@link RecordedProgram}, and {@link SeriesRecording}. - */ -public abstract class DvrItemPresenter extends Presenter { - private final Set mBoundViewHolders = new HashSet<>(); - private final OnClickListener mOnClickListener = onCreateOnClickListener(); - - @Override - @CallSuper - public void onBindViewHolder(ViewHolder viewHolder, Object o) { - viewHolder.view.setTag(o); - viewHolder.view.setOnClickListener(mOnClickListener); - mBoundViewHolders.add(viewHolder); - } - - @Override - @CallSuper - public void onUnbindViewHolder(ViewHolder viewHolder) { - mBoundViewHolders.remove(viewHolder); - } - - /** - * Unbinds all bound view holders. - */ - public void unbindAllViewHolders() { - // When browse fragments are destroyed, RecyclerView would not call presenters' - // onUnbindViewHolder(). We should handle it by ourselves to prevent resources leaks. - for (ViewHolder viewHolder : new HashSet<>(mBoundViewHolders)) { - onUnbindViewHolder(viewHolder); - } - } - - /** - * Creates {@link OnClickListener} for DVR library's card views. - */ - protected OnClickListener onCreateOnClickListener() { - return new OnClickListener() { - @Override - public void onClick(View view) { - if (view instanceof RecordingCardView) { - RecordingCardView v = (RecordingCardView) view; - DvrUiHelper.startDetailsActivity((Activity) v.getContext(), - v.getTag(), v.getImageView(), false); - } - } - }; - } -} \ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/DvrMissingStorageErrorFragment.java b/src/com/android/tv/dvr/ui/DvrMissingStorageErrorFragment.java index 2e2c2849..8dc9eb4e 100644 --- a/src/com/android/tv/dvr/ui/DvrMissingStorageErrorFragment.java +++ b/src/com/android/tv/dvr/ui/DvrMissingStorageErrorFragment.java @@ -17,29 +17,27 @@ package com.android.tv.dvr.ui; import android.app.Activity; +import android.content.ActivityNotFoundException; +import android.content.Intent; import android.os.Bundle; -import android.support.v17.leanback.app.GuidedStepFragment; +import android.provider.Settings; import android.support.v17.leanback.widget.GuidanceStylist.Guidance; import android.support.v17.leanback.widget.GuidedAction; -import android.text.TextUtils; +import android.util.Log; import com.android.tv.R; -import com.android.tv.common.SoftPreconditions; +import com.android.tv.dvr.ui.browse.DvrDetailsActivity; import java.util.List; public class DvrMissingStorageErrorFragment extends DvrGuidedStepFragment { - private static final int ACTION_CANCEL = 1; - private static final int ACTION_FORGET_STORAGE = 2; - private String mInputId; + private static String TAG = "DvrMissingStorageErrorFragment"; + + private static final int ACTION_OK = 1; + private static final int ACTION_OPEN_STORAGE_SETTINGS = 2; @Override public void onCreate(Bundle savedInstanceState) { - Bundle args = getArguments(); - if (args != null) { - mInputId = args.getString(DvrHalfSizedDialogFragment.KEY_INPUT_ID); - } - SoftPreconditions.checkArgument(!TextUtils.isEmpty(mInputId)); super.onCreate(savedInstanceState); } @@ -55,25 +53,31 @@ public class DvrMissingStorageErrorFragment extends DvrGuidedStepFragment { public void onCreateActions(List actions, Bundle savedInstanceState) { Activity activity = getActivity(); actions.add(new GuidedAction.Builder(activity) - .id(ACTION_CANCEL) - .title(getResources().getString(R.string.dvr_action_error_cancel)) + .id(ACTION_OK) + .title(android.R.string.ok) .build()); actions.add(new GuidedAction.Builder(activity) - .id(ACTION_FORGET_STORAGE) - .title(getResources().getString(R.string.dvr_action_error_forget_storage)) + .id(ACTION_OPEN_STORAGE_SETTINGS) + .title(getResources().getString(R.string.dvr_action_error_storage_settings)) .build()); } @Override public void onGuidedActionClicked(GuidedAction action) { - if (action.getId() == ACTION_FORGET_STORAGE) { - DvrForgetStorageErrorFragment fragment = new DvrForgetStorageErrorFragment(); - Bundle args = new Bundle(); - args.putString(DvrHalfSizedDialogFragment.KEY_INPUT_ID, mInputId); - fragment.setArguments(args); - GuidedStepFragment.add(getFragmentManager(), fragment, R.id.halfsized_dialog_host); + Activity activity = getActivity(); + if (activity instanceof DvrDetailsActivity) { + activity.finish(); + } else { + dismissDialog(); + } + if (action.getId() != ACTION_OPEN_STORAGE_SETTINGS) { return; } - dismissDialog(); + final Intent intent = new Intent(Settings.ACTION_INTERNAL_STORAGE_SETTINGS); + try { + getContext().startActivity(intent); + } catch (ActivityNotFoundException e) { + Log.e(TAG, "Can't start internal storage settings activity", e); + } } -} \ No newline at end of file +} diff --git a/src/com/android/tv/dvr/ui/DvrPlaybackCardPresenter.java b/src/com/android/tv/dvr/ui/DvrPlaybackCardPresenter.java deleted file mode 100644 index 8c4c856c..00000000 --- a/src/com/android/tv/dvr/ui/DvrPlaybackCardPresenter.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright (c) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.dvr.ui; - -import android.content.Context; -import android.content.Intent; -import android.content.res.Resources; -import android.text.TextUtils; -import android.util.Log; -import android.view.View; -import android.view.View.OnClickListener; -import android.view.ViewGroup; - -import com.android.tv.R; -import com.android.tv.dvr.RecordedProgram; -import com.android.tv.dvr.DvrPlaybackActivity; -import com.android.tv.util.Utils; - -/** - * This class is used to generate Views and bind Objects for related recordings in DVR playback. - */ -public class DvrPlaybackCardPresenter extends RecordedProgramPresenter { - private static final String TAG = "DvrPlaybackCardPresenter"; - private static final boolean DEBUG = false; - - private final int mRelatedRecordingCardWidth; - private final int mRelatedRecordingCardHeight; - - DvrPlaybackCardPresenter(Context context) { - super(context); - mRelatedRecordingCardWidth = - context.getResources().getDimensionPixelSize(R.dimen.dvr_related_recordings_width); - mRelatedRecordingCardHeight = - context.getResources().getDimensionPixelSize(R.dimen.dvr_related_recordings_height); - } - - @Override - public ViewHolder onCreateViewHolder(ViewGroup parent) { - Resources res = parent.getResources(); - RecordingCardView view = new RecordingCardView( - getContext(), mRelatedRecordingCardWidth, mRelatedRecordingCardHeight); - return new ViewHolder(view); - } - - @Override - protected OnClickListener onCreateOnClickListener() { - return new OnClickListener() { - @Override - public void onClick(View v) { - long programId = ((RecordedProgram) v.getTag()).getId(); - if (DEBUG) Log.d(TAG, "Play Related Recording:" + programId); - Intent intent = new Intent(getContext(), DvrPlaybackActivity.class); - intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_ID, programId); - getContext().startActivity(intent); - } - }; - } - - @Override - protected String getDescription(RecordedProgram program) { - String description = program.getDescription(); - if (TextUtils.isEmpty(description)) { - description = - getContext().getResources().getString(R.string.dvr_msg_no_program_description); - } - return description; - } -} \ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/DvrPlaybackControlHelper.java b/src/com/android/tv/dvr/ui/DvrPlaybackControlHelper.java deleted file mode 100644 index 0bc4ecb1..00000000 --- a/src/com/android/tv/dvr/ui/DvrPlaybackControlHelper.java +++ /dev/null @@ -1,313 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.tv.dvr.ui; - -import android.app.Activity; -import android.graphics.drawable.Drawable; -import android.media.MediaMetadata; -import android.media.session.MediaController; -import android.media.session.MediaController.TransportControls; -import android.media.session.PlaybackState; -import android.support.v17.leanback.app.PlaybackControlGlue; -import android.support.v17.leanback.widget.AbstractDetailsDescriptionPresenter; -import android.support.v17.leanback.widget.Action; -import android.support.v17.leanback.widget.OnActionClickedListener; -import android.support.v17.leanback.widget.PlaybackControlsRow; -import android.support.v17.leanback.widget.PlaybackControlsRowPresenter; -import android.support.v17.leanback.widget.RowPresenter; -import android.text.TextUtils; -import android.util.Log; -import android.view.KeyEvent; -import android.view.View; - -import com.android.tv.R; -import com.android.tv.util.TimeShiftUtils; - -/** - * A helper class to assist {@link DvrPlaybackOverlayFragment} to manage its controls row and - * send command to the media controller. It also helps to update playback states displayed in the - * fragment according to information the media session provides. - */ -public class DvrPlaybackControlHelper extends PlaybackControlGlue { - private static final String TAG = "DvrPlaybackControlHelper"; - private static final boolean DEBUG = false; - - /** - * Indicates the ID of the media under playback is unknown. - */ - public static int UNKNOWN_MEDIA_ID = -1; - - private int mPlaybackState = PlaybackState.STATE_NONE; - private int mPlaybackSpeedLevel; - private int mPlaybackSpeedId; - private boolean mReadyToControl; - - private final MediaController mMediaController; - private final MediaController.Callback mMediaControllerCallback = new MediaControllerCallback(); - private final TransportControls mTransportControls; - private final int mExtraPaddingTopForNoDescription; - - public DvrPlaybackControlHelper(Activity activity, DvrPlaybackOverlayFragment overlayFragment) { - super(activity, overlayFragment, new int[TimeShiftUtils.MAX_SPEED_LEVEL + 1]); - mMediaController = activity.getMediaController(); - mMediaController.registerCallback(mMediaControllerCallback); - mTransportControls = mMediaController.getTransportControls(); - mExtraPaddingTopForNoDescription = activity.getResources() - .getDimensionPixelOffset(R.dimen.dvr_playback_controls_extra_padding_top); - } - - @Override - public PlaybackControlsRowPresenter createControlsRowAndPresenter() { - PlaybackControlsRow controlsRow = new PlaybackControlsRow(this); - setControlsRow(controlsRow); - AbstractDetailsDescriptionPresenter detailsPresenter = - new AbstractDetailsDescriptionPresenter() { - @Override - protected void onBindDescription( - AbstractDetailsDescriptionPresenter.ViewHolder viewHolder, Object object) { - PlaybackControlGlue glue = (PlaybackControlGlue) object; - if (glue.hasValidMedia()) { - viewHolder.getTitle().setText(glue.getMediaTitle()); - viewHolder.getSubtitle().setText(glue.getMediaSubtitle()); - } else { - viewHolder.getTitle().setText(""); - viewHolder.getSubtitle().setText(""); - } - if (TextUtils.isEmpty(viewHolder.getSubtitle().getText())) { - viewHolder.view.setPadding(viewHolder.view.getPaddingLeft(), - mExtraPaddingTopForNoDescription, - viewHolder.view.getPaddingRight(), viewHolder.view.getPaddingBottom()); - } - } - }; - PlaybackControlsRowPresenter presenter = - new PlaybackControlsRowPresenter(detailsPresenter) { - @Override - protected void onBindRowViewHolder(RowPresenter.ViewHolder vh, Object item) { - super.onBindRowViewHolder(vh, item); - vh.setOnKeyListener(DvrPlaybackControlHelper.this); - } - - @Override - protected void onUnbindRowViewHolder(RowPresenter.ViewHolder vh) { - super.onUnbindRowViewHolder(vh); - vh.setOnKeyListener(null); - } - }; - presenter.setProgressColor(getContext().getResources() - .getColor(R.color.play_controls_progress_bar_watched)); - presenter.setBackgroundColor(getContext().getResources() - .getColor(R.color.play_controls_body_background_enabled)); - presenter.setOnActionClickedListener(new OnActionClickedListener() { - @Override - public void onActionClicked(Action action) { - if (mReadyToControl) { - DvrPlaybackControlHelper.super.onActionClicked(action); - } - } - }); - return presenter; - } - - @Override - public boolean onKey(View v, int keyCode, KeyEvent event) { - if (mReadyToControl) { - if (keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE && event.getAction() == KeyEvent.ACTION_DOWN - && (mPlaybackState == PlaybackState.STATE_FAST_FORWARDING - || mPlaybackState == PlaybackState.STATE_REWINDING)) { - // Workaround of b/31489271. Clicks play/pause button first to reset play controls - // to "play" state. Then we can pass MEDIA_PAUSE to let playback be paused. - onActionClicked(getControlsRow().getActionForKeyCode(keyCode)); - } - return super.onKey(v, keyCode, event); - } - return false; - } - - @Override - public boolean hasValidMedia() { - PlaybackState playbackState = mMediaController.getPlaybackState(); - return playbackState != null; - } - - @Override - public boolean isMediaPlaying() { - PlaybackState playbackState = mMediaController.getPlaybackState(); - if (playbackState == null) { - return false; - } - int state = playbackState.getState(); - return state != PlaybackState.STATE_NONE && state != PlaybackState.STATE_CONNECTING - && state != PlaybackState.STATE_PAUSED; - } - - /** - * Returns the ID of the media under playback. - */ - public long getMediaId() { - MediaMetadata mediaMetadata = mMediaController.getMetadata(); - return mediaMetadata == null ? UNKNOWN_MEDIA_ID - : mediaMetadata.getLong(MediaMetadata.METADATA_KEY_MEDIA_ID); - } - - @Override - public CharSequence getMediaTitle() { - MediaMetadata mediaMetadata = mMediaController.getMetadata(); - return mediaMetadata == null ? "" - : mediaMetadata.getString(MediaMetadata.METADATA_KEY_TITLE); - } - - @Override - public CharSequence getMediaSubtitle() { - MediaMetadata mediaMetadata = mMediaController.getMetadata(); - return mediaMetadata == null ? "" - : mediaMetadata.getString(MediaMetadata.METADATA_KEY_DISPLAY_SUBTITLE); - } - - @Override - public int getMediaDuration() { - MediaMetadata mediaMetadata = mMediaController.getMetadata(); - return mediaMetadata == null ? 0 - : (int) mediaMetadata.getLong(MediaMetadata.METADATA_KEY_DURATION); - } - - @Override - public Drawable getMediaArt() { - // Do not show the poster art on control row. - return null; - } - - @Override - public long getSupportedActions() { - return ACTION_PLAY_PAUSE | ACTION_FAST_FORWARD | ACTION_REWIND; - } - - @Override - public int getCurrentSpeedId() { - return mPlaybackSpeedId; - } - - @Override - public int getCurrentPosition() { - PlaybackState playbackState = mMediaController.getPlaybackState(); - if (playbackState == null) { - return 0; - } - return (int) playbackState.getPosition(); - } - - /** - * Unregister media controller's callback. - */ - public void unregisterCallback() { - mMediaController.unregisterCallback(mMediaControllerCallback); - } - - @Override - protected void startPlayback(int speedId) { - if (getCurrentSpeedId() == speedId) { - return; - } - if (speedId == PLAYBACK_SPEED_NORMAL) { - mTransportControls.play(); - } else if (speedId <= -PLAYBACK_SPEED_FAST_L0) { - mTransportControls.rewind(); - } else if (speedId >= PLAYBACK_SPEED_FAST_L0){ - mTransportControls.fastForward(); - } - } - - @Override - protected void pausePlayback() { - mTransportControls.pause(); - } - - @Override - protected void skipToNext() { - // Do nothing. - } - - @Override - protected void skipToPrevious() { - // Do nothing. - } - - @Override - protected void onRowChanged(PlaybackControlsRow row) { - // Do nothing. - } - - private void onStateChanged(int state, long positionMs, int speedLevel) { - if (DEBUG) Log.d(TAG, "onStateChanged"); - getControlsRow().setCurrentTime((int) positionMs); - if (state == mPlaybackState && mPlaybackSpeedLevel == speedLevel) { - // Only position is changed, no need to update controls row - return; - } - // NOTICE: The below two variables should only be used in this method. - // The only usage of them is to confirm if the state is changed or not. - mPlaybackState = state; - mPlaybackSpeedLevel = speedLevel; - switch (state) { - case PlaybackState.STATE_PLAYING: - mPlaybackSpeedId = PLAYBACK_SPEED_NORMAL; - setFadingEnabled(true); - mReadyToControl = true; - break; - case PlaybackState.STATE_PAUSED: - mPlaybackSpeedId = PLAYBACK_SPEED_PAUSED; - setFadingEnabled(true); - mReadyToControl = true; - break; - case PlaybackState.STATE_FAST_FORWARDING: - mPlaybackSpeedId = PLAYBACK_SPEED_FAST_L0 + speedLevel; - setFadingEnabled(false); - mReadyToControl = true; - break; - case PlaybackState.STATE_REWINDING: - mPlaybackSpeedId = -PLAYBACK_SPEED_FAST_L0 - speedLevel; - setFadingEnabled(false); - mReadyToControl = true; - break; - case PlaybackState.STATE_CONNECTING: - setFadingEnabled(false); - mReadyToControl = false; - break; - case PlaybackState.STATE_NONE: - mReadyToControl = false; - break; - default: - setFadingEnabled(true); - break; - } - onStateChanged(); - } - - private class MediaControllerCallback extends MediaController.Callback { - @Override - public void onPlaybackStateChanged(PlaybackState state) { - if (DEBUG) Log.d(TAG, "Playback state changed: " + state.getState()); - onStateChanged(state.getState(), state.getPosition(), (int) state.getPlaybackSpeed()); - } - - @Override - public void onMetadataChanged(MediaMetadata metadata) { - DvrPlaybackControlHelper.this.onMetadataChanged(); - ((DvrPlaybackOverlayFragment) getFragment()).onMediaControllerUpdated(); - } - } -} \ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/DvrPlaybackOverlayFragment.java b/src/com/android/tv/dvr/ui/DvrPlaybackOverlayFragment.java deleted file mode 100644 index 51ec93b8..00000000 --- a/src/com/android/tv/dvr/ui/DvrPlaybackOverlayFragment.java +++ /dev/null @@ -1,304 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.tv.dvr.ui; - -import android.content.Context; -import android.content.Intent; -import android.graphics.Point; -import android.hardware.display.DisplayManager; -import android.media.tv.TvContentRating; -import android.os.Bundle; -import android.media.session.PlaybackState; -import android.media.tv.TvInputManager; -import android.media.tv.TvView; -import android.support.v17.leanback.app.PlaybackOverlayFragment; -import android.support.v17.leanback.widget.ArrayObjectAdapter; -import android.support.v17.leanback.widget.ClassPresenterSelector; -import android.support.v17.leanback.widget.HeaderItem; -import android.support.v17.leanback.widget.ListRow; -import android.support.v17.leanback.widget.ListRowPresenter; -import android.support.v17.leanback.widget.PlaybackControlsRow; -import android.support.v17.leanback.widget.PlaybackControlsRowPresenter; -import android.support.v17.leanback.widget.SinglePresenterSelector; -import android.view.Display; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Toast; -import android.text.TextUtils; -import android.util.Log; - -import com.android.tv.R; -import com.android.tv.TvApplication; -import com.android.tv.data.BaseProgram; -import com.android.tv.dvr.RecordedProgram; -import com.android.tv.dialog.PinDialogFragment; -import com.android.tv.dvr.DvrDataManager; -import com.android.tv.dvr.DvrPlayer; -import com.android.tv.dvr.DvrPlaybackMediaSessionHelper; -import com.android.tv.parental.ContentRatingsManager; -import com.android.tv.util.Utils; - -public class DvrPlaybackOverlayFragment extends PlaybackOverlayFragment { - // TODO: Handles audio focus. Deals with block and ratings. - private static final String TAG = "DvrPlaybackOverlayFragment"; - private static final boolean DEBUG = false; - - private static final String MEDIA_SESSION_TAG = "com.android.tv.dvr.mediasession"; - private static final float DISPLAY_ASPECT_RATIO_EPSILON = 0.01f; - - // mProgram is only used to store program from intent. Don't use it elsewhere. - private RecordedProgram mProgram; - private DvrPlaybackMediaSessionHelper mMediaSessionHelper; - private DvrPlaybackControlHelper mPlaybackControlHelper; - private ArrayObjectAdapter mRowsAdapter; - private SortedArrayAdapter mRelatedRecordingsRowAdapter; - private DvrPlaybackCardPresenter mRelatedRecordingCardPresenter; - private DvrDataManager mDvrDataManager; - private ContentRatingsManager mContentRatingsManager; - private TvView mTvView; - private View mBlockScreenView; - private ListRow mRelatedRecordingsRow; - private int mExtraPaddingNoRelatedRow; - private int mWindowWidth; - private int mWindowHeight; - private float mAppliedAspectRatio; - private float mWindowAspectRatio; - private boolean mPinChecked; - - @Override - public void onCreate(Bundle savedInstanceState) { - if (DEBUG) Log.d(TAG, "onCreate"); - super.onCreate(savedInstanceState); - mExtraPaddingNoRelatedRow = getActivity().getResources() - .getDimensionPixelOffset(R.dimen.dvr_playback_fragment_extra_padding_top); - mDvrDataManager = TvApplication.getSingletons(getActivity()).getDvrDataManager(); - mContentRatingsManager = TvApplication.getSingletons(getContext()) - .getTvInputManagerHelper().getContentRatingsManager(); - mProgram = getProgramFromIntent(getActivity().getIntent()); - if (mProgram == null) { - Toast.makeText(getActivity(), getString(R.string.dvr_program_not_found), - Toast.LENGTH_SHORT).show(); - getActivity().finish(); - return; - } - Point size = new Point(); - ((DisplayManager) getContext().getSystemService(Context.DISPLAY_SERVICE)) - .getDisplay(Display.DEFAULT_DISPLAY).getSize(size); - mWindowWidth = size.x; - mWindowHeight = size.y; - mWindowAspectRatio = mAppliedAspectRatio = (float) mWindowWidth / mWindowHeight; - setBackgroundType(PlaybackOverlayFragment.BG_LIGHT); - setFadingEnabled(true); - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - mTvView = (TvView) getActivity().findViewById(R.id.dvr_tv_view); - mBlockScreenView = getActivity().findViewById(R.id.block_screen); - mMediaSessionHelper = new DvrPlaybackMediaSessionHelper( - getActivity(), MEDIA_SESSION_TAG, new DvrPlayer(mTvView), this); - mPlaybackControlHelper = new DvrPlaybackControlHelper(getActivity(), this); - setUpRows(); - preparePlayback(getActivity().getIntent()); - DvrPlayer dvrPlayer = mMediaSessionHelper.getDvrPlayer(); - dvrPlayer.setAspectRatioChangedListener(new DvrPlayer.AspectRatioChangedListener() { - @Override - public void onAspectRatioChanged(float videoAspectRatio) { - updateAspectRatio(videoAspectRatio); - } - }); - mPinChecked = getActivity().getIntent() - .getBooleanExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_PIN_CHECKED, false); - dvrPlayer.setContentBlockedListener(new DvrPlayer.ContentBlockedListener() { - @Override - public void onContentBlocked(TvContentRating rating) { - if (mPinChecked) { - mTvView.unblockContent(rating); - return; - } - mBlockScreenView.setVisibility(View.VISIBLE); - getActivity().getMediaController().getTransportControls().pause(); - new PinDialogFragment(PinDialogFragment.PIN_DIALOG_TYPE_UNLOCK_DVR, - new PinDialogFragment.ResultListener() { - @Override - public void done(boolean success) { - if (success) { - mPinChecked = true; - mTvView.unblockContent(rating); - mBlockScreenView.setVisibility(View.GONE); - getActivity().getMediaController() - .getTransportControls().play(); - } - } - }, mContentRatingsManager.getDisplayNameForRating(rating)) - .show(getActivity().getFragmentManager(), PinDialogFragment.DIALOG_TAG); - } - }); - } - - @Override - public void onPause() { - if (DEBUG) Log.d(TAG, "onPause"); - super.onPause(); - if (mMediaSessionHelper.getPlaybackState() == PlaybackState.STATE_FAST_FORWARDING - || mMediaSessionHelper.getPlaybackState() == PlaybackState.STATE_REWINDING) { - getActivity().getMediaController().getTransportControls().pause(); - } - if (mMediaSessionHelper.getPlaybackState() == PlaybackState.STATE_NONE) { - getActivity().requestVisibleBehind(false); - } else { - getActivity().requestVisibleBehind(true); - } - } - - @Override - public void onDestroy() { - if (DEBUG) Log.d(TAG, "onDestroy"); - mPlaybackControlHelper.unregisterCallback(); - mMediaSessionHelper.release(); - mRelatedRecordingCardPresenter.unbindAllViewHolders(); - super.onDestroy(); - } - - /** - * Passes the intent to the fragment. - */ - public void onNewIntent(Intent intent) { - mProgram = getProgramFromIntent(intent); - if (mProgram == null) { - Toast.makeText(getActivity(), getString(R.string.dvr_program_not_found), - Toast.LENGTH_SHORT).show(); - // Continue playing the original program - return; - } - preparePlayback(intent); - } - - /** - * Should be called when windows' size is changed in order to notify DVR player - * to update it's view width/height and position. - */ - public void onWindowSizeChanged(final int windowWidth, final int windowHeight) { - mWindowWidth = windowWidth; - mWindowHeight = windowHeight; - mWindowAspectRatio = (float) mWindowWidth / mWindowHeight; - updateAspectRatio(mAppliedAspectRatio); - } - - public RecordedProgram getNextEpisode(RecordedProgram program) { - int position = mRelatedRecordingsRowAdapter.findInsertPosition(program); - if (position == mRelatedRecordingsRowAdapter.size()) { - return null; - } else { - return (RecordedProgram) mRelatedRecordingsRowAdapter.get(position); - } - } - - void onMediaControllerUpdated() { - mRowsAdapter.notifyArrayItemRangeChanged(0, 1); - } - - private void updateAspectRatio(float videoAspectRatio) { - if (Math.abs(mAppliedAspectRatio - videoAspectRatio) < DISPLAY_ASPECT_RATIO_EPSILON) { - // No need to change - return; - } - if (videoAspectRatio < mWindowAspectRatio) { - int newPadding = (mWindowWidth - Math.round(mWindowHeight * videoAspectRatio)) / 2; - ((ViewGroup) mTvView.getParent()).setPadding(newPadding, 0, newPadding, 0); - } else { - int newPadding = (mWindowHeight - Math.round(mWindowWidth / videoAspectRatio)) / 2; - ((ViewGroup) mTvView.getParent()).setPadding(0, newPadding, 0, newPadding); - } - mAppliedAspectRatio = videoAspectRatio; - } - - private void preparePlayback(Intent intent) { - mMediaSessionHelper.setupPlayback(mProgram, getSeekTimeFromIntent(intent)); - getActivity().getMediaController().getTransportControls().prepare(); - updateRelatedRecordingsRow(); - } - - private void updateRelatedRecordingsRow() { - boolean wasEmpty = (mRelatedRecordingsRowAdapter.size() == 0); - mRelatedRecordingsRowAdapter.clear(); - long programId = mProgram.getId(); - String seriesId = mProgram.getSeriesId(); - if (!TextUtils.isEmpty(seriesId)) { - if (DEBUG) Log.d(TAG, "Update related recordings with:" + seriesId); - for (RecordedProgram program : mDvrDataManager.getRecordedPrograms()) { - if (seriesId.equals(program.getSeriesId()) && programId != program.getId()) { - mRelatedRecordingsRowAdapter.add(program); - } - } - } - View view = getView(); - if (mRelatedRecordingsRowAdapter.size() == 0) { - mRowsAdapter.remove(mRelatedRecordingsRow); - view.setPadding(view.getPaddingLeft(), mExtraPaddingNoRelatedRow, - view.getPaddingRight(), view.getPaddingBottom()); - } else if (wasEmpty){ - mRowsAdapter.add(mRelatedRecordingsRow); - view.setPadding(view.getPaddingLeft(), 0, - view.getPaddingRight(), view.getPaddingBottom()); - } - } - - private void setUpRows() { - PlaybackControlsRowPresenter controlsRowPresenter = - mPlaybackControlHelper.createControlsRowAndPresenter(); - - ClassPresenterSelector selector = new ClassPresenterSelector(); - selector.addClassPresenter(PlaybackControlsRow.class, controlsRowPresenter); - selector.addClassPresenter(ListRow.class, new ListRowPresenter()); - - mRowsAdapter = new ArrayObjectAdapter(selector); - mRowsAdapter.add(mPlaybackControlHelper.getControlsRow()); - mRelatedRecordingsRow = getRelatedRecordingsRow(); - setAdapter(mRowsAdapter); - } - - private ListRow getRelatedRecordingsRow() { - mRelatedRecordingCardPresenter = new DvrPlaybackCardPresenter(getActivity()); - mRelatedRecordingsRowAdapter = new RelatedRecordingsAdapter(mRelatedRecordingCardPresenter); - HeaderItem header = new HeaderItem(0, - getActivity().getString(R.string.dvr_playback_related_recordings)); - return new ListRow(header, mRelatedRecordingsRowAdapter); - } - - private RecordedProgram getProgramFromIntent(Intent intent) { - long programId = intent.getLongExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_ID, -1); - return mDvrDataManager.getRecordedProgram(programId); - } - - private long getSeekTimeFromIntent(Intent intent) { - return intent.getLongExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_SEEK_TIME, - TvInputManager.TIME_SHIFT_INVALID_TIME); - } - - private class RelatedRecordingsAdapter extends SortedArrayAdapter { - RelatedRecordingsAdapter(DvrPlaybackCardPresenter presenter) { - super(new SinglePresenterSelector(presenter), BaseProgram.EPISODE_COMPARATOR); - } - - @Override - long getId(BaseProgram item) { - return item.getId(); - } - } -} \ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/DvrPrioritySettingsFragment.java b/src/com/android/tv/dvr/ui/DvrPrioritySettingsFragment.java new file mode 100644 index 00000000..562898a3 --- /dev/null +++ b/src/com/android/tv/dvr/ui/DvrPrioritySettingsFragment.java @@ -0,0 +1,250 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.dvr.ui; + +import android.app.FragmentManager; +import android.content.Context; +import android.graphics.Typeface; +import android.os.Bundle; +import android.support.v17.leanback.app.GuidedStepFragment; +import android.support.v17.leanback.widget.GuidanceStylist.Guidance; +import android.support.v17.leanback.widget.GuidedAction; +import android.support.v17.leanback.widget.GuidedActionsStylist; +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; + +import com.android.tv.R; +import com.android.tv.TvApplication; +import com.android.tv.dvr.DvrDataManager; +import com.android.tv.dvr.DvrManager; +import com.android.tv.dvr.DvrScheduleManager; +import com.android.tv.dvr.data.SeriesRecording; + +import java.util.ArrayList; +import java.util.List; + +/** + * Fragment for DVR series recording settings. + */ +public class DvrPrioritySettingsFragment extends GuidedStepFragment { + /** + * Name of series recording id starting the fragment. + * Type: Long + */ + public static final String COME_FROM_SERIES_RECORDING_ID = "series_recording_id"; + + private static final int ONE_TIME_RECORDING_ID = 0; + // button action's IDs are negative. + private static final long ACTION_ID_SAVE = -100L; + + private final List mSeriesRecordings = new ArrayList<>(); + + private SeriesRecording mSelectedRecording; + private SeriesRecording mComeFromSeriesRecording; + private float mSelectedActionElevation; + private int mActionColor; + private int mSelectedActionColor; + + @Override + public void onAttach(Context context) { + super.onAttach(context); + mSeriesRecordings.clear(); + mSeriesRecordings.add(new SeriesRecording.Builder() + .setTitle(getString(R.string.dvr_priority_action_one_time_recording)) + .setPriority(Long.MAX_VALUE) + .setId(ONE_TIME_RECORDING_ID) + .build()); + DvrDataManager dvrDataManager = TvApplication.getSingletons(context).getDvrDataManager(); + long comeFromSeriesRecordingId = + getArguments().getLong(COME_FROM_SERIES_RECORDING_ID, -1); + for (SeriesRecording series : dvrDataManager.getSeriesRecordings()) { + if (series.getState() == SeriesRecording.STATE_SERIES_NORMAL + || series.getId() == comeFromSeriesRecordingId) { + mSeriesRecordings.add(series); + } + } + mSeriesRecordings.sort(SeriesRecording.PRIORITY_COMPARATOR); + mComeFromSeriesRecording = dvrDataManager.getSeriesRecording(comeFromSeriesRecordingId); + mSelectedActionElevation = getResources().getDimension(R.dimen.card_elevation_normal); + mActionColor = getResources().getColor(R.color.dvr_guided_step_action_text_color, null); + mSelectedActionColor = + getResources().getColor(R.color.dvr_guided_step_action_text_color_selected, null); + } + + @Override + public void onResume() { + super.onResume(); + setSelectedActionPosition(mComeFromSeriesRecording == null ? 1 + : mSeriesRecordings.indexOf(mComeFromSeriesRecording)); + } + + @Override + public Guidance onCreateGuidance(Bundle savedInstanceState) { + String breadcrumb = mComeFromSeriesRecording == null ? null + : mComeFromSeriesRecording.getTitle(); + return new Guidance(getString(R.string.dvr_priority_title), + getString(R.string.dvr_priority_description), breadcrumb, null); + } + + @Override + public void onCreateActions(List actions, Bundle savedInstanceState) { + int position = 0; + for (SeriesRecording seriesRecording : mSeriesRecordings) { + actions.add(new GuidedAction.Builder(getActivity()) + .id(position++) + .title(seriesRecording.getTitle()) + .build()); + } + } + + @Override + public void onCreateButtonActions(List actions, Bundle savedInstanceState) { + actions.add(new GuidedAction.Builder(getActivity()) + .id(ACTION_ID_SAVE) + .title(getString(R.string.dvr_priority_button_action_save)) + .build()); + actions.add(new GuidedAction.Builder(getActivity()) + .clickAction(GuidedAction.ACTION_ID_CANCEL) + .build()); + } + + @Override + public void onGuidedActionClicked(GuidedAction action) { + long actionId = action.getId(); + if (actionId == ACTION_ID_SAVE) { + DvrManager dvrManager = TvApplication.getSingletons(getContext()).getDvrManager(); + int size = mSeriesRecordings.size(); + for (int i = 1; i < size; ++i) { + long priority = DvrScheduleManager.suggestSeriesPriority(size - i); + SeriesRecording seriesRecording = mSeriesRecordings.get(i); + if (seriesRecording.getPriority() != priority) { + dvrManager.updateSeriesRecording(SeriesRecording.buildFrom(seriesRecording) + .setPriority(priority).build()); + } + } + FragmentManager fragmentManager = getFragmentManager(); + fragmentManager.popBackStack(); + } else if (actionId == GuidedAction.ACTION_ID_CANCEL) { + FragmentManager fragmentManager = getFragmentManager(); + fragmentManager.popBackStack(); + } else if (mSelectedRecording == null) { + mSelectedRecording = mSeriesRecordings.get((int) actionId); + for (int i = 0; i < mSeriesRecordings.size(); ++i) { + updateItem(i); + } + } else { + mSelectedRecording = null; + for (int i = 0; i < mSeriesRecordings.size(); ++i) { + updateItem(i); + } + } + } + + @Override + public void onGuidedActionFocused(GuidedAction action) { + super.onGuidedActionFocused(action); + if (mSelectedRecording == null) { + return; + } + if (action.getId() < 0) { + mSelectedRecording = null; + for (int i = 0; i < mSeriesRecordings.size(); ++i) { + updateItem(i); + } + return; + } + int position = (int) action.getId(); + int previousPosition = mSeriesRecordings.indexOf(mSelectedRecording); + mSeriesRecordings.remove(mSelectedRecording); + mSeriesRecordings.add(position, mSelectedRecording); + updateItem(previousPosition); + updateItem(position); + notifyActionChanged(previousPosition); + notifyActionChanged(position); + } + + @Override + public GuidedActionsStylist onCreateButtonActionsStylist() { + return new DvrGuidedActionsStylist(true); + } + + @Override + public GuidedActionsStylist onCreateActionsStylist() { + return new DvrGuidedActionsStylist(false) { + @Override + public void onBindViewHolder(ViewHolder vh, GuidedAction action) { + super.onBindViewHolder(vh, action); + updateItem(vh.itemView, (int) action.getId()); + } + + @Override + public int onProvideItemLayoutId() { + return R.layout.priority_settings_action_item; + } + }; + } + + private void updateItem(int position) { + View itemView = getActionItemView(position); + if (itemView == null) { + return; + } + updateItem(itemView, position); + } + + private void updateItem(View itemView, int position) { + GuidedAction action = getActions().get(position); + action.setTitle(mSeriesRecordings.get(position).getTitle()); + boolean selected = mSelectedRecording != null + && mSeriesRecordings.indexOf(mSelectedRecording) == position; + TextView titleView = (TextView) itemView.findViewById(R.id.guidedactions_item_title); + ImageView imageView = (ImageView) itemView.findViewById(R.id.guidedactions_item_tail_image); + if (position == 0) { + // one-time recording + itemView.setBackgroundResource(R.drawable.setup_selector_background); + imageView.setVisibility(View.GONE); + itemView.setFocusable(false); + itemView.setElevation(0); + // strings.xml tag doesn't work. + titleView.setTypeface(titleView.getTypeface(), Typeface.ITALIC); + } else if (mSelectedRecording == null) { + titleView.setTextColor(mActionColor); + itemView.setBackgroundResource(R.drawable.setup_selector_background); + imageView.setImageResource(R.drawable.ic_draggable_white); + imageView.setVisibility(View.VISIBLE); + itemView.setFocusable(true); + itemView.setElevation(0); + titleView.setTypeface(titleView.getTypeface(), Typeface.NORMAL); + } else if (selected) { + titleView.setTextColor(mSelectedActionColor); + itemView.setBackgroundResource(R.drawable.priority_settings_action_item_selected); + imageView.setImageResource(R.drawable.ic_dragging_grey); + imageView.setVisibility(View.VISIBLE); + itemView.setFocusable(true); + itemView.setElevation(mSelectedActionElevation); + titleView.setTypeface(titleView.getTypeface(), Typeface.NORMAL); + } else { + titleView.setTextColor(mActionColor); + itemView.setBackgroundResource(R.drawable.setup_selector_background); + imageView.setVisibility(View.INVISIBLE); + itemView.setFocusable(true); + itemView.setElevation(0); + titleView.setTypeface(titleView.getTypeface(), Typeface.NORMAL); + } + } +} \ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/DvrScheduleFragment.java b/src/com/android/tv/dvr/ui/DvrScheduleFragment.java index da6d1637..d6008315 100644 --- a/src/com/android/tv/dvr/ui/DvrScheduleFragment.java +++ b/src/com/android/tv/dvr/ui/DvrScheduleFragment.java @@ -32,9 +32,8 @@ import com.android.tv.TvApplication; import com.android.tv.common.SoftPreconditions; import com.android.tv.data.Program; import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.DvrUiHelper; -import com.android.tv.dvr.ScheduledRecording; -import com.android.tv.dvr.SeriesRecording; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.data.SeriesRecording; import com.android.tv.dvr.ui.DvrConflictFragment.DvrProgramConflictFragment; import com.android.tv.util.Utils; @@ -48,18 +47,26 @@ import java.util.List; */ @TargetApi(Build.VERSION_CODES.N) public class DvrScheduleFragment extends DvrGuidedStepFragment { + /** + * Key for the whether to add the current program to series. + * Type: boolean + */ + public static final String KEY_ADD_CURRENT_PROGRAM_TO_SERIES = "add_current_program_to_series"; + private static final String TAG = "DvrScheduleFragment"; private static final int ACTION_RECORD_EPISODE = 1; private static final int ACTION_RECORD_SERIES = 2; private Program mProgram; + private boolean mAddCurrentProgramToSeries; @Override public void onCreate(Bundle savedInstanceState) { Bundle args = getArguments(); if (args != null) { mProgram = args.getParcelable(DvrHalfSizedDialogFragment.KEY_PROGRAM); + mAddCurrentProgramToSeries = args.getBoolean(KEY_ADD_CURRENT_PROGRAM_TO_SERIES, false); } DvrManager dvrManager = TvApplication.getSingletons(getContext()).getDvrManager(); SoftPreconditions.checkArgument(mProgram != null && mProgram.isEpisodic(), TAG, @@ -139,8 +146,10 @@ public class DvrScheduleFragment extends DvrGuidedStepFragment { .build(); getDvrManager().updateSeriesRecording(seriesRecording); } + DvrUiHelper.startSeriesSettingsActivity(getContext(), - seriesRecording.getId(), null, true, true, true); + seriesRecording.getId(), null, true, true, true, + mAddCurrentProgramToSeries ? mProgram : null); dismissDialog(); } } diff --git a/src/com/android/tv/dvr/ui/DvrSchedulesActivity.java b/src/com/android/tv/dvr/ui/DvrSchedulesActivity.java deleted file mode 100644 index f6e6ac26..00000000 --- a/src/com/android/tv/dvr/ui/DvrSchedulesActivity.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.tv.dvr.ui; - -import android.app.Activity; -import android.app.ProgressDialog; -import android.os.Bundle; -import android.support.annotation.IntDef; - -import com.android.tv.R; -import com.android.tv.TvApplication; -import com.android.tv.data.Program; -import com.android.tv.dvr.EpisodicProgramLoadTask; -import com.android.tv.dvr.SeriesRecording; -import com.android.tv.dvr.SeriesRecordingScheduler; -import com.android.tv.dvr.ui.list.DvrSchedulesFragment; -import com.android.tv.dvr.ui.list.DvrSeriesSchedulesFragment; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -/** - * Activity to show the list of recording schedules. - */ -public class DvrSchedulesActivity extends Activity { - /** - * The key for the type of the schedules which will be listed in the list. The type of the value - * should be {@link ScheduleListType}. - */ - public static final String KEY_SCHEDULES_TYPE = "schedules_type"; - - @Retention(RetentionPolicy.SOURCE) - @IntDef({TYPE_FULL_SCHEDULE, TYPE_SERIES_SCHEDULE}) - public @interface ScheduleListType {} - /** - * A type which means the activity will display the full scheduled recordings. - */ - public static final int TYPE_FULL_SCHEDULE = 0; - /** - * A type which means the activity will display a scheduled recording list of a series - * recording. - */ - public static final int TYPE_SERIES_SCHEDULE = 1; - - @Override - public void onCreate(final Bundle savedInstanceState) { - TvApplication.setCurrentRunningProcess(this, true); - // Pass null to prevent automatically re-creating fragments - super.onCreate(null); - setContentView(R.layout.activity_dvr_schedules); - int scheduleType = getIntent().getIntExtra(KEY_SCHEDULES_TYPE, TYPE_FULL_SCHEDULE); - if (scheduleType == TYPE_FULL_SCHEDULE) { - DvrSchedulesFragment schedulesFragment = new DvrSchedulesFragment(); - schedulesFragment.setArguments(getIntent().getExtras()); - getFragmentManager().beginTransaction().add( - R.id.fragment_container, schedulesFragment).commit(); - } else if (scheduleType == TYPE_SERIES_SCHEDULE) { - final ProgressDialog dialog = ProgressDialog.show(this, null, getString( - R.string.dvr_series_schedules_progress_message_reading_programs)); - SeriesRecording seriesRecording = getIntent().getExtras() - .getParcelable(DvrSeriesSchedulesFragment - .SERIES_SCHEDULES_KEY_SERIES_RECORDING); - // To get programs faster, hold the update of the series schedules. - SeriesRecordingScheduler.getInstance(this).pauseUpdate(); - new EpisodicProgramLoadTask(this, Collections.singletonList(seriesRecording)) { - @Override - protected void onPostExecute(List programs) { - SeriesRecordingScheduler.getInstance(DvrSchedulesActivity.this).resumeUpdate(); - dialog.dismiss(); - Bundle args = getIntent().getExtras(); - args.putParcelableArrayList(DvrSeriesSchedulesFragment - .SERIES_SCHEDULES_KEY_SERIES_PROGRAMS, new ArrayList<>(programs)); - DvrSeriesSchedulesFragment schedulesFragment = new DvrSeriesSchedulesFragment(); - schedulesFragment.setArguments(args); - getFragmentManager().beginTransaction().add( - R.id.fragment_container, schedulesFragment).commit(); - } - }.setLoadCurrentProgram(true) - .setLoadDisallowedProgram(true) - .setLoadScheduledEpisode(true) - .setIgnoreChannelOption(true) - .execute(); - } else { - finish(); - } - } -} diff --git a/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java b/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java index f57e4b05..667af34a 100644 --- a/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java +++ b/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java @@ -22,9 +22,6 @@ import android.support.v17.leanback.app.GuidedStepFragment; import com.android.tv.R; import com.android.tv.TvApplication; -import com.android.tv.common.SoftPreconditions; -import com.android.tv.dvr.ui.SeriesDeletionFragment; -import com.android.tv.ui.sidepanel.SettingsFragment; /** * Activity to show details view in DVR. @@ -42,7 +39,7 @@ public class DvrSeriesDeletionActivity extends Activity { setContentView(R.layout.activity_dvr_series_settings); // Check savedInstanceState to prevent that activity is being showed with animation. if (savedInstanceState == null) { - SeriesDeletionFragment deletionFragment = new SeriesDeletionFragment(); + DvrSeriesDeletionFragment deletionFragment = new DvrSeriesDeletionFragment(); deletionFragment.setArguments(getIntent().getExtras()); GuidedStepFragment.addAsRoot(this, deletionFragment, R.id.dvr_settings_view_frame); } diff --git a/src/com/android/tv/dvr/ui/DvrSeriesDeletionFragment.java b/src/com/android/tv/dvr/ui/DvrSeriesDeletionFragment.java new file mode 100644 index 00000000..8bf8560f --- /dev/null +++ b/src/com/android/tv/dvr/ui/DvrSeriesDeletionFragment.java @@ -0,0 +1,253 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.dvr.ui; + +import android.content.Context; +import android.media.tv.TvInputManager; +import android.os.Bundle; +import android.support.v17.leanback.app.GuidedStepFragment; +import android.support.v17.leanback.widget.GuidanceStylist.Guidance; +import android.support.v17.leanback.widget.GuidedAction; +import android.support.v17.leanback.widget.GuidedActionsStylist; +import android.text.TextUtils; +import android.view.ViewGroup.LayoutParams; +import android.widget.Toast; + +import com.android.tv.R; +import com.android.tv.TvApplication; +import com.android.tv.common.SoftPreconditions; +import com.android.tv.dvr.DvrDataManager; +import com.android.tv.dvr.DvrManager; +import com.android.tv.dvr.DvrWatchedPositionManager; +import com.android.tv.dvr.data.RecordedProgram; +import com.android.tv.dvr.data.SeriesRecording; +import com.android.tv.ui.GuidedActionsStylistWithDivider; +import com.android.tv.util.Utils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +/** + * Fragment for DVR series recording settings. + */ +public class DvrSeriesDeletionFragment extends GuidedStepFragment { + private static final long WATCHED_TIME_UNIT_THRESHOLD = TimeUnit.MINUTES.toMillis(2); + + // Since recordings' IDs are used as its check actions' IDs, which are random positive numbers, + // negative values are used by other actions to prevent duplicated IDs. + private static final long ACTION_ID_SELECT_WATCHED = -110; + private static final long ACTION_ID_SELECT_ALL = -111; + private static final long ACTION_ID_DELETE = -112; + + private DvrDataManager mDvrDataManager; + private DvrWatchedPositionManager mDvrWatchedPositionManager; + private List mRecordings; + private final Set mWatchedRecordings = new HashSet<>(); + private boolean mAllSelected; + private long mSeriesRecordingId; + private int mOneLineActionHeight; + + @Override + public void onAttach(Context context) { + super.onAttach(context); + mSeriesRecordingId = getArguments() + .getLong(DvrSeriesDeletionActivity.SERIES_RECORDING_ID, -1); + SoftPreconditions.checkArgument(mSeriesRecordingId != -1); + mDvrDataManager = TvApplication.getSingletons(context).getDvrDataManager(); + mDvrWatchedPositionManager = + TvApplication.getSingletons(context).getDvrWatchedPositionManager(); + mRecordings = mDvrDataManager.getRecordedPrograms(mSeriesRecordingId); + mOneLineActionHeight = getResources().getDimensionPixelSize( + R.dimen.dvr_settings_one_line_action_container_height); + if (mRecordings.isEmpty()) { + Toast.makeText(getActivity(), getString(R.string.dvr_series_deletion_no_recordings), + Toast.LENGTH_LONG).show(); + finishGuidedStepFragments(); + return; + } + Collections.sort(mRecordings, RecordedProgram.EPISODE_COMPARATOR); + } + + @Override + public Guidance onCreateGuidance(Bundle savedInstanceState) { + String breadcrumb = null; + SeriesRecording series = mDvrDataManager.getSeriesRecording(mSeriesRecordingId); + if (series != null) { + breadcrumb = series.getTitle(); + } + return new Guidance(getString(R.string.dvr_series_deletion_title), + getString(R.string.dvr_series_deletion_description), breadcrumb, null); + } + + @Override + public void onCreateActions(List actions, Bundle savedInstanceState) { + actions.add(new GuidedAction.Builder(getActivity()) + .id(ACTION_ID_SELECT_WATCHED) + .title(getString(R.string.dvr_series_select_watched)) + .build()); + actions.add(new GuidedAction.Builder(getActivity()) + .id(ACTION_ID_SELECT_ALL) + .title(getString(R.string.dvr_series_select_all)) + .build()); + actions.add(GuidedActionsStylistWithDivider.createDividerAction(getContext())); + for (RecordedProgram recording : mRecordings) { + long watchedPositionMs = + mDvrWatchedPositionManager.getWatchedPosition(recording.getId()); + String title = recording.getEpisodeDisplayTitle(getContext()); + if (TextUtils.isEmpty(title)) { + title = TextUtils.isEmpty(recording.getTitle()) ? + getString(R.string.channel_banner_no_title) : recording.getTitle(); + } + String description; + if (watchedPositionMs != TvInputManager.TIME_SHIFT_INVALID_TIME) { + description = getWatchedString(watchedPositionMs, recording.getDurationMillis()); + mWatchedRecordings.add(recording.getId()); + } else { + description = getString(R.string.dvr_series_never_watched); + } + actions.add(new GuidedAction.Builder(getActivity()) + .id(recording.getId()) + .title(title) + .description(description) + .checkSetId(GuidedAction.CHECKBOX_CHECK_SET_ID) + .build()); + } + } + + @Override + public void onCreateButtonActions(List actions, Bundle savedInstanceState) { + actions.add(new GuidedAction.Builder(getActivity()) + .id(ACTION_ID_DELETE) + .title(getString(R.string.dvr_detail_delete)) + .build()); + actions.add(new GuidedAction.Builder(getActivity()) + .clickAction(GuidedAction.ACTION_ID_CANCEL) + .build()); + } + + @Override + public void onGuidedActionClicked(GuidedAction action) { + long actionId = action.getId(); + if (actionId == ACTION_ID_DELETE) { + List idsToDelete = new ArrayList<>(); + for (GuidedAction guidedAction : getActions()) { + if (guidedAction.getCheckSetId() == GuidedAction.CHECKBOX_CHECK_SET_ID + && guidedAction.isChecked()) { + idsToDelete.add(guidedAction.getId()); + } + } + if (!idsToDelete.isEmpty()) { + DvrManager dvrManager = TvApplication.getSingletons(getActivity()).getDvrManager(); + dvrManager.removeRecordedPrograms(idsToDelete); + } + Toast.makeText(getContext(), getResources().getQuantityString( + R.plurals.dvr_msg_episodes_deleted, idsToDelete.size(), idsToDelete.size(), + mRecordings.size()), Toast.LENGTH_LONG).show(); + finishGuidedStepFragments(); + } else if (actionId == GuidedAction.ACTION_ID_CANCEL) { + finishGuidedStepFragments(); + } else if (actionId == ACTION_ID_SELECT_WATCHED) { + for (GuidedAction guidedAction : getActions()) { + if (guidedAction.getCheckSetId() == GuidedAction.CHECKBOX_CHECK_SET_ID) { + long recordingId = guidedAction.getId(); + if (mWatchedRecordings.contains(recordingId)) { + guidedAction.setChecked(true); + } else { + guidedAction.setChecked(false); + } + notifyActionChanged(findActionPositionById(recordingId)); + } + } + mAllSelected = updateSelectAllState(); + } else if (actionId == ACTION_ID_SELECT_ALL) { + mAllSelected = !mAllSelected; + for (GuidedAction guidedAction : getActions()) { + if (guidedAction.getCheckSetId() == GuidedAction.CHECKBOX_CHECK_SET_ID) { + guidedAction.setChecked(mAllSelected); + notifyActionChanged(findActionPositionById(guidedAction.getId())); + } + } + updateSelectAllState(action, mAllSelected); + } else { + mAllSelected = updateSelectAllState(); + } + } + + @Override + public GuidedActionsStylist onCreateButtonActionsStylist() { + return new DvrGuidedActionsStylist(true); + } + + @Override + public GuidedActionsStylist onCreateActionsStylist() { + return new GuidedActionsStylistWithDivider() { + @Override + public void onBindViewHolder(ViewHolder vh, GuidedAction action) { + super.onBindViewHolder(vh, action); + if (action.getId() == ACTION_DIVIDER) { + return; + } + LayoutParams lp = vh.itemView.getLayoutParams(); + if (action.getCheckSetId() != GuidedAction.CHECKBOX_CHECK_SET_ID) { + lp.height = mOneLineActionHeight; + } else { + vh.itemView.setLayoutParams( + new LayoutParams(lp.width, LayoutParams.WRAP_CONTENT)); + } + } + }; + } + + private String getWatchedString(long watchedPositionMs, long durationMs) { + if (durationMs > WATCHED_TIME_UNIT_THRESHOLD) { + return getResources().getString(R.string.dvr_series_watched_info_minutes, + Math.max(1, Utils.getRoundOffMinsFromMs(watchedPositionMs)), + Utils.getRoundOffMinsFromMs(durationMs)); + } else { + return getResources().getString(R.string.dvr_series_watched_info_seconds, + Math.max(1, TimeUnit.MILLISECONDS.toSeconds(watchedPositionMs)), + TimeUnit.MILLISECONDS.toSeconds(durationMs)); + } + } + + private boolean updateSelectAllState() { + for (GuidedAction guidedAction : getActions()) { + if (guidedAction.getCheckSetId() == GuidedAction.CHECKBOX_CHECK_SET_ID) { + if (!guidedAction.isChecked()) { + if (mAllSelected) { + updateSelectAllState(findActionById(ACTION_ID_SELECT_ALL), false); + } + return false; + } + } + } + if (!mAllSelected) { + updateSelectAllState(findActionById(ACTION_ID_SELECT_ALL), true); + } + return true; + } + + private void updateSelectAllState(GuidedAction selectAll, boolean select) { + selectAll.setTitle(select ? getString(R.string.dvr_series_deselect_all) + : getString(R.string.dvr_series_select_all)); + notifyActionChanged(findActionPositionById(ACTION_ID_SELECT_ALL)); + } +} diff --git a/src/com/android/tv/dvr/ui/DvrSeriesScheduledFragment.java b/src/com/android/tv/dvr/ui/DvrSeriesScheduledFragment.java index 1173df46..8f880f16 100644 --- a/src/com/android/tv/dvr/ui/DvrSeriesScheduledFragment.java +++ b/src/com/android/tv/dvr/ui/DvrSeriesScheduledFragment.java @@ -25,22 +25,29 @@ import android.support.v17.leanback.widget.GuidedAction; import com.android.tv.R; import com.android.tv.TvApplication; -import com.android.tv.dvr.DvrDataManager; +import com.android.tv.data.Program; import com.android.tv.dvr.DvrScheduleManager; -import com.android.tv.dvr.ScheduledRecording; -import com.android.tv.dvr.SeriesRecording; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.data.SeriesRecording; +import com.android.tv.dvr.ui.list.DvrSchedulesActivity; import com.android.tv.dvr.ui.list.DvrSeriesSchedulesFragment; import java.util.List; public class DvrSeriesScheduledFragment extends DvrGuidedStepFragment { + /** + * The key for program list which will be passed to {@link DvrSeriesSchedulesFragment}. + * Type: List<{@link Program}> + */ + public static final String SERIES_SCHEDULED_KEY_PROGRAMS = "series_scheduled_key_programs"; + private final static long SERIES_RECORDING_ID_NOT_SET = -1; private final static int ACTION_VIEW_SCHEDULES = 1; - private DvrScheduleManager mDvrScheduleManager; private SeriesRecording mSeriesRecording; private boolean mShowViewScheduleOption; + private List mPrograms; private int mSchedulesAddedCount = 0; private boolean mHasConflict = false; @@ -58,22 +65,25 @@ public class DvrSeriesScheduledFragment extends DvrGuidedStepFragment { } mShowViewScheduleOption = getArguments().getBoolean( DvrSeriesScheduledDialogActivity.SHOW_VIEW_SCHEDULE_OPTION); - mDvrScheduleManager = TvApplication.getSingletons(context).getDvrScheduleManager(); mSeriesRecording = TvApplication.getSingletons(context).getDvrDataManager() .getSeriesRecording(seriesRecordingId); if (mSeriesRecording == null) { getActivity().finish(); return; } + mPrograms = (List) BigArguments.getArgument(SERIES_SCHEDULED_KEY_PROGRAMS); + BigArguments.reset(); mSchedulesAddedCount = TvApplication.getSingletons(getContext()).getDvrManager() .getAvailableScheduledRecording(mSeriesRecording.getId()).size(); + DvrScheduleManager dvrScheduleManager = + TvApplication.getSingletons(context).getDvrScheduleManager(); List conflictingRecordings = - mDvrScheduleManager.getConflictingSchedules(mSeriesRecording); + dvrScheduleManager.getConflictingSchedules(mSeriesRecording); mHasConflict = !conflictingRecordings.isEmpty(); for (ScheduledRecording recording : conflictingRecordings) { if (recording.getSeriesRecordingId() == mSeriesRecording.getId()) { ++mInThisSeriesConflictCount; - } else { + } else if (recording.getPriority() < mSeriesRecording.getPriority()) { ++mOutThisSeriesConflictCount; } } @@ -113,6 +123,9 @@ public class DvrSeriesScheduledFragment extends DvrGuidedStepFragment { .TYPE_SERIES_SCHEDULE); intent.putExtra(DvrSeriesSchedulesFragment.SERIES_SCHEDULES_KEY_SERIES_RECORDING, mSeriesRecording); + BigArguments.reset(); + BigArguments.setArgument(DvrSeriesSchedulesFragment + .SERIES_SCHEDULES_KEY_SERIES_PROGRAMS, mPrograms); startActivity(intent); } getActivity().finish(); @@ -121,30 +134,30 @@ public class DvrSeriesScheduledFragment extends DvrGuidedStepFragment { private String getDescription() { if (!mHasConflict) { return getResources().getQuantityString( - R.plurals.dvr_series_recording_scheduled_no_conflict, mSchedulesAddedCount, + R.plurals.dvr_series_scheduled_no_conflict, mSchedulesAddedCount, mSchedulesAddedCount, mSeriesRecording.getTitle()); } else { // mInThisSeriesConflictCount equals 0 and mOutThisSeriesConflictCount equals 0 means // mHasConflict is false. So we don't need to check that case. if (mInThisSeriesConflictCount != 0 && mOutThisSeriesConflictCount != 0) { - return getResources().getQuantityString(R.plurals - .dvr_series_recording_scheduled_this_and_other_series_conflict, + return getResources().getQuantityString( + R.plurals.dvr_series_scheduled_this_and_other_series_conflict, mSchedulesAddedCount, mSchedulesAddedCount, mSeriesRecording.getTitle(), mInThisSeriesConflictCount + mOutThisSeriesConflictCount); } else if (mInThisSeriesConflictCount != 0) { - return getResources().getQuantityString(R.plurals - .dvr_series_recording_scheduled_only_this_series_conflict, + return getResources().getQuantityString( + R.plurals.dvr_series_recording_scheduled_only_this_series_conflict, mSchedulesAddedCount, mSchedulesAddedCount, mSeriesRecording.getTitle(), mInThisSeriesConflictCount); } else { if (mOutThisSeriesConflictCount == 1) { - return getResources().getQuantityString(R.plurals - .dvr_series_recording_scheduled_only_other_series_one_conflict, + return getResources().getQuantityString( + R.plurals.dvr_series_scheduled_only_other_series_one_conflict, mSchedulesAddedCount, mSchedulesAddedCount, mSeriesRecording.getTitle()); } else { - return getResources().getQuantityString(R.plurals - .dvr_series_recording_scheduled_only_other_series_conflict, + return getResources().getQuantityString( + R.plurals.dvr_series_scheduled_only_other_series_many_conflicts, mSchedulesAddedCount, mSchedulesAddedCount, mSeriesRecording.getTitle(), mOutThisSeriesConflictCount); } diff --git a/src/com/android/tv/dvr/ui/DvrSeriesSettingsActivity.java b/src/com/android/tv/dvr/ui/DvrSeriesSettingsActivity.java index 3f7671b3..6dd20b3a 100644 --- a/src/com/android/tv/dvr/ui/DvrSeriesSettingsActivity.java +++ b/src/com/android/tv/dvr/ui/DvrSeriesSettingsActivity.java @@ -17,7 +17,6 @@ package com.android.tv.dvr.ui; import android.app.Activity; -import android.graphics.Color; import android.graphics.drawable.ColorDrawable; import android.os.Bundle; import android.support.v17.leanback.app.GuidedStepFragment; @@ -38,25 +37,34 @@ public class DvrSeriesSettingsActivity extends Activity { /** * Name of the boolean flag to decide if the series recording with empty schedule and recording * will be removed. + * Type: boolean */ public static final String REMOVE_EMPTY_SERIES_RECORDING = "remove_empty_series_recording"; /** * Name of the boolean flag to decide if the setting fragment should be translucent. + * Type: boolean */ public static final String IS_WINDOW_TRANSLUCENT = "windows_translucent"; /** - * Name of the channel id list. If the channel list is given, we show the channels - * from the values in channel option. - * Type: Long array + * Name of the program list. The list contains the programs which belong to the series. + * Type: List<{@link com.android.tv.data.Program}> */ - public static final String CHANNEL_ID_LIST = "channel_id_list"; + public static final String PROGRAM_LIST = "program_list"; /** * Name of the boolean flag to check if the confirm dialog should show view schedule option. + * Type: boolean */ public static final String SHOW_VIEW_SCHEDULE_OPTION_IN_DIALOG = "show_view_schedule_option_in_dialog"; + /** + * Name of the current program added to series. The current program will be recorded only when + * the series recording is initialized from media controller. But for other case, the current + * program won't be recorded. + */ + public static final String CURRENT_PROGRAM = "current_program"; + @Override public void onCreate(Bundle savedInstanceState) { TvApplication.setCurrentRunningProcess(this, true); @@ -66,7 +74,7 @@ public class DvrSeriesSettingsActivity extends Activity { SoftPreconditions.checkArgument(seriesRecordingId != -1); if (savedInstanceState == null) { - SeriesSettingsFragment settingFragment = new SeriesSettingsFragment(); + DvrSeriesSettingsFragment settingFragment = new DvrSeriesSettingsFragment(); settingFragment.setArguments(getIntent().getExtras()); GuidedStepFragment.addAsRoot(this, settingFragment, R.id.dvr_settings_view_frame); } diff --git a/src/com/android/tv/dvr/ui/DvrSeriesSettingsFragment.java b/src/com/android/tv/dvr/ui/DvrSeriesSettingsFragment.java new file mode 100644 index 00000000..f28382da --- /dev/null +++ b/src/com/android/tv/dvr/ui/DvrSeriesSettingsFragment.java @@ -0,0 +1,366 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.dvr.ui; + +import android.app.FragmentManager; +import android.content.Context; +import android.os.Bundle; +import android.support.v17.leanback.app.GuidedStepFragment; +import android.support.v17.leanback.widget.GuidanceStylist.Guidance; +import android.support.v17.leanback.widget.GuidedAction; +import android.support.v17.leanback.widget.GuidedActionsStylist; +import android.util.LongSparseArray; + +import com.android.tv.R; +import com.android.tv.TvApplication; +import com.android.tv.data.Channel; +import com.android.tv.data.ChannelDataManager; +import com.android.tv.data.Program; +import com.android.tv.dvr.DvrDataManager; +import com.android.tv.dvr.DvrManager; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.data.SeasonEpisodeNumber; +import com.android.tv.dvr.data.SeriesRecording; +import com.android.tv.dvr.data.SeriesRecording.ChannelOption; +import com.android.tv.dvr.recorder.SeriesRecordingScheduler; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Fragment for DVR series recording settings. + */ +public class DvrSeriesSettingsFragment extends GuidedStepFragment + implements DvrDataManager.SeriesRecordingListener { + private static final String TAG = "SeriesSettingsFragment"; + private static final boolean DEBUG = false; + + private static final long ACTION_ID_PRIORITY = 10; + private static final long ACTION_ID_CHANNEL = 11; + + private static final long SUB_ACTION_ID_CHANNEL_ALL = 102; + // Each channel's action id = SUB_ACTION_ID_CHANNEL_ONE_BASE + channel id + private static final long SUB_ACTION_ID_CHANNEL_ONE_BASE = 500; + + private DvrDataManager mDvrDataManager; + private SeriesRecording mSeriesRecording; + private long mSeriesRecordingId; + @ChannelOption int mChannelOption; + private long mSelectedChannelId; + private int mBackStackCount; + private boolean mShowViewScheduleOptionInDialog; + private Program mCurrentProgram; + + private String mFragmentTitle; + private String mProrityActionTitle; + private String mProrityActionHighestText; + private String mProrityActionLowestText; + private String mChannelsActionTitle; + private String mChannelsActionAllText; + private LongSparseArray mId2Channel = new LongSparseArray<>(); + private List mChannels = new ArrayList<>(); + private List mPrograms; + + private GuidedAction mPriorityGuidedAction; + private GuidedAction mChannelsGuidedAction; + + @Override + public void onAttach(Context context) { + super.onAttach(context); + mBackStackCount = getFragmentManager().getBackStackEntryCount(); + mDvrDataManager = TvApplication.getSingletons(context).getDvrDataManager(); + mSeriesRecordingId = getArguments().getLong(DvrSeriesSettingsActivity.SERIES_RECORDING_ID); + mSeriesRecording = mDvrDataManager.getSeriesRecording(mSeriesRecordingId); + if (mSeriesRecording == null) { + getActivity().finish(); + return; + } + mShowViewScheduleOptionInDialog = getArguments().getBoolean( + DvrSeriesSettingsActivity.SHOW_VIEW_SCHEDULE_OPTION_IN_DIALOG); + mCurrentProgram = getArguments().getParcelable(DvrSeriesSettingsActivity.CURRENT_PROGRAM); + mDvrDataManager.addSeriesRecordingListener(this); + mPrograms = (List) BigArguments.getArgument( + DvrSeriesSettingsActivity.PROGRAM_LIST); + BigArguments.reset(); + if (mPrograms == null) { + getActivity().finish(); + return; + } + Set channelIds = new HashSet<>(); + ChannelDataManager channelDataManager = + TvApplication.getSingletons(context).getChannelDataManager(); + for (Program program : mPrograms) { + long channelId = program.getChannelId(); + if (channelIds.add(channelId)) { + Channel channel = channelDataManager.getChannel(channelId); + if (channel != null) { + mId2Channel.put(channel.getId(), channel); + mChannels.add(channel); + } + } + } + mChannelOption = mSeriesRecording.getChannelOption(); + mSelectedChannelId = Channel.INVALID_ID; + if (mChannelOption == SeriesRecording.OPTION_CHANNEL_ONE) { + Channel channel = channelDataManager.getChannel(mSeriesRecording.getChannelId()); + if (channel != null) { + mSelectedChannelId = channel.getId(); + } else { + mChannelOption = SeriesRecording.OPTION_CHANNEL_ALL; + } + } + mChannels.sort(Channel.CHANNEL_NUMBER_COMPARATOR); + mFragmentTitle = getString(R.string.dvr_series_settings_title); + mProrityActionTitle = getString(R.string.dvr_series_settings_priority); + mProrityActionHighestText = getString(R.string.dvr_series_settings_priority_highest); + mProrityActionLowestText = getString(R.string.dvr_series_settings_priority_lowest); + mChannelsActionTitle = getString(R.string.dvr_series_settings_channels); + mChannelsActionAllText = getString(R.string.dvr_series_settings_channels_all); + } + + @Override + public void onResume() { + super.onResume(); + // To avoid the order of series's priority has changed, but series doesn't get update. + updatePriorityGuidedAction(); + } + + @Override + public void onDetach() { + super.onDetach(); + mDvrDataManager.removeSeriesRecordingListener(this); + } + + @Override + public void onDestroy() { + if (getFragmentManager().getBackStackEntryCount() == mBackStackCount && getArguments() + .getBoolean(DvrSeriesSettingsActivity.REMOVE_EMPTY_SERIES_RECORDING)) { + mDvrDataManager.checkAndRemoveEmptySeriesRecording(mSeriesRecordingId); + } + super.onDestroy(); + } + + @Override + public Guidance onCreateGuidance(Bundle savedInstanceState) { + String breadcrumb = mSeriesRecording.getTitle(); + String title = mFragmentTitle; + return new Guidance(title, null, breadcrumb, null); + } + + @Override + public void onCreateActions(List actions, Bundle savedInstanceState) { + mPriorityGuidedAction = new GuidedAction.Builder(getActivity()) + .id(ACTION_ID_PRIORITY) + .title(mProrityActionTitle) + .build(); + actions.add(mPriorityGuidedAction); + + mChannelsGuidedAction = new GuidedAction.Builder(getActivity()) + .id(ACTION_ID_CHANNEL) + .title(mChannelsActionTitle) + .subActions(buildChannelSubAction()) + .build(); + actions.add(mChannelsGuidedAction); + updateChannelsGuidedAction(false); + } + + @Override + public void onCreateButtonActions(List actions, Bundle savedInstanceState) { + actions.add(new GuidedAction.Builder(getActivity()) + .clickAction(GuidedAction.ACTION_ID_OK) + .build()); + actions.add(new GuidedAction.Builder(getActivity()) + .clickAction(GuidedAction.ACTION_ID_CANCEL) + .build()); + } + + @Override + public void onGuidedActionClicked(GuidedAction action) { + long actionId = action.getId(); + if (actionId == GuidedAction.ACTION_ID_OK) { + if (mChannelOption != mSeriesRecording.getChannelOption() + || mSeriesRecording.isStopped() + || (mChannelOption == SeriesRecording.OPTION_CHANNEL_ONE + && mSeriesRecording.getChannelId() != mSelectedChannelId)) { + SeriesRecording.Builder builder = SeriesRecording.buildFrom(mSeriesRecording) + .setChannelOption(mChannelOption) + .setState(SeriesRecording.STATE_SERIES_NORMAL); + if (mSelectedChannelId != Channel.INVALID_ID) { + builder.setChannelId(mSelectedChannelId); + } + DvrManager dvrManager = TvApplication.getSingletons(getContext()).getDvrManager(); + dvrManager.updateSeriesRecording(builder.build()); + if (mCurrentProgram != null && (mChannelOption == SeriesRecording.OPTION_CHANNEL_ALL + || mSelectedChannelId == mCurrentProgram.getChannelId())) { + dvrManager.addSchedule(mCurrentProgram); + } + updateSchedulesToSeries(); + showConfirmDialog(); + } else { + showConfirmDialog(); + } + } else if (actionId == GuidedAction.ACTION_ID_CANCEL) { + finishGuidedStepFragments(); + } else if (actionId == ACTION_ID_PRIORITY) { + FragmentManager fragmentManager = getFragmentManager(); + DvrPrioritySettingsFragment fragment = new DvrPrioritySettingsFragment(); + Bundle args = new Bundle(); + args.putLong(DvrPrioritySettingsFragment.COME_FROM_SERIES_RECORDING_ID, + mSeriesRecording.getId()); + fragment.setArguments(args); + GuidedStepFragment.add(fragmentManager, fragment, R.id.dvr_settings_view_frame); + } + } + + @Override + public boolean onSubGuidedActionClicked(GuidedAction action) { + long actionId = action.getId(); + if (actionId == SUB_ACTION_ID_CHANNEL_ALL) { + mChannelOption = SeriesRecording.OPTION_CHANNEL_ALL; + mSelectedChannelId = Channel.INVALID_ID; + updateChannelsGuidedAction(true); + return true; + } else if (actionId > SUB_ACTION_ID_CHANNEL_ONE_BASE) { + mChannelOption = SeriesRecording.OPTION_CHANNEL_ONE; + mSelectedChannelId = actionId - SUB_ACTION_ID_CHANNEL_ONE_BASE; + updateChannelsGuidedAction(true); + return true; + } + return false; + } + + @Override + public GuidedActionsStylist onCreateButtonActionsStylist() { + return new DvrGuidedActionsStylist(true); + } + + private void updateChannelsGuidedAction(boolean notifyActionChanged) { + if (mChannelOption == SeriesRecording.OPTION_CHANNEL_ALL) { + mChannelsGuidedAction.setDescription(mChannelsActionAllText); + } else if (mId2Channel.get(mSelectedChannelId) != null){ + mChannelsGuidedAction.setDescription(mId2Channel.get(mSelectedChannelId) + .getDisplayText()); + } + if (notifyActionChanged) { + notifyActionChanged(findActionPositionById(ACTION_ID_CHANNEL)); + } + } + + private void updatePriorityGuidedAction() { + int totalSeriesCount = 0; + int priorityOrder = 0; + for (SeriesRecording seriesRecording : mDvrDataManager.getSeriesRecordings()) { + if (seriesRecording.getState() == SeriesRecording.STATE_SERIES_NORMAL + || seriesRecording.getId() == mSeriesRecording.getId()) { + ++totalSeriesCount; + } + if (seriesRecording.getState() == SeriesRecording.STATE_SERIES_NORMAL + && seriesRecording.getId() != mSeriesRecording.getId() + && seriesRecording.getPriority() > mSeriesRecording.getPriority()) { + ++priorityOrder; + } + } + if (priorityOrder == 0) { + mPriorityGuidedAction.setDescription(mProrityActionHighestText); + } else if (priorityOrder >= totalSeriesCount - 1) { + mPriorityGuidedAction.setDescription(mProrityActionLowestText); + } else { + mPriorityGuidedAction.setDescription(getString( + R.string.dvr_series_settings_priority_rank, priorityOrder + 1)); + } + notifyActionChanged(findActionPositionById(ACTION_ID_PRIORITY)); + } + + private void updateSchedulesToSeries() { + List recordingCandidates = new ArrayList<>(); + Set scheduledEpisodes = new HashSet<>(); + for (ScheduledRecording r : mDvrDataManager.getScheduledRecordings(mSeriesRecordingId)) { + if (r.getState() != ScheduledRecording.STATE_RECORDING_FAILED + && r.getState() != ScheduledRecording.STATE_RECORDING_CLIPPED) { + scheduledEpisodes.add(new SeasonEpisodeNumber( + r.getSeriesRecordingId(), r.getSeasonNumber(), r.getEpisodeNumber())); + } + } + for (Program program : mPrograms) { + // Removes current programs and scheduled episodes out, matches the channel option. + if (program.getStartTimeUtcMillis() >= System.currentTimeMillis() + && mSeriesRecording.matchProgram(program) + && !scheduledEpisodes.contains(new SeasonEpisodeNumber( + mSeriesRecordingId, program.getSeasonNumber(), program.getEpisodeNumber()))) { + recordingCandidates.add(program); + } + } + if (recordingCandidates.isEmpty()) { + return; + } + List programsToSchedule = SeriesRecordingScheduler.pickOneProgramPerEpisode( + mDvrDataManager, Collections.singletonList(mSeriesRecording), recordingCandidates) + .get(mSeriesRecordingId); + if (!programsToSchedule.isEmpty()) { + TvApplication.getSingletons(getContext()).getDvrManager() + .addScheduleToSeriesRecording(mSeriesRecording, programsToSchedule); + } + } + + private List buildChannelSubAction() { + List channelSubActions = new ArrayList<>(); + channelSubActions.add(new GuidedAction.Builder(getActivity()) + .id(SUB_ACTION_ID_CHANNEL_ALL) + .title(mChannelsActionAllText) + .build()); + for (Channel channel : mChannels) { + channelSubActions.add(new GuidedAction.Builder(getActivity()) + .id(SUB_ACTION_ID_CHANNEL_ONE_BASE + channel.getId()) + .title(channel.getDisplayText()) + .build()); + } + return channelSubActions; + } + + private void showConfirmDialog() { + DvrUiHelper.StartSeriesScheduledDialogActivity(getContext(), mSeriesRecording, + mShowViewScheduleOptionInDialog, mPrograms); + finishGuidedStepFragments(); + } + + @Override + public void onSeriesRecordingAdded(SeriesRecording... seriesRecordings) { } + + @Override + public void onSeriesRecordingRemoved(SeriesRecording... seriesRecordings) { + for (SeriesRecording series : seriesRecordings) { + if (series.getId() == mSeriesRecording.getId()) { + finishGuidedStepFragments(); + return; + } + } + } + + @Override + public void onSeriesRecordingChanged(SeriesRecording... seriesRecordings) { + for (SeriesRecording seriesRecording : seriesRecordings) { + if (seriesRecording.getId() == mSeriesRecordingId) { + mSeriesRecording = seriesRecording; + updatePriorityGuidedAction(); + return; + } + } + } +} \ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java b/src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java index c3867886..b476fff7 100644 --- a/src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java +++ b/src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java @@ -33,7 +33,7 @@ import com.android.tv.data.Channel; import com.android.tv.data.ChannelDataManager; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; -import com.android.tv.dvr.ScheduledRecording; +import com.android.tv.dvr.data.ScheduledRecording; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -131,15 +131,8 @@ public class DvrStopRecordingFragment extends DvrGuidedStepFragment { String title = getString(R.string.dvr_stop_recording_dialog_title); String description; if (mStopReason == REASON_ON_CONFLICT) { - String programTitle = mSchedule.getProgramTitle(); - if (TextUtils.isEmpty(programTitle)) { - ChannelDataManager channelDataManager = - TvApplication.getSingletons(getActivity()).getChannelDataManager(); - Channel channel = channelDataManager.getChannel(mSchedule.getChannelId()); - programTitle = channel.getDisplayName(); - } description = getString(R.string.dvr_stop_recording_dialog_description_on_conflict, - mSchedule.getProgramTitle()); + mSchedule.getProgramDisplayTitle(getContext())); } else { description = getString(R.string.dvr_stop_recording_dialog_description); } diff --git a/src/com/android/tv/dvr/ui/DvrStopSeriesRecordingFragment.java b/src/com/android/tv/dvr/ui/DvrStopSeriesRecordingFragment.java index feaa2357..fe3a4a60 100644 --- a/src/com/android/tv/dvr/ui/DvrStopSeriesRecordingFragment.java +++ b/src/com/android/tv/dvr/ui/DvrStopSeriesRecordingFragment.java @@ -31,8 +31,8 @@ import com.android.tv.R; import com.android.tv.TvApplication; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.ScheduledRecording; -import com.android.tv.dvr.SeriesRecording; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.data.SeriesRecording; import java.util.ArrayList; import java.util.List; diff --git a/src/com/android/tv/dvr/ui/DvrUiHelper.java b/src/com/android/tv/dvr/ui/DvrUiHelper.java new file mode 100644 index 00000000..507db6e7 --- /dev/null +++ b/src/com/android/tv/dvr/ui/DvrUiHelper.java @@ -0,0 +1,575 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.dvr.ui; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.app.ProgressDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.media.tv.TvInputManager; +import android.os.Build; +import android.os.Bundle; +import android.support.annotation.MainThread; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.ActivityOptionsCompat; +import android.widget.ImageView; +import android.widget.Toast; + +import com.android.tv.MainActivity; +import com.android.tv.R; +import com.android.tv.TvApplication; +import com.android.tv.common.SoftPreconditions; +import com.android.tv.data.Channel; +import com.android.tv.data.Program; +import com.android.tv.dialog.HalfSizedDialogFragment; +import com.android.tv.dvr.DvrManager; +import com.android.tv.dvr.DvrStorageStatusManager; +import com.android.tv.dvr.data.RecordedProgram; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.data.SeriesRecording; +import com.android.tv.dvr.provider.EpisodicProgramLoadTask; +import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrAlreadyRecordedDialogFragment; +import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrAlreadyScheduledDialogFragment; +import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrChannelRecordDurationOptionDialogFragment; +import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrChannelWatchConflictDialogFragment; +import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrInsufficientSpaceErrorDialogFragment; +import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrMissingStorageErrorDialogFragment; +import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrNoFreeSpaceErrorDialogFragment; +import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrProgramConflictDialogFragment; +import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrScheduleDialogFragment; +import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrSmallSizedStorageErrorDialogFragment; +import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrStopRecordingDialogFragment; +import com.android.tv.dvr.ui.browse.DvrBrowseActivity; +import com.android.tv.dvr.ui.browse.DvrDetailsActivity; +import com.android.tv.dvr.ui.list.DvrSchedulesActivity; +import com.android.tv.dvr.ui.list.DvrSchedulesFragment; +import com.android.tv.dvr.ui.list.DvrSeriesSchedulesFragment; +import com.android.tv.util.ToastUtils; +import com.android.tv.util.Utils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +/** + * A helper class for DVR UI. + */ +@MainThread +@TargetApi(Build.VERSION_CODES.N) +public class DvrUiHelper { + private static String TAG = "DvrUiHelper"; + + private static ProgressDialog sProgressDialog = null; + + /** + * Checks if the storage status is good for recording and shows error messages if needed. + * + * @param recordingRequestRunnable if the storage status is OK to record or users choose to + * perform the operation anyway, this Runnable will run. + */ + public static void checkStorageStatusAndShowErrorMessage(Activity activity, String inputId, + Runnable recordingRequestRunnable) { + if (Utils.isBundledInput(inputId)) { + switch (TvApplication.getSingletons(activity).getDvrStorageStatusManager() + .getDvrStorageStatus()) { + case DvrStorageStatusManager.STORAGE_STATUS_TOTAL_CAPACITY_TOO_SMALL: + showDvrSmallSizedStorageErrorDialog(activity); + return; + case DvrStorageStatusManager.STORAGE_STATUS_MISSING: + showDvrMissingStorageErrorDialog(activity); + return; + case DvrStorageStatusManager.STORAGE_STATUS_FREE_SPACE_INSUFFICIENT: + showDvrNoFreeSpaceErrorDialog(activity, recordingRequestRunnable); + return; + } + } + recordingRequestRunnable.run(); + } + + /** + * Shows the schedule dialog. + */ + public static void showScheduleDialog(Activity activity, Program program, + boolean addCurrentProgramToSeries) { + if (SoftPreconditions.checkNotNull(program) == null) { + return; + } + Bundle args = new Bundle(); + args.putParcelable(DvrHalfSizedDialogFragment.KEY_PROGRAM, program); + args.putBoolean(DvrScheduleFragment.KEY_ADD_CURRENT_PROGRAM_TO_SERIES, + addCurrentProgramToSeries); + showDialogFragment(activity, new DvrScheduleDialogFragment(), args, true, true); + } + + /** + * Shows the recording duration options dialog. + */ + public static void showChannelRecordDurationOptions(Activity activity, Channel channel) { + if (SoftPreconditions.checkNotNull(channel) == null) { + return; + } + Bundle args = new Bundle(); + args.putLong(DvrHalfSizedDialogFragment.KEY_CHANNEL_ID, channel.getId()); + showDialogFragment(activity, new DvrChannelRecordDurationOptionDialogFragment(), args); + } + + /** + * Shows the dialog which says that the new schedule conflicts with others. + */ + public static void showScheduleConflictDialog(Activity activity, Program program) { + if (program == null) { + return; + } + Bundle args = new Bundle(); + args.putParcelable(DvrHalfSizedDialogFragment.KEY_PROGRAM, program); + showDialogFragment(activity, new DvrProgramConflictDialogFragment(), args, false, true); + } + + /** + * Shows the conflict dialog for the channel watching. + */ + public static void showChannelWatchConflictDialog(MainActivity activity, Channel channel) { + if (channel == null) { + return; + } + Bundle args = new Bundle(); + args.putLong(DvrHalfSizedDialogFragment.KEY_CHANNEL_ID, channel.getId()); + showDialogFragment(activity, new DvrChannelWatchConflictDialogFragment(), args); + } + + /** + * Shows DVR insufficient space error dialog. + */ + public static void showDvrInsufficientSpaceErrorDialog(MainActivity activity, + Set failedScheduledRecordingInfoSet) { + Bundle args = new Bundle(); + ArrayList failedScheduledRecordingInfoArray = + new ArrayList<>(failedScheduledRecordingInfoSet); + args.putStringArrayList(DvrInsufficientSpaceErrorFragment.FAILED_SCHEDULED_RECORDING_INFOS, + failedScheduledRecordingInfoArray); + showDialogFragment(activity, new DvrInsufficientSpaceErrorDialogFragment(), args); + Utils.clearRecordingFailedReason(activity, + TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE); + Utils.clearFailedScheduledRecordingInfoSet(activity); + } + + /** + * Shows DVR no free space error dialog. + * + * @param recordingRequestRunnable the recording request to be executed when users choose + * {@link DvrGuidedStepFragment#ACTION_RECORD_ANYWAY}. + */ + public static void showDvrNoFreeSpaceErrorDialog(Activity activity, + Runnable recordingRequestRunnable) { + DvrHalfSizedDialogFragment fragment = new DvrNoFreeSpaceErrorDialogFragment(); + fragment.setOnActionClickListener(new HalfSizedDialogFragment.OnActionClickListener() { + @Override + public void onActionClick(long actionId) { + if (actionId == DvrGuidedStepFragment.ACTION_RECORD_ANYWAY) { + recordingRequestRunnable.run(); + } else if (actionId == DvrGuidedStepFragment.ACTION_DELETE_RECORDINGS) { + Intent intent = new Intent(activity, DvrBrowseActivity.class); + activity.startActivity(intent); + } + } + }); + showDialogFragment(activity, fragment, null); + } + + /** + * Shows DVR missing storage error dialog. + */ + private static void showDvrMissingStorageErrorDialog(Activity activity) { + showDialogFragment(activity, new DvrMissingStorageErrorDialogFragment(), null); + } + + /** + * Shows DVR small sized storage error dialog. + */ + public static void showDvrSmallSizedStorageErrorDialog(Activity activity) { + showDialogFragment(activity, new DvrSmallSizedStorageErrorDialogFragment(), null); + } + + /** + * Shows stop recording dialog. + */ + public static void showStopRecordingDialog(Activity activity, long channelId, int reason, + HalfSizedDialogFragment.OnActionClickListener listener) { + Bundle args = new Bundle(); + args.putLong(DvrHalfSizedDialogFragment.KEY_CHANNEL_ID, channelId); + args.putInt(DvrStopRecordingFragment.KEY_REASON, reason); + DvrHalfSizedDialogFragment fragment = new DvrStopRecordingDialogFragment(); + fragment.setOnActionClickListener(listener); + showDialogFragment(activity, fragment, args); + } + + /** + * Shows "already scheduled" dialog. + */ + public static void showAlreadyScheduleDialog(Activity activity, Program program) { + if (program == null) { + return; + } + Bundle args = new Bundle(); + args.putParcelable(DvrHalfSizedDialogFragment.KEY_PROGRAM, program); + showDialogFragment(activity, new DvrAlreadyScheduledDialogFragment(), args, false, true); + } + + /** + * Shows "already recorded" dialog. + */ + public static void showAlreadyRecordedDialog(Activity activity, Program program) { + if (program == null) { + return; + } + Bundle args = new Bundle(); + args.putParcelable(DvrHalfSizedDialogFragment.KEY_PROGRAM, program); + showDialogFragment(activity, new DvrAlreadyRecordedDialogFragment(), args, false, true); + } + + /** + * Handle the request of recording a current program. It will handle creating schedules and + * shows the proper dialog and toast message respectively for timed-recording and program + * recording cases. + * + * @param addProgramToSeries denotes whether the program to be recorded should be added into + * the series recording when users choose to record the entire series. + */ + public static void requestRecordingCurrentProgram(Activity activity, + Channel channel, Program program, boolean addProgramToSeries) { + if (program == null) { + DvrUiHelper.showChannelRecordDurationOptions(activity, channel); + } else if (DvrUiHelper.handleCreateSchedule(activity, program, addProgramToSeries)) { + String msg = activity.getString(R.string.dvr_msg_current_program_scheduled, + program.getTitle(), Utils.toTimeString(program.getEndTimeUtcMillis(), false)); + Toast.makeText(activity, msg, Toast.LENGTH_SHORT).show(); + } + } + + /** + * Handle the request of recording a future program. It will handle creating schedules and + * shows the proper toast message. + * + * @param addProgramToSeries denotes whether the program to be recorded should be added into + * the series recording when users choose to record the entire series. + */ + public static void requestRecordingFutureProgram(Activity activity, + Program program, boolean addProgramToSeries) { + if (DvrUiHelper.handleCreateSchedule(activity, program, addProgramToSeries)) { + String msg = activity.getString( + R.string.dvr_msg_program_scheduled, program.getTitle()); + ToastUtils.show(activity, msg, Toast.LENGTH_SHORT); + } + } + + /** + * Handles the action to create the new schedule. It returns {@code true} if the schedule is + * added and there's no additional UI, otherwise {@code false}. + */ + private static boolean handleCreateSchedule(Activity activity, Program program, + boolean addProgramToSeries) { + if (program == null) { + return false; + } + DvrManager dvrManager = TvApplication.getSingletons(activity).getDvrManager(); + if (!program.isEpisodic()) { + // One time recording. + dvrManager.addSchedule(program); + if (!dvrManager.getConflictingSchedules(program).isEmpty()) { + DvrUiHelper.showScheduleConflictDialog(activity, program); + return false; + } + } else { + // Show recorded program rather than the schedule. + RecordedProgram recordedProgram = dvrManager.getRecordedProgram(program.getTitle(), + program.getSeasonNumber(), program.getEpisodeNumber()); + if (recordedProgram != null) { + DvrUiHelper.showAlreadyRecordedDialog(activity, program); + return false; + } + ScheduledRecording duplicate = dvrManager.getScheduledRecording(program.getTitle(), + program.getSeasonNumber(), program.getEpisodeNumber()); + if (duplicate != null + && (duplicate.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED + || duplicate.getState() + == ScheduledRecording.STATE_RECORDING_IN_PROGRESS)) { + DvrUiHelper.showAlreadyScheduleDialog(activity, program); + return false; + } + SeriesRecording seriesRecording = dvrManager.getSeriesRecording(program); + if (seriesRecording == null || seriesRecording.isStopped()) { + DvrUiHelper.showScheduleDialog(activity, program, addProgramToSeries); + return false; + } else { + // Just add the schedule. + dvrManager.addSchedule(program); + } + } + return true; + } + + private static void showDialogFragment(Activity activity, + DvrHalfSizedDialogFragment dialogFragment, Bundle args) { + showDialogFragment(activity, dialogFragment, args, false, false); + } + + private static void showDialogFragment(Activity activity, + DvrHalfSizedDialogFragment dialogFragment, Bundle args, boolean keepSidePanelHistory, + boolean keepProgramGuide) { + dialogFragment.setArguments(args); + if (activity instanceof MainActivity) { + ((MainActivity) activity).getOverlayManager() + .showDialogFragment(DvrHalfSizedDialogFragment.DIALOG_TAG, dialogFragment, + keepSidePanelHistory, keepProgramGuide); + } else { + dialogFragment.show(activity.getFragmentManager(), + DvrHalfSizedDialogFragment.DIALOG_TAG); + } + } + + /** + * Checks whether channel watch conflict dialog is open or not. + */ + public static boolean isChannelWatchConflictDialogShown(MainActivity activity) { + return activity.getOverlayManager().getCurrentDialog() instanceof + DvrChannelWatchConflictDialogFragment; + } + + private static ScheduledRecording getEarliestScheduledRecording(List + recordings) { + ScheduledRecording earlistScheduledRecording = null; + if (!recordings.isEmpty()) { + Collections.sort(recordings, + ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR); + earlistScheduledRecording = recordings.get(0); + } + return earlistScheduledRecording; + } + + /** + * Shows the schedules activity to resolve the tune conflict. + */ + public static void startSchedulesActivityForTuneConflict(Context context, Channel channel) { + if (channel == null) { + return; + } + List conflicts = TvApplication.getSingletons(context).getDvrManager() + .getConflictingSchedulesForTune(channel.getId()); + startSchedulesActivity(context, getEarliestScheduledRecording(conflicts)); + } + + /** + * Shows the schedules activity to resolve the one time recording conflict. + */ + public static void startSchedulesActivityForOneTimeRecordingConflict(Context context, + List conflicts) { + startSchedulesActivity(context, getEarliestScheduledRecording(conflicts)); + } + + /** + * Shows the schedules activity with full schedule. + */ + public static void startSchedulesActivity(Context context, ScheduledRecording + focusedScheduledRecording) { + Intent intent = new Intent(context, DvrSchedulesActivity.class); + intent.putExtra(DvrSchedulesActivity.KEY_SCHEDULES_TYPE, + DvrSchedulesActivity.TYPE_FULL_SCHEDULE); + if (focusedScheduledRecording != null) { + intent.putExtra(DvrSchedulesFragment.SCHEDULES_KEY_SCHEDULED_RECORDING, + focusedScheduledRecording); + } + context.startActivity(intent); + } + + /** + * Shows the schedules activity for series recording. + */ + public static void startSchedulesActivityForSeries(Context context, + SeriesRecording seriesRecording) { + Intent intent = new Intent(context, DvrSchedulesActivity.class); + intent.putExtra(DvrSchedulesActivity.KEY_SCHEDULES_TYPE, + DvrSchedulesActivity.TYPE_SERIES_SCHEDULE); + intent.putExtra(DvrSeriesSchedulesFragment.SERIES_SCHEDULES_KEY_SERIES_RECORDING, + seriesRecording); + context.startActivity(intent); + } + + /** + * Shows the series settings activity. + * + * @param programs list of programs which belong to the series. + */ + public static void startSeriesSettingsActivity(Context context, long seriesRecordingId, + @Nullable List programs, boolean removeEmptySeriesSchedule, + boolean isWindowTranslucent, boolean showViewScheduleOptionInDialog, + Program currentProgram) { + SeriesRecording series = TvApplication.getSingletons(context).getDvrDataManager() + .getSeriesRecording(seriesRecordingId); + if (series == null) { + return; + } + if (programs != null) { + startSeriesSettingsActivityInternal(context, seriesRecordingId, programs, + removeEmptySeriesSchedule, isWindowTranslucent, + showViewScheduleOptionInDialog, currentProgram); + } else { + EpisodicProgramLoadTask episodicProgramLoadTask = + new EpisodicProgramLoadTask(context, series) { + @Override + protected void onPostExecute(List loadedPrograms) { + sProgressDialog.dismiss(); + sProgressDialog = null; + startSeriesSettingsActivityInternal(context, seriesRecordingId, + loadedPrograms == null ? Collections.EMPTY_LIST : loadedPrograms, + removeEmptySeriesSchedule, isWindowTranslucent, + showViewScheduleOptionInDialog, currentProgram); + } + }.setLoadCurrentProgram(true) + .setLoadDisallowedProgram(true) + .setLoadScheduledEpisode(true) + .setIgnoreChannelOption(true); + sProgressDialog = ProgressDialog.show(context, null, context.getString( + R.string.dvr_series_progress_message_reading_programs), true, true, + new DialogInterface.OnCancelListener() { + @Override + public void onCancel(DialogInterface dialogInterface) { + episodicProgramLoadTask.cancel(true); + sProgressDialog = null; + } + }); + episodicProgramLoadTask.execute(); + } + } + + private static void startSeriesSettingsActivityInternal(Context context, long seriesRecordingId, + @NonNull List programs, boolean removeEmptySeriesSchedule, + boolean isWindowTranslucent, boolean showViewScheduleOptionInDialog, + Program currentProgram) { + SoftPreconditions.checkState(programs != null, + TAG, "Start series settings activity but programs is null"); + Intent intent = new Intent(context, DvrSeriesSettingsActivity.class); + intent.putExtra(DvrSeriesSettingsActivity.SERIES_RECORDING_ID, seriesRecordingId); + BigArguments.reset(); + BigArguments.setArgument(DvrSeriesSettingsActivity.PROGRAM_LIST, programs); + intent.putExtra(DvrSeriesSettingsActivity.REMOVE_EMPTY_SERIES_RECORDING, + removeEmptySeriesSchedule); + intent.putExtra(DvrSeriesSettingsActivity.IS_WINDOW_TRANSLUCENT, isWindowTranslucent); + intent.putExtra(DvrSeriesSettingsActivity.SHOW_VIEW_SCHEDULE_OPTION_IN_DIALOG, + showViewScheduleOptionInDialog); + intent.putExtra(DvrSeriesSettingsActivity.CURRENT_PROGRAM, currentProgram); + context.startActivity(intent); + } + + /** + * Shows "series recording scheduled" dialog activity. + */ + public static void StartSeriesScheduledDialogActivity(Context context, + SeriesRecording seriesRecording, boolean showViewScheduleOptionInDialog, + List programs) { + if (seriesRecording == null) { + return; + } + Intent intent = new Intent(context, DvrSeriesScheduledDialogActivity.class); + intent.putExtra(DvrSeriesScheduledDialogActivity.SERIES_RECORDING_ID, + seriesRecording.getId()); + intent.putExtra(DvrSeriesScheduledDialogActivity.SHOW_VIEW_SCHEDULE_OPTION, + showViewScheduleOptionInDialog); + BigArguments.reset(); + BigArguments.setArgument(DvrSeriesScheduledFragment.SERIES_SCHEDULED_KEY_PROGRAMS, + programs); + context.startActivity(intent); + } + + /** + * Shows the details activity for the DVR items. The type of DVR items may be + * {@link ScheduledRecording}, {@link RecordedProgram}, or {@link SeriesRecording}. + */ + public static void startDetailsActivity(Activity activity, Object dvrItem, + @Nullable ImageView imageView, boolean hideViewSchedule) { + if (dvrItem == null) { + return; + } + Intent intent = new Intent(activity, DvrDetailsActivity.class); + long recordingId; + int viewType; + if (dvrItem instanceof ScheduledRecording) { + ScheduledRecording schedule = (ScheduledRecording) dvrItem; + recordingId = schedule.getId(); + if (schedule.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED) { + viewType = DvrDetailsActivity.SCHEDULED_RECORDING_VIEW; + } else if (schedule.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) { + viewType = DvrDetailsActivity.CURRENT_RECORDING_VIEW; + } else { + return; + } + } else if (dvrItem instanceof RecordedProgram) { + recordingId = ((RecordedProgram) dvrItem).getId(); + viewType = DvrDetailsActivity.RECORDED_PROGRAM_VIEW; + } else if (dvrItem instanceof SeriesRecording) { + recordingId = ((SeriesRecording) dvrItem).getId(); + viewType = DvrDetailsActivity.SERIES_RECORDING_VIEW; + } else { + return; + } + intent.putExtra(DvrDetailsActivity.RECORDING_ID, recordingId); + intent.putExtra(DvrDetailsActivity.DETAILS_VIEW_TYPE, viewType); + intent.putExtra(DvrDetailsActivity.HIDE_VIEW_SCHEDULE, hideViewSchedule); + Bundle bundle = null; + if (imageView != null) { + bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(activity, imageView, + DvrDetailsActivity.SHARED_ELEMENT_NAME).toBundle(); + } + activity.startActivity(intent, bundle); + } + + /** + * Shows the cancel all dialog for series schedules list. + */ + public static void showCancelAllSeriesRecordingDialog(DvrSchedulesActivity activity, + SeriesRecording seriesRecording) { + DvrStopSeriesRecordingDialogFragment dvrStopSeriesRecordingDialogFragment = + new DvrStopSeriesRecordingDialogFragment(); + Bundle arguments = new Bundle(); + arguments.putParcelable(DvrStopSeriesRecordingFragment.KEY_SERIES_RECORDING, + seriesRecording); + dvrStopSeriesRecordingDialogFragment.setArguments(arguments); + dvrStopSeriesRecordingDialogFragment.show(activity.getFragmentManager(), + DvrStopSeriesRecordingDialogFragment.DIALOG_TAG); + } + + /** + * Shows the series deletion activity. + */ + public static void startSeriesDeletionActivity(Context context, long seriesRecordingId) { + Intent intent = new Intent(context, DvrSeriesDeletionActivity.class); + intent.putExtra(DvrSeriesDeletionActivity.SERIES_RECORDING_ID, seriesRecordingId); + context.startActivity(intent); + } + + public static void showAddScheduleToast(Context context, + String title, long startTimeMs, long endTimeMs) { + String msg = (startTimeMs > System.currentTimeMillis()) ? + context.getString(R.string.dvr_msg_program_scheduled, title) + : context.getString(R.string.dvr_msg_current_program_scheduled, title, + Utils.toTimeString(endTimeMs, false)); + Toast.makeText(context, msg, Toast.LENGTH_SHORT).show(); + } +} \ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/FadeBackground.java b/src/com/android/tv/dvr/ui/FadeBackground.java new file mode 100644 index 00000000..4f06ebcf --- /dev/null +++ b/src/com/android/tv/dvr/ui/FadeBackground.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.dvr.ui; + +import android.animation.Animator; +import android.animation.ObjectAnimator; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.transition.Transition; +import android.transition.TransitionValues; +import android.transition.Visibility; +import android.util.AttributeSet; +import android.view.ViewGroup; + +import com.android.tv.R; + +/** + * This transition fades in/out of the background of the view by changing the background color. + */ +public class FadeBackground extends Transition { + private final int mMode; + + public FadeBackground(Context context, AttributeSet attrs) { + super(context, attrs); + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FadeBackground); + mMode = a.getInt(R.styleable.FadeBackground_fadingMode, Visibility.MODE_IN); + a.recycle(); + } + + @Override + public void captureStartValues(TransitionValues transitionValues) { } + + @Override + public void captureEndValues(TransitionValues transitionValues) { } + + @Override + public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, + TransitionValues endValues) { + if (startValues == null || endValues == null) { + return null; + } + Drawable background = endValues.view.getBackground(); + if (background instanceof ColorDrawable) { + int color = ((ColorDrawable) background).getColor(); + int transparentColor = Color.argb(0, Color.red(color), Color.green(color), + Color.blue(color)); + return mMode == Visibility.MODE_OUT + ? ObjectAnimator.ofArgb(background, "color", transparentColor) + : ObjectAnimator.ofArgb(background, "color", transparentColor, color); + } + return null; + } +} diff --git a/src/com/android/tv/dvr/ui/FullScheduleCardHolder.java b/src/com/android/tv/dvr/ui/FullScheduleCardHolder.java deleted file mode 100644 index d4d4d8ab..00000000 --- a/src/com/android/tv/dvr/ui/FullScheduleCardHolder.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.dvr.ui; - -/** - * Special object for schedule preview; - */ -final class FullScheduleCardHolder { - /** - * Full schedule card holder. - */ - static final FullScheduleCardHolder FULL_SCHEDULE_CARD_HOLDER = new FullScheduleCardHolder(); - - private FullScheduleCardHolder() { } -} diff --git a/src/com/android/tv/dvr/ui/FullSchedulesCardPresenter.java b/src/com/android/tv/dvr/ui/FullSchedulesCardPresenter.java deleted file mode 100644 index 7dd85f45..00000000 --- a/src/com/android/tv/dvr/ui/FullSchedulesCardPresenter.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.dvr.ui; - -import android.content.Context; -import android.support.v17.leanback.widget.Presenter; -import android.view.View; -import android.view.ViewGroup; - -import com.android.tv.R; -import com.android.tv.TvApplication; -import com.android.tv.dvr.DvrUiHelper; -import com.android.tv.dvr.ScheduledRecording; -import com.android.tv.util.Utils; - -import java.util.Collections; -import java.util.List; - -/** - * Presents a {@link ScheduledRecording} in the {@link DvrBrowseFragment}. - */ -public class FullSchedulesCardPresenter extends Presenter { - @Override - public ViewHolder onCreateViewHolder(ViewGroup parent) { - Context context = parent.getContext(); - RecordingCardView view = new RecordingCardView(context); - return new ScheduledRecordingViewHolder(view); - } - - @Override - public void onBindViewHolder(ViewHolder baseHolder, Object o) { - final ScheduledRecordingViewHolder viewHolder = (ScheduledRecordingViewHolder) baseHolder; - final RecordingCardView cardView = (RecordingCardView) viewHolder.view; - final Context context = viewHolder.view.getContext(); - - cardView.setImage(context.getDrawable(R.drawable.dvr_full_schedule)); - cardView.setTitle(context.getString(R.string.dvr_full_schedule_card_view_title)); - List scheduledRecordings = TvApplication.getSingletons(context) - .getDvrDataManager().getAvailableScheduledRecordings(); - int fullDays = 0; - if (!scheduledRecordings.isEmpty()) { - fullDays = Utils.computeDateDifference(System.currentTimeMillis(), - Collections.max(scheduledRecordings, ScheduledRecording.START_TIME_COMPARATOR) - .getStartTimeMs()) + 1; - } - cardView.setContent(context.getResources().getQuantityString( - R.plurals.dvr_full_schedule_card_view_content, fullDays, fullDays), null); - - View.OnClickListener clickListener = new View.OnClickListener() { - @Override - public void onClick(View v) { - DvrUiHelper.startSchedulesActivity(context, null); - } - }; - baseHolder.view.setOnClickListener(clickListener); - } - - @Override - public void onUnbindViewHolder(ViewHolder baseHolder) { - ScheduledRecordingViewHolder viewHolder = (ScheduledRecordingViewHolder) baseHolder; - final RecordingCardView cardView = (RecordingCardView) viewHolder.view; - cardView.reset(); - } - - private static final class ScheduledRecordingViewHolder extends ViewHolder { - ScheduledRecordingViewHolder(RecordingCardView view) { - super(view); - } - } -} diff --git a/src/com/android/tv/dvr/ui/HalfSizedDialogFragment.java b/src/com/android/tv/dvr/ui/HalfSizedDialogFragment.java deleted file mode 100644 index d320816e..00000000 --- a/src/com/android/tv/dvr/ui/HalfSizedDialogFragment.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.dvr.ui; - -import android.content.DialogInterface; -import android.os.Bundle; -import android.os.Handler; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import com.android.tv.R; -import com.android.tv.dialog.SafeDismissDialogFragment; - -import java.util.concurrent.TimeUnit; - -public class HalfSizedDialogFragment extends SafeDismissDialogFragment { - public static final String DIALOG_TAG = HalfSizedDialogFragment.class.getSimpleName(); - public static final String TRACKER_LABEL = "Half sized dialog"; - - private static final long AUTO_DISMISS_TIME_THRESHOLD_MS = TimeUnit.SECONDS.toMillis(30); - - private OnActionClickListener mOnActionClickListener; - - private Handler mHandler = new Handler(); - private Runnable mAutoDismisser = new Runnable() { - @Override - public void run() { - dismiss(); - } - }; - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - return inflater.inflate(R.layout.halfsized_dialog, container, false); - } - - @Override - public void onStart() { - super.onStart(); - getDialog().setOnKeyListener(new DialogInterface.OnKeyListener() { - public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent keyEvent) { - mHandler.removeCallbacks(mAutoDismisser); - mHandler.postDelayed(mAutoDismisser, AUTO_DISMISS_TIME_THRESHOLD_MS); - return false; - } - }); - mHandler.postDelayed(mAutoDismisser, AUTO_DISMISS_TIME_THRESHOLD_MS); - } - - @Override - public void onPause() { - super.onPause(); - if (mOnActionClickListener != null) { - // Dismisses the dialog to prevent the callback being forgotten during - // fragment re-creating. - dismiss(); - } - } - - @Override - public void onStop() { - super.onStop(); - mHandler.removeCallbacks(mAutoDismisser); - } - - @Override - public int getTheme() { - return R.style.Theme_TV_dialog_HalfSizedDialog; - } - - @Override - public String getTrackerLabel() { - return TRACKER_LABEL; - } - - /** - * Sets {@link OnActionClickListener} for the dialog fragment. If listener is set, the dialog - * will be automatically closed when it's paused to prevent the fragment being re-created by - * the framework, which will result the listener being forgotten. - */ - public void setOnActionClickListener(OnActionClickListener listener) { - mOnActionClickListener = listener; - } - - /** - * Returns {@link OnActionClickListener} for sub-classes or any inner fragments. - */ - protected OnActionClickListener getOnActionClickListener() { - return mOnActionClickListener; - } - - /** - * An interface to provide callbacks for half-sized dialogs. Subclasses or inner fragments - * should invoke {@link OnActionClickListener#onActionClick(long)} and provide the identifier - * of the action user clicked. - */ - public interface OnActionClickListener { - void onActionClick(long actionId); - } -} \ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/PrioritySettingsFragment.java b/src/com/android/tv/dvr/ui/PrioritySettingsFragment.java deleted file mode 100644 index 158bd824..00000000 --- a/src/com/android/tv/dvr/ui/PrioritySettingsFragment.java +++ /dev/null @@ -1,251 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.dvr.ui; - -import android.app.FragmentManager; -import android.content.Context; -import android.graphics.Typeface; -import android.os.Bundle; -import android.support.v17.leanback.app.GuidedStepFragment; -import android.support.v17.leanback.widget.GuidanceStylist.Guidance; -import android.support.v17.leanback.widget.GuidedAction; -import android.support.v17.leanback.widget.GuidedActionsStylist; -import android.view.View; -import android.widget.ImageView; -import android.widget.TextView; - -import com.android.tv.R; -import com.android.tv.TvApplication; -import com.android.tv.dvr.DvrDataManager; -import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.DvrScheduleManager; -import com.android.tv.dvr.SeriesRecording; - -import java.util.ArrayList; -import java.util.List; - -/** - * Fragment for DVR series recording settings. - */ -public class PrioritySettingsFragment extends GuidedStepFragment { - /** - * Name of series recording id starting the fragment. - * Type: Long - */ - public static final String COME_FROM_SERIES_RECORDING_ID = "series_recording_id"; - - private static final int ONE_TIME_RECORDING_ID = 0; - // button action's IDs are negative. - private static final long ACTION_ID_SAVE = -100L; - - private final List mSeriesRecordings = new ArrayList<>(); - - private SeriesRecording mSelectedRecording; - private SeriesRecording mComeFromSeriesRecording; - private float mSelectedActionElevation; - private int mActionColor; - private int mSelectedActionColor; - - @Override - public void onAttach(Context context) { - super.onAttach(context); - mSeriesRecordings.clear(); - mSeriesRecordings.add(new SeriesRecording.Builder() - .setTitle(getString(R.string.dvr_priority_action_one_time_recording)) - .setPriority(Long.MAX_VALUE) - .setId(ONE_TIME_RECORDING_ID) - .build()); - DvrDataManager dvrDataManager = TvApplication.getSingletons(context).getDvrDataManager(); - long comeFromSeriesRecordingId = - getArguments().getLong(COME_FROM_SERIES_RECORDING_ID, -1); - for (SeriesRecording series : dvrDataManager.getSeriesRecordings()) { - if (series.getState() == SeriesRecording.STATE_SERIES_NORMAL - || series.getId() == comeFromSeriesRecordingId) { - mSeriesRecordings.add(series); - } - } - mSeriesRecordings.sort(SeriesRecording.PRIORITY_COMPARATOR); - mComeFromSeriesRecording = dvrDataManager.getSeriesRecording(comeFromSeriesRecordingId); - mSelectedActionElevation = getResources().getDimension(R.dimen.card_elevation_normal); - mActionColor = getResources().getColor(R.color.dvr_guided_step_action_text_color, null); - mSelectedActionColor = - getResources().getColor(R.color.dvr_guided_step_action_text_color_selected, null); - } - - @Override - public void onResume() { - super.onResume(); - setSelectedActionPosition(mComeFromSeriesRecording == null ? 1 - : mSeriesRecordings.indexOf(mComeFromSeriesRecording)); - } - - @Override - public Guidance onCreateGuidance(Bundle savedInstanceState) { - String breadcrumb = mComeFromSeriesRecording == null ? null - : mComeFromSeriesRecording.getTitle(); - return new Guidance(getString(R.string.dvr_priority_title), - getString(R.string.dvr_priority_description), breadcrumb, null); - } - - @Override - public void onCreateActions(List actions, Bundle savedInstanceState) { - int position = 0; - for (SeriesRecording seriesRecording : mSeriesRecordings) { - actions.add(new GuidedAction.Builder(getActivity()) - .id(position++) - .title(seriesRecording.getTitle()) - .build()); - } - } - - @Override - public void onCreateButtonActions(List actions, Bundle savedInstanceState) { - actions.add(new GuidedAction.Builder(getActivity()) - .id(ACTION_ID_SAVE) - .title(getString(R.string.dvr_priority_button_action_save)) - .build()); - actions.add(new GuidedAction.Builder(getActivity()) - .clickAction(GuidedAction.ACTION_ID_CANCEL) - .build()); - } - - @Override - public void onGuidedActionClicked(GuidedAction action) { - long actionId = action.getId(); - if (actionId == ACTION_ID_SAVE) { - DvrManager dvrManager = TvApplication.getSingletons(getContext()).getDvrManager(); - int size = mSeriesRecordings.size(); - for (int i = 1; i < size; ++i) { - long priority = DvrScheduleManager.suggestSeriesPriority(size - i); - SeriesRecording seriesRecording = mSeriesRecordings.get(i); - if (seriesRecording.getPriority() != priority) { - dvrManager.updateSeriesRecording(SeriesRecording.buildFrom(seriesRecording) - .setPriority(priority).build()); - } - } - FragmentManager fragmentManager = getFragmentManager(); - fragmentManager.popBackStack(); - } else if (actionId == GuidedAction.ACTION_ID_CANCEL) { - FragmentManager fragmentManager = getFragmentManager(); - fragmentManager.popBackStack(); - } else if (mSelectedRecording == null) { - mSelectedRecording = mSeriesRecordings.get((int) actionId); - for (int i = 0; i < mSeriesRecordings.size(); ++i) { - updateItem(i); - } - } else { - mSelectedRecording = null; - for (int i = 0; i < mSeriesRecordings.size(); ++i) { - updateItem(i); - } - } - } - - @Override - public void onGuidedActionFocused(GuidedAction action) { - super.onGuidedActionFocused(action); - if (mSelectedRecording == null) { - return; - } - if (action.getId() < 0) { - int selectedPosition = mSeriesRecordings.indexOf(mSelectedRecording); - mSelectedRecording = null; - for (int i = 0; i < mSeriesRecordings.size(); ++i) { - updateItem(i); - } - return; - } - int position = (int) action.getId(); - int previousPosition = mSeriesRecordings.indexOf(mSelectedRecording); - mSeriesRecordings.remove(mSelectedRecording); - mSeriesRecordings.add(position, mSelectedRecording); - updateItem(previousPosition); - updateItem(position); - notifyActionChanged(previousPosition); - notifyActionChanged(position); - } - - @Override - public GuidedActionsStylist onCreateButtonActionsStylist() { - return new DvrGuidedActionsStylist(true); - } - - @Override - public GuidedActionsStylist onCreateActionsStylist() { - return new DvrGuidedActionsStylist(false) { - @Override - public void onBindViewHolder(ViewHolder vh, GuidedAction action) { - super.onBindViewHolder(vh, action); - updateItem(vh.itemView, (int) action.getId()); - } - - @Override - public int onProvideItemLayoutId() { - return R.layout.priority_settings_action_item; - } - }; - } - - private void updateItem(int position) { - View itemView = getActionItemView(position); - if (itemView == null) { - return; - } - updateItem(itemView, position); - } - - private void updateItem(View itemView, int position) { - GuidedAction action = getActions().get(position); - action.setTitle(mSeriesRecordings.get(position).getTitle()); - boolean selected = mSelectedRecording != null - && mSeriesRecordings.indexOf(mSelectedRecording) == position; - TextView titleView = (TextView) itemView.findViewById(R.id.guidedactions_item_title); - ImageView imageView = (ImageView) itemView.findViewById(R.id.guidedactions_item_tail_image); - if (position == 0) { - // one-time recording - itemView.setBackgroundResource(R.drawable.setup_selector_background); - imageView.setVisibility(View.GONE); - itemView.setFocusable(false); - itemView.setElevation(0); - // strings.xml tag doesn't work. - titleView.setTypeface(titleView.getTypeface(), Typeface.ITALIC); - } else if (mSelectedRecording == null) { - titleView.setTextColor(mActionColor); - itemView.setBackgroundResource(R.drawable.setup_selector_background); - imageView.setImageResource(R.drawable.ic_draggable_white); - imageView.setVisibility(View.VISIBLE); - itemView.setFocusable(true); - itemView.setElevation(0); - titleView.setTypeface(titleView.getTypeface(), Typeface.NORMAL); - } else if (selected) { - titleView.setTextColor(mSelectedActionColor); - itemView.setBackgroundResource(R.drawable.priority_settings_action_item_selected); - imageView.setImageResource(R.drawable.ic_dragging_grey); - imageView.setVisibility(View.VISIBLE); - itemView.setFocusable(true); - itemView.setElevation(mSelectedActionElevation); - titleView.setTypeface(titleView.getTypeface(), Typeface.NORMAL); - } else { - titleView.setTextColor(mActionColor); - itemView.setBackgroundResource(R.drawable.setup_selector_background); - imageView.setVisibility(View.INVISIBLE); - itemView.setFocusable(true); - itemView.setElevation(0); - titleView.setTypeface(titleView.getTypeface(), Typeface.NORMAL); - } - } -} diff --git a/src/com/android/tv/dvr/ui/RecordedProgramDetailsFragment.java b/src/com/android/tv/dvr/ui/RecordedProgramDetailsFragment.java deleted file mode 100644 index e698b8a2..00000000 --- a/src/com/android/tv/dvr/ui/RecordedProgramDetailsFragment.java +++ /dev/null @@ -1,170 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.tv.dvr.ui; - -import android.content.res.Resources; -import android.media.tv.TvInputManager; -import android.os.Bundle; -import android.support.v17.leanback.widget.Action; -import android.support.v17.leanback.widget.OnActionClickedListener; -import android.support.v17.leanback.widget.SparseArrayObjectAdapter; -import android.text.TextUtils; - -import com.android.tv.R; -import com.android.tv.TvApplication; -import com.android.tv.data.Channel; -import com.android.tv.dvr.DvrDataManager; -import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.DvrWatchedPositionManager; -import com.android.tv.dvr.RecordedProgram; - -/** - * {@link DetailsFragment} for recorded program in DVR. - */ -public class RecordedProgramDetailsFragment extends DvrDetailsFragment - implements DvrDataManager.RecordedProgramListener { - private static final int ACTION_RESUME_PLAYING = 1; - private static final int ACTION_PLAY_FROM_BEGINNING = 2; - private static final int ACTION_DELETE_RECORDING = 3; - - private DvrWatchedPositionManager mDvrWatchedPositionManager; - - private RecordedProgram mRecordedProgram; - private DetailsContent mDetailsContent; - private boolean mPaused; - private DvrDataManager mDvrDataManager; - - @Override - public void onCreate(Bundle savedInstanceState) { - mDvrDataManager = TvApplication.getSingletons(getContext()).getDvrDataManager(); - mDvrDataManager.addRecordedProgramListener(this); - super.onCreate(savedInstanceState); - } - - @Override - public void onCreateInternal() { - mDvrWatchedPositionManager = TvApplication.getSingletons(getActivity()) - .getDvrWatchedPositionManager(); - setDetailsOverviewRow(mDetailsContent); - } - - @Override - public void onResume() { - super.onResume(); - if (mPaused) { - updateActions(); - mPaused = false; - } - } - - @Override - public void onPause() { - super.onPause(); - mPaused = true; - } - - @Override - public void onDestroy() { - mDvrDataManager.removeRecordedProgramListener(this); - super.onDestroy(); - } - - @Override - protected boolean onLoadRecordingDetails(Bundle args) { - long recordedProgramId = args.getLong(DvrDetailsActivity.RECORDING_ID); - mRecordedProgram = mDvrDataManager.getRecordedProgram(recordedProgramId); - if (mRecordedProgram == null) { - // notify super class to end activity before initializing anything - return false; - } - mDetailsContent = createDetailsContent(); - return true; - } - - private DetailsContent createDetailsContent() { - Channel channel = TvApplication.getSingletons(getContext()).getChannelDataManager() - .getChannel(mRecordedProgram.getChannelId()); - String description = TextUtils.isEmpty(mRecordedProgram.getLongDescription()) - ? mRecordedProgram.getDescription() : mRecordedProgram.getLongDescription(); - return new DetailsContent.Builder() - .setTitle(getTitleFromProgram(mRecordedProgram, channel)) - .setStartTimeUtcMillis(mRecordedProgram.getStartTimeUtcMillis()) - .setEndTimeUtcMillis(mRecordedProgram.getEndTimeUtcMillis()) - .setDescription(description) - .setImageUris(mRecordedProgram, channel) - .build(); - } - - @Override - protected SparseArrayObjectAdapter onCreateActionsAdapter() { - SparseArrayObjectAdapter adapter = - new SparseArrayObjectAdapter(new ActionPresenterSelector()); - Resources res = getResources(); - if (mDvrWatchedPositionManager.getWatchedStatus(mRecordedProgram) - == DvrWatchedPositionManager.DVR_WATCHED_STATUS_WATCHING) { - adapter.set(ACTION_RESUME_PLAYING, new Action(ACTION_RESUME_PLAYING, - res.getString(R.string.dvr_detail_resume_play), null, - res.getDrawable(R.drawable.lb_ic_play))); - adapter.set(ACTION_PLAY_FROM_BEGINNING, new Action(ACTION_PLAY_FROM_BEGINNING, - res.getString(R.string.dvr_detail_play_from_beginning), null, - res.getDrawable(R.drawable.lb_ic_replay))); - } else { - adapter.set(ACTION_PLAY_FROM_BEGINNING, new Action(ACTION_PLAY_FROM_BEGINNING, - res.getString(R.string.dvr_detail_watch), null, - res.getDrawable(R.drawable.lb_ic_play))); - } - adapter.set(ACTION_DELETE_RECORDING, new Action(ACTION_DELETE_RECORDING, - res.getString(R.string.dvr_detail_delete), null, - res.getDrawable(R.drawable.ic_delete_32dp))); - return adapter; - } - - @Override - protected OnActionClickedListener onCreateOnActionClickedListener() { - return new OnActionClickedListener() { - @Override - public void onActionClicked(Action action) { - if (action.getId() == ACTION_PLAY_FROM_BEGINNING) { - startPlayback(mRecordedProgram, TvInputManager.TIME_SHIFT_INVALID_TIME); - } else if (action.getId() == ACTION_RESUME_PLAYING) { - startPlayback(mRecordedProgram, mDvrWatchedPositionManager - .getWatchedPosition(mRecordedProgram.getId())); - } else if (action.getId() == ACTION_DELETE_RECORDING) { - DvrManager dvrManager = TvApplication - .getSingletons(getActivity()).getDvrManager(); - dvrManager.removeRecordedProgram(mRecordedProgram); - getActivity().finish(); - } - } - }; - } - - @Override - public void onRecordedProgramsAdded(RecordedProgram... recordedPrograms) { } - - @Override - public void onRecordedProgramsChanged(RecordedProgram... recordedPrograms) { } - - @Override - public void onRecordedProgramsRemoved(RecordedProgram... recordedPrograms) { - for (RecordedProgram recordedProgram : recordedPrograms) { - if (recordedProgram.getId() == mRecordedProgram.getId()) { - getActivity().finish(); - } - } - } -} diff --git a/src/com/android/tv/dvr/ui/RecordedProgramPresenter.java b/src/com/android/tv/dvr/ui/RecordedProgramPresenter.java deleted file mode 100644 index 1bf34310..00000000 --- a/src/com/android/tv/dvr/ui/RecordedProgramPresenter.java +++ /dev/null @@ -1,182 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.dvr.ui; - -import android.app.Activity; -import android.content.Context; -import android.media.tv.TvContract; -import android.media.tv.TvInputManager; -import android.net.Uri; -import android.text.Spannable; -import android.text.SpannableString; -import android.text.TextUtils; -import android.text.style.TextAppearanceSpan; -import android.view.View; -import android.view.ViewGroup; - -import com.android.tv.R; -import com.android.tv.TvApplication; -import com.android.tv.dvr.RecordedProgram; -import com.android.tv.data.Channel; -import com.android.tv.data.ChannelDataManager; -import com.android.tv.dvr.DvrWatchedPositionManager; -import com.android.tv.dvr.DvrWatchedPositionManager.WatchedPositionChangedListener; -import com.android.tv.util.Utils; - -import java.util.concurrent.TimeUnit; - -/** - * Presents a {@link RecordedProgram} in the {@link DvrBrowseFragment}. - */ -public class RecordedProgramPresenter extends DvrItemPresenter { - private final ChannelDataManager mChannelDataManager; - private final DvrWatchedPositionManager mDvrWatchedPositionManager; - private final Context mContext; - private String mTodayString; - private String mYesterdayString; - private final int mProgressBarColor; - private final boolean mShowEpisodeTitle; - - private static final class RecordedProgramViewHolder extends ViewHolder - implements WatchedPositionChangedListener { - private RecordedProgram mProgram; - - RecordedProgramViewHolder(RecordingCardView view, int progressColor) { - super(view); - view.setProgressBarColor(progressColor); - } - - private void setProgram(RecordedProgram program) { - mProgram = program; - } - - private void setProgressBar(long watchedPositionMs) { - ((RecordingCardView) view).setProgressBar( - (watchedPositionMs == TvInputManager.TIME_SHIFT_INVALID_TIME) ? null - : Math.min(100, (int) (100.0f * watchedPositionMs - / mProgram.getDurationMillis()))); - } - - @Override - public void onWatchedPositionChanged(long programId, long positionMs) { - if (programId == mProgram.getId()) { - setProgressBar(positionMs); - } - } - } - - public RecordedProgramPresenter(Context context, boolean showEpisodeTitle) { - mContext = context; - mChannelDataManager = TvApplication.getSingletons(context).getChannelDataManager(); - mTodayString = context.getString(R.string.dvr_date_today); - mYesterdayString = context.getString(R.string.dvr_date_yesterday); - mDvrWatchedPositionManager = - TvApplication.getSingletons(context).getDvrWatchedPositionManager(); - mProgressBarColor = context.getResources() - .getColor(R.color.play_controls_progress_bar_watched); - mShowEpisodeTitle = showEpisodeTitle; - } - - public RecordedProgramPresenter(Context context) { - this(context, false); - } - - @Override - public ViewHolder onCreateViewHolder(ViewGroup parent) { - RecordingCardView view = new RecordingCardView(mContext); - return new RecordedProgramViewHolder(view, mProgressBarColor); - } - - @Override - public void onBindViewHolder(ViewHolder viewHolder, Object o) { - final RecordedProgram program = (RecordedProgram) o; - final RecordingCardView cardView = (RecordingCardView) viewHolder.view; - Channel channel = mChannelDataManager.getChannel(program.getChannelId()); - String titleString = mShowEpisodeTitle ? program.getEpisodeDisplayTitle(mContext) - : program.getTitleWithEpisodeNumber(mContext); - SpannableString title = titleString == null ? null : new SpannableString(titleString); - if (TextUtils.isEmpty(title)) { - title = new SpannableString(channel != null ? channel.getDisplayName() - : mContext.getResources().getString(R.string.no_program_information)); - } else if (!mShowEpisodeTitle) { - // TODO: Some translation may add delimiters in-between program titles, we should use - // a more robust way to get the span range. - String programTitle = program.getTitle(); - title.setSpan(new TextAppearanceSpan(mContext, - R.style.text_appearance_card_view_episode_number), programTitle == null ? 0 - : programTitle.length(), title.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - } - cardView.setTitle(title); - String imageUri = null; - boolean isChannelLogo = false; - if (program.getPosterArtUri() != null) { - imageUri = program.getPosterArtUri(); - } else if (program.getThumbnailUri() != null) { - imageUri = program.getThumbnailUri(); - } else if (channel != null) { - imageUri = TvContract.buildChannelLogoUri(channel.getId()).toString(); - isChannelLogo = true; - } - cardView.setImageUri(imageUri, isChannelLogo); - int durationMinutes = - Math.max(1, (int) TimeUnit.MILLISECONDS.toMinutes(program.getDurationMillis())); - String durationString = getContext().getResources().getQuantityString( - R.plurals.dvr_program_duration, durationMinutes, durationMinutes); - cardView.setContent(getDescription(program), durationString); - if (viewHolder instanceof RecordedProgramViewHolder) { - RecordedProgramViewHolder cardViewHolder = (RecordedProgramViewHolder) viewHolder; - cardViewHolder.setProgram(program); - mDvrWatchedPositionManager.addListener(cardViewHolder, program.getId()); - cardViewHolder - .setProgressBar(mDvrWatchedPositionManager.getWatchedPosition(program.getId())); - } - super.onBindViewHolder(viewHolder, o); - } - - @Override - public void onUnbindViewHolder(ViewHolder viewHolder) { - if (viewHolder instanceof RecordedProgramViewHolder) { - mDvrWatchedPositionManager.removeListener((RecordedProgramViewHolder) viewHolder, - ((RecordedProgramViewHolder) viewHolder).mProgram.getId()); - } - ((RecordingCardView) viewHolder.view).reset(); - super.onUnbindViewHolder(viewHolder); - } - - /** - * Returns description would be used in its card view. - */ - protected String getDescription(RecordedProgram recording) { - int dateDifference = Utils.computeDateDifference(recording.getStartTimeUtcMillis(), - System.currentTimeMillis()); - if (dateDifference == 0) { - return mTodayString; - } else if (dateDifference == 1) { - return mYesterdayString; - } else { - return Utils.getDurationString(mContext, recording.getStartTimeUtcMillis(), - recording.getStartTimeUtcMillis(), false, true, false, 0); - } - } - - /** - * Returns context. - */ - protected Context getContext() { - return mContext; - } -} diff --git a/src/com/android/tv/dvr/ui/RecordingCardView.java b/src/com/android/tv/dvr/ui/RecordingCardView.java deleted file mode 100644 index 51c3b03b..00000000 --- a/src/com/android/tv/dvr/ui/RecordingCardView.java +++ /dev/null @@ -1,185 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.dvr.ui; - -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.net.Uri; -import android.support.annotation.Nullable; -import android.support.v17.leanback.widget.BaseCardView; -import android.text.TextUtils; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.ImageView; -import android.widget.ProgressBar; -import android.widget.TextView; - -import com.android.tv.R; -import com.android.tv.dvr.RecordedProgram; -import com.android.tv.util.ImageLoader; - -/** - * A CardView for displaying info about a {@link com.android.tv.dvr.ScheduledRecording} or - * {@link RecordedProgram} or - * {@link com.android.tv.dvr.SeriesRecording}. - */ -class RecordingCardView extends BaseCardView { - private final ImageView mImageView; - private final int mImageWidth; - private final int mImageHeight; - private String mImageUri; - private final TextView mTitleView; - private final TextView mMajorContentView; - private final TextView mMinorContentView; - private final ProgressBar mProgressBar; - private final View mAffiliatedIconContainer; - private final ImageView mAffiliatedIcon; - private final Drawable mDefaultImage; - - RecordingCardView(Context context) { - this(context, - context.getResources().getDimensionPixelSize(R.dimen.dvr_card_image_layout_width), - context.getResources().getDimensionPixelSize(R.dimen.dvr_card_image_layout_height)); - } - - RecordingCardView(Context context, int imageWidth, int imageHeight) { - super(context); - //TODO(dvr): move these to the layout XML. - setCardType(BaseCardView.CARD_TYPE_INFO_UNDER_WITH_EXTRA); - setInfoVisibility(BaseCardView.CARD_REGION_VISIBLE_ALWAYS); - setFocusable(true); - setFocusableInTouchMode(true); - mDefaultImage = getResources().getDrawable(R.drawable.dvr_default_poster, null); - - LayoutInflater inflater = LayoutInflater.from(getContext()); - inflater.inflate(R.layout.dvr_recording_card_view, this); - mImageView = (ImageView) findViewById(R.id.image); - mImageWidth = imageWidth; - mImageHeight = imageHeight; - mProgressBar = (ProgressBar) findViewById(R.id.recording_progress); - mAffiliatedIconContainer = findViewById(R.id.affiliated_icon_container); - mAffiliatedIcon = (ImageView) findViewById(R.id.affiliated_icon); - mTitleView = (TextView) findViewById(R.id.title); - mMajorContentView = (TextView) findViewById(R.id.content_major); - mMinorContentView = (TextView) findViewById(R.id.content_minor); - } - - void setTitle(CharSequence title) { - mTitleView.setText(title); - } - - void setContent(CharSequence majorContent, CharSequence minorContent) { - if (!TextUtils.isEmpty(majorContent)) { - mMajorContentView.setText(majorContent); - mMajorContentView.setVisibility(View.VISIBLE); - } else { - mMajorContentView.setVisibility(View.GONE); - } - if (!TextUtils.isEmpty(minorContent)) { - mMinorContentView.setText(minorContent); - mMinorContentView.setVisibility(View.VISIBLE); - } else { - mMinorContentView.setVisibility(View.GONE); - } - } - - /** - * Sets progress bar. If progress is {@code null}, hides progress bar. - */ - void setProgressBar(Integer progress) { - if (progress == null) { - mProgressBar.setVisibility(View.GONE); - } else { - mProgressBar.setProgress(progress); - mProgressBar.setVisibility(View.VISIBLE); - } - } - - /** - * Sets the color of progress bar. - */ - void setProgressBarColor(int color) { - mProgressBar.getProgressDrawable().setTint(color); - } - - void setImageUri(String uri, boolean isChannelLogo) { - if (isChannelLogo) { - mImageView.setScaleType(ImageView.ScaleType.CENTER_INSIDE); - } else { - mImageView.setScaleType(ImageView.ScaleType.CENTER_CROP); - } - mImageUri = uri; - if (TextUtils.isEmpty(uri)) { - mImageView.setImageDrawable(mDefaultImage); - } else { - ImageLoader.loadBitmap(getContext(), uri, mImageWidth, mImageHeight, - new RecordingCardImageLoaderCallback(this, uri)); - } - } - - /** - * Set image to card view. - */ - public void setImage(Drawable image) { - if (image != null) { - mImageView.setImageDrawable(image); - } - } - - public void setAffiliatedIcon(int imageResId) { - if (imageResId > 0) { - mAffiliatedIconContainer.setVisibility(View.VISIBLE); - mAffiliatedIcon.setImageResource(imageResId); - } else { - mAffiliatedIconContainer.setVisibility(View.INVISIBLE); - } - } - - /** - * Returns image view. - */ - public ImageView getImageView() { - return mImageView; - } - - private static class RecordingCardImageLoaderCallback - extends ImageLoader.ImageLoaderCallback { - private final String mUri; - - RecordingCardImageLoaderCallback(RecordingCardView referent, String uri) { - super(referent); - mUri = uri; - } - - @Override - public void onBitmapLoaded(RecordingCardView view, @Nullable Bitmap bitmap) { - if (bitmap == null || !mUri.equals(view.mImageUri)) { - view.mImageView.setImageDrawable(view.mDefaultImage); - } else { - view.mImageView.setImageDrawable(new BitmapDrawable(view.getResources(), bitmap)); - } - } - } - - public void reset() { - mTitleView.setText(null); - setContent(null, null); - mImageView.setImageDrawable(mDefaultImage); - } -} diff --git a/src/com/android/tv/dvr/ui/RecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/RecordingDetailsFragment.java deleted file mode 100644 index 4e19ec3f..00000000 --- a/src/com/android/tv/dvr/ui/RecordingDetailsFragment.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.tv.dvr.ui; - -import android.os.Bundle; -import android.support.v17.leanback.app.DetailsFragment; -import android.text.Spannable; -import android.text.SpannableString; -import android.text.TextUtils; -import android.text.style.TextAppearanceSpan; - -import com.android.tv.R; -import com.android.tv.TvApplication; -import com.android.tv.data.Channel; -import com.android.tv.dvr.ScheduledRecording; - -/** - * {@link DetailsFragment} for recordings in DVR. - */ -abstract class RecordingDetailsFragment extends DvrDetailsFragment { - private ScheduledRecording mRecording; - - @Override - protected void onCreateInternal() { - setDetailsOverviewRow(createDetailsContent()); - } - - @Override - protected boolean onLoadRecordingDetails(Bundle args) { - long scheduledRecordingId = args.getLong(DvrDetailsActivity.RECORDING_ID); - mRecording = TvApplication.getSingletons(getContext()).getDvrDataManager() - .getScheduledRecording(scheduledRecordingId); - return mRecording != null; - } - - /** - * Returns {@link ScheduledRecording} for the current fragment. - */ - public ScheduledRecording getRecording() { - return mRecording; - } - - private DetailsContent createDetailsContent() { - Channel channel = TvApplication.getSingletons(getContext()).getChannelDataManager() - .getChannel(mRecording.getChannelId()); - SpannableString title = mRecording.getProgramTitleWithEpisodeNumber(getContext()) == null ? - null : new SpannableString(mRecording - .getProgramTitleWithEpisodeNumber(getContext())); - if (TextUtils.isEmpty(title)) { - title = new SpannableString(channel != null ? channel.getDisplayName() - : getContext().getResources().getString( - R.string.no_program_information)); - } else { - String programTitle = mRecording.getProgramTitle(); - title.setSpan(new TextAppearanceSpan(getContext(), - R.style.text_appearance_card_view_episode_number), programTitle == null ? 0 - : programTitle.length(), title.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - } - String description = !TextUtils.isEmpty(mRecording.getProgramDescription()) ? - mRecording.getProgramDescription() : mRecording.getProgramLongDescription(); - if (TextUtils.isEmpty(description)) { - description = channel != null ? channel.getDescription() : null; - } - return new DetailsContent.Builder() - .setTitle(title) - .setStartTimeUtcMillis(mRecording.getStartTimeMs()) - .setEndTimeUtcMillis(mRecording.getEndTimeMs()) - .setDescription(description) - .setImageUris(mRecording.getProgramPosterArtUri(), - mRecording.getProgramThumbnailUri(), channel) - .build(); - } -} diff --git a/src/com/android/tv/dvr/ui/ScheduledRecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/ScheduledRecordingDetailsFragment.java deleted file mode 100644 index 60816bb5..00000000 --- a/src/com/android/tv/dvr/ui/ScheduledRecordingDetailsFragment.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.tv.dvr.ui; - -import android.content.res.Resources; -import android.os.Bundle; -import android.support.v17.leanback.widget.Action; -import android.support.v17.leanback.widget.OnActionClickedListener; -import android.support.v17.leanback.widget.SparseArrayObjectAdapter; -import android.text.TextUtils; - -import com.android.tv.R; -import com.android.tv.TvApplication; -import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.DvrUiHelper; - -/** - * {@link RecordingDetailsFragment} for scheduled recording in DVR. - */ -public class ScheduledRecordingDetailsFragment extends RecordingDetailsFragment { - private static final int ACTION_VIEW_SCHEDULE = 1; - private static final int ACTION_CANCEL = 2; - - private DvrManager mDvrManager; - private Action mScheduleAction; - private boolean mHideViewSchedule; - - @Override - public void onCreate(Bundle savedInstance) { - mDvrManager = TvApplication.getSingletons(getContext()).getDvrManager(); - mHideViewSchedule = getArguments().getBoolean(DvrDetailsActivity.HIDE_VIEW_SCHEDULE); - super.onCreate(savedInstance); - } - - @Override - public void onResume() { - super.onResume(); - if (mScheduleAction != null) { - mScheduleAction.setIcon(getResources().getDrawable(getScheduleIconId())); - } - } - - @Override - protected SparseArrayObjectAdapter onCreateActionsAdapter() { - SparseArrayObjectAdapter adapter = - new SparseArrayObjectAdapter(new ActionPresenterSelector()); - Resources res = getResources(); - if (!mHideViewSchedule) { - mScheduleAction = new Action(ACTION_VIEW_SCHEDULE, - res.getString(R.string.dvr_detail_view_schedule), null, - res.getDrawable(getScheduleIconId())); - adapter.set(ACTION_VIEW_SCHEDULE, mScheduleAction); - } - adapter.set(ACTION_CANCEL, new Action(ACTION_CANCEL, - res.getString(R.string.epg_dvr_dialog_message_remove_recording_schedule), null, - res.getDrawable(R.drawable.ic_dvr_cancel_32dp))); - return adapter; - } - - @Override - protected OnActionClickedListener onCreateOnActionClickedListener() { - return new OnActionClickedListener() { - @Override - public void onActionClicked(Action action) { - long actionId = action.getId(); - if (actionId == ACTION_VIEW_SCHEDULE) { - DvrUiHelper.startSchedulesActivity(getContext(), getRecording()); - } else if (actionId == ACTION_CANCEL) { - mDvrManager.removeScheduledRecording(getRecording()); - getActivity().finish(); - } - } - }; - } - - private int getScheduleIconId() { - if (mDvrManager.isConflicting(getRecording())) { - return R.drawable.ic_warning_white_32dp; - } else { - return R.drawable.ic_schedule_32dp; - } - } -} diff --git a/src/com/android/tv/dvr/ui/ScheduledRecordingPresenter.java b/src/com/android/tv/dvr/ui/ScheduledRecordingPresenter.java deleted file mode 100644 index 5f447f13..00000000 --- a/src/com/android/tv/dvr/ui/ScheduledRecordingPresenter.java +++ /dev/null @@ -1,177 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.dvr.ui; - -import android.app.Activity; -import android.content.Context; -import android.media.tv.TvContract; -import android.os.Handler; -import android.text.Spannable; -import android.text.SpannableString; -import android.text.TextUtils; -import android.text.style.TextAppearanceSpan; -import android.view.ViewGroup; - -import com.android.tv.ApplicationSingletons; -import com.android.tv.R; -import com.android.tv.TvApplication; -import com.android.tv.data.Channel; -import com.android.tv.data.ChannelDataManager; -import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.ScheduledRecording; -import com.android.tv.util.Utils; - -import java.util.concurrent.TimeUnit; - -/** - * Presents a {@link ScheduledRecording} in the {@link DvrBrowseFragment}. - */ -public class ScheduledRecordingPresenter extends DvrItemPresenter { - private static final long PROGRESS_UPDATE_INTERVAL_MS = TimeUnit.SECONDS.toMillis(5); - - private final ChannelDataManager mChannelDataManager; - private final DvrManager mDvrManager; - private final Context mContext; - private final int mProgressBarColor; - - private static final class ScheduledRecordingViewHolder extends ViewHolder { - private final Handler mHandler = new Handler(); - private ScheduledRecording mScheduledRecording; - private final Runnable mProgressBarUpdater = new Runnable() { - @Override - public void run() { - updateProgressBar(); - mHandler.postDelayed(this, PROGRESS_UPDATE_INTERVAL_MS); - } - }; - - ScheduledRecordingViewHolder(RecordingCardView view, int progressBarColor) { - super(view); - view.setProgressBarColor(progressBarColor); - } - - private void updateProgressBar() { - if (mScheduledRecording == null) { - return; - } - int recordingState = mScheduledRecording.getState(); - RecordingCardView cardView = (RecordingCardView) view; - if (recordingState == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) { - cardView.setProgressBar(Math.max(0, Math.min((int) (100 * - (System.currentTimeMillis() - mScheduledRecording.getStartTimeMs()) - / mScheduledRecording.getDuration()), 100))); - } else if (recordingState == ScheduledRecording.STATE_RECORDING_FINISHED) { - cardView.setProgressBar(100); - } else { - // Hides progress bar. - cardView.setProgressBar(null); - } - } - - private void startUpdateProgressBar() { - mHandler.post(mProgressBarUpdater); - } - - private void stopUpdateProgressBar() { - mHandler.removeCallbacks(mProgressBarUpdater); - } - } - - public ScheduledRecordingPresenter(Context context) { - ApplicationSingletons singletons = TvApplication.getSingletons(context); - mChannelDataManager = singletons.getChannelDataManager(); - mDvrManager = singletons.getDvrManager(); - mContext = context; - mProgressBarColor = context.getResources() - .getColor(R.color.play_controls_recording_icon_color_on_focus); - } - - @Override - public ViewHolder onCreateViewHolder(ViewGroup parent) { - Context context = parent.getContext(); - RecordingCardView view = new RecordingCardView(context); - return new ScheduledRecordingViewHolder(view, mProgressBarColor); - } - - @Override - public void onBindViewHolder(ViewHolder baseHolder, Object o) { - final ScheduledRecordingViewHolder viewHolder = (ScheduledRecordingViewHolder) baseHolder; - final ScheduledRecording recording = (ScheduledRecording) o; - final RecordingCardView cardView = (RecordingCardView) viewHolder.view; - final Context context = viewHolder.view.getContext(); - - setTitleAndImage(cardView, recording); - int dateDifference = Utils.computeDateDifference(System.currentTimeMillis(), - recording.getStartTimeMs()); - if (dateDifference <= 0) { - cardView.setContent(mContext.getString(R.string.dvr_date_today_time, - Utils.getDurationString(context, recording.getStartTimeMs(), - recording.getEndTimeMs(), false, false, true, 0)), null); - } else if (dateDifference == 1) { - cardView.setContent(mContext.getString(R.string.dvr_date_tomorrow_time, - Utils.getDurationString(context, recording.getStartTimeMs(), - recording.getEndTimeMs(), false, false, true, 0)), null); - } else { - cardView.setContent(Utils.getDurationString(context, recording.getStartTimeMs(), - recording.getStartTimeMs(), false, true, false, 0), null); - } - if (mDvrManager.isConflicting(recording)) { - cardView.setAffiliatedIcon(R.drawable.ic_warning_white_32dp); - } else { - cardView.setAffiliatedIcon(0); - } - viewHolder.updateProgressBar(); - viewHolder.mScheduledRecording = recording; - viewHolder.startUpdateProgressBar(); - super.onBindViewHolder(viewHolder, o); - } - - @Override - public void onUnbindViewHolder(ViewHolder baseHolder) { - ScheduledRecordingViewHolder viewHolder = (ScheduledRecordingViewHolder) baseHolder; - viewHolder.stopUpdateProgressBar(); - final RecordingCardView cardView = (RecordingCardView) viewHolder.view; - viewHolder.mScheduledRecording = null; - cardView.reset(); - super.onUnbindViewHolder(viewHolder); - } - - private void setTitleAndImage(RecordingCardView cardView, ScheduledRecording recording) { - Channel channel = mChannelDataManager.getChannel(recording.getChannelId()); - SpannableString title = recording.getProgramTitleWithEpisodeNumber(mContext) == null ? - null : new SpannableString(recording.getProgramTitleWithEpisodeNumber(mContext)); - if (TextUtils.isEmpty(title)) { - title = new SpannableString(channel != null ? channel.getDisplayName() - : mContext.getResources().getString(R.string.no_program_information)); - } else { - String programTitle = recording.getProgramTitle(); - title.setSpan(new TextAppearanceSpan(mContext, - R.style.text_appearance_card_view_episode_number), - programTitle == null ? 0 : programTitle.length(), title.length(), - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - } - String imageUri = recording.getProgramPosterArtUri(); - boolean isChannelLogo = false; - if (TextUtils.isEmpty(imageUri)) { - imageUri = channel != null ? - TvContract.buildChannelLogoUri(channel.getId()).toString() : null; - isChannelLogo = true; - } - cardView.setTitle(title); - cardView.setImageUri(imageUri, isChannelLogo); - } -} diff --git a/src/com/android/tv/dvr/ui/SeriesDeletionFragment.java b/src/com/android/tv/dvr/ui/SeriesDeletionFragment.java deleted file mode 100644 index 36e3cfc1..00000000 --- a/src/com/android/tv/dvr/ui/SeriesDeletionFragment.java +++ /dev/null @@ -1,252 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.dvr.ui; - -import android.content.Context; -import android.media.tv.TvInputManager; -import android.os.Bundle; -import android.support.v17.leanback.app.GuidedStepFragment; -import android.support.v17.leanback.widget.GuidanceStylist.Guidance; -import android.support.v17.leanback.widget.GuidedAction; -import android.support.v17.leanback.widget.GuidedActionsStylist; -import android.text.TextUtils; -import android.view.ViewGroup.LayoutParams; -import android.widget.Toast; - -import com.android.tv.R; -import com.android.tv.TvApplication; -import com.android.tv.common.SoftPreconditions; -import com.android.tv.dvr.DvrDataManager; -import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.DvrWatchedPositionManager; -import com.android.tv.dvr.RecordedProgram; -import com.android.tv.dvr.SeriesRecording; -import com.android.tv.ui.GuidedActionsStylistWithDivider; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.concurrent.TimeUnit; - -/** - * Fragment for DVR series recording settings. - */ -public class SeriesDeletionFragment extends GuidedStepFragment { - private static final long WATCHED_TIME_UNIT_THRESHOLD = TimeUnit.MINUTES.toMillis(2); - - // Since recordings' IDs are used as its check actions' IDs, which are random positive numbers, - // negative values are used by other actions to prevent duplicated IDs. - private static final long ACTION_ID_SELECT_WATCHED = -110; - private static final long ACTION_ID_SELECT_ALL = -111; - private static final long ACTION_ID_DELETE = -112; - - private DvrDataManager mDvrDataManager; - private DvrWatchedPositionManager mDvrWatchedPositionManager; - private List mRecordings; - private final Set mWatchedRecordings = new HashSet<>(); - private boolean mAllSelected; - private long mSeriesRecordingId; - private int mOneLineActionHeight; - - @Override - public void onAttach(Context context) { - super.onAttach(context); - mSeriesRecordingId = getArguments() - .getLong(DvrSeriesDeletionActivity.SERIES_RECORDING_ID, -1); - SoftPreconditions.checkArgument(mSeriesRecordingId != -1); - mDvrDataManager = TvApplication.getSingletons(context).getDvrDataManager(); - mDvrWatchedPositionManager = - TvApplication.getSingletons(context).getDvrWatchedPositionManager(); - mRecordings = mDvrDataManager.getRecordedPrograms(mSeriesRecordingId); - mOneLineActionHeight = getResources().getDimensionPixelSize( - R.dimen.dvr_settings_one_line_action_container_height); - if (mRecordings.isEmpty()) { - Toast.makeText(getActivity(), getString(R.string.dvr_series_deletion_no_recordings), - Toast.LENGTH_LONG).show(); - finishGuidedStepFragments(); - return; - } - Collections.sort(mRecordings, RecordedProgram.EPISODE_COMPARATOR); - } - - @Override - public Guidance onCreateGuidance(Bundle savedInstanceState) { - String breadcrumb = null; - SeriesRecording series = mDvrDataManager.getSeriesRecording(mSeriesRecordingId); - if (series != null) { - breadcrumb = series.getTitle(); - } - return new Guidance(getString(R.string.dvr_series_deletion_title), - getString(R.string.dvr_series_deletion_description), breadcrumb, null); - } - - @Override - public void onCreateActions(List actions, Bundle savedInstanceState) { - actions.add(new GuidedAction.Builder(getActivity()) - .id(ACTION_ID_SELECT_WATCHED) - .title(getString(R.string.dvr_series_select_watched)) - .build()); - actions.add(new GuidedAction.Builder(getActivity()) - .id(ACTION_ID_SELECT_ALL) - .title(getString(R.string.dvr_series_select_all)) - .build()); - actions.add(GuidedActionsStylistWithDivider.createDividerAction(getContext())); - for (RecordedProgram recording : mRecordings) { - long watchedPositionMs = - mDvrWatchedPositionManager.getWatchedPosition(recording.getId()); - String title = recording.getEpisodeDisplayTitle(getContext()); - if (TextUtils.isEmpty(title)) { - title = TextUtils.isEmpty(recording.getTitle()) ? - getString(R.string.channel_banner_no_title) : recording.getTitle(); - } - String description; - if (watchedPositionMs != TvInputManager.TIME_SHIFT_INVALID_TIME) { - description = getWatchedString(watchedPositionMs, recording.getDurationMillis()); - mWatchedRecordings.add(recording.getId()); - } else { - description = getString(R.string.dvr_series_never_watched); - } - actions.add(new GuidedAction.Builder(getActivity()) - .id(recording.getId()) - .title(title) - .description(description) - .checkSetId(GuidedAction.CHECKBOX_CHECK_SET_ID) - .build()); - } - } - - @Override - public void onCreateButtonActions(List actions, Bundle savedInstanceState) { - actions.add(new GuidedAction.Builder(getActivity()) - .id(ACTION_ID_DELETE) - .title(getString(R.string.dvr_detail_delete)) - .build()); - actions.add(new GuidedAction.Builder(getActivity()) - .clickAction(GuidedAction.ACTION_ID_CANCEL) - .build()); - } - - @Override - public void onGuidedActionClicked(GuidedAction action) { - long actionId = action.getId(); - if (actionId == ACTION_ID_DELETE) { - List idsToDelete = new ArrayList<>(); - for (GuidedAction guidedAction : getActions()) { - if (guidedAction.getCheckSetId() == GuidedAction.CHECKBOX_CHECK_SET_ID - && guidedAction.isChecked()) { - idsToDelete.add(guidedAction.getId()); - } - } - if (!idsToDelete.isEmpty()) { - DvrManager dvrManager = TvApplication.getSingletons(getActivity()).getDvrManager(); - dvrManager.removeRecordedPrograms(idsToDelete); - } - Toast.makeText(getContext(), getResources().getQuantityString( - R.plurals.dvr_msg_episodes_deleted, idsToDelete.size(), idsToDelete.size(), - mRecordings.size()), Toast.LENGTH_LONG).show(); - finishGuidedStepFragments(); - } else if (actionId == GuidedAction.ACTION_ID_CANCEL) { - finishGuidedStepFragments(); - } else if (actionId == ACTION_ID_SELECT_WATCHED) { - for (GuidedAction guidedAction : getActions()) { - if (guidedAction.getCheckSetId() == GuidedAction.CHECKBOX_CHECK_SET_ID) { - long recordingId = guidedAction.getId(); - if (mWatchedRecordings.contains(recordingId)) { - guidedAction.setChecked(true); - } else { - guidedAction.setChecked(false); - } - notifyActionChanged(findActionPositionById(recordingId)); - } - } - mAllSelected = updateSelectAllState(); - } else if (actionId == ACTION_ID_SELECT_ALL) { - mAllSelected = !mAllSelected; - for (GuidedAction guidedAction : getActions()) { - if (guidedAction.getCheckSetId() == GuidedAction.CHECKBOX_CHECK_SET_ID) { - guidedAction.setChecked(mAllSelected); - notifyActionChanged(findActionPositionById(guidedAction.getId())); - } - } - updateSelectAllState(action, mAllSelected); - } else { - mAllSelected = updateSelectAllState(); - } - } - - @Override - public GuidedActionsStylist onCreateButtonActionsStylist() { - return new DvrGuidedActionsStylist(true); - } - - @Override - public GuidedActionsStylist onCreateActionsStylist() { - return new GuidedActionsStylistWithDivider() { - @Override - public void onBindViewHolder(ViewHolder vh, GuidedAction action) { - super.onBindViewHolder(vh, action); - if (action.getId() == ACTION_DIVIDER) { - return; - } - LayoutParams lp = vh.itemView.getLayoutParams(); - if (action.getCheckSetId() != GuidedAction.CHECKBOX_CHECK_SET_ID) { - lp.height = mOneLineActionHeight; - } else { - vh.itemView.setLayoutParams( - new LayoutParams(lp.width, LayoutParams.WRAP_CONTENT)); - } - } - }; - } - - private String getWatchedString(long watchedPositionMs, long durationMs) { - if (durationMs > WATCHED_TIME_UNIT_THRESHOLD) { - return getResources().getString(R.string.dvr_series_watched_info_minutes, - Math.max(1, TimeUnit.MILLISECONDS.toMinutes(watchedPositionMs)), - TimeUnit.MILLISECONDS.toMinutes(durationMs)); - } else { - return getResources().getString(R.string.dvr_series_watched_info_seconds, - Math.max(1, TimeUnit.MILLISECONDS.toSeconds(watchedPositionMs)), - TimeUnit.MILLISECONDS.toSeconds(durationMs)); - } - } - - private boolean updateSelectAllState() { - for (GuidedAction guidedAction : getActions()) { - if (guidedAction.getCheckSetId() == GuidedAction.CHECKBOX_CHECK_SET_ID) { - if (!guidedAction.isChecked()) { - if (mAllSelected) { - updateSelectAllState(findActionById(ACTION_ID_SELECT_ALL), false); - } - return false; - } - } - } - if (!mAllSelected) { - updateSelectAllState(findActionById(ACTION_ID_SELECT_ALL), true); - } - return true; - } - - private void updateSelectAllState(GuidedAction selectAll, boolean select) { - selectAll.setTitle(select ? getString(R.string.dvr_series_deselect_all) - : getString(R.string.dvr_series_select_all)); - notifyActionChanged(findActionPositionById(ACTION_ID_SELECT_ALL)); - } -} diff --git a/src/com/android/tv/dvr/ui/SeriesRecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/SeriesRecordingDetailsFragment.java deleted file mode 100644 index e9e391d4..00000000 --- a/src/com/android/tv/dvr/ui/SeriesRecordingDetailsFragment.java +++ /dev/null @@ -1,375 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.tv.dvr.ui; - -import android.content.res.Resources; -import android.graphics.drawable.Drawable; -import android.media.tv.TvInputManager; -import android.os.Bundle; -import android.support.v17.leanback.app.DetailsFragment; -import android.support.v17.leanback.widget.Action; -import android.support.v17.leanback.widget.ArrayObjectAdapter; -import android.support.v17.leanback.widget.ClassPresenterSelector; -import android.support.v17.leanback.widget.DetailsOverviewRow; -import android.support.v17.leanback.widget.DetailsOverviewRowPresenter; -import android.support.v17.leanback.widget.HeaderItem; -import android.support.v17.leanback.widget.ListRow; -import android.support.v17.leanback.widget.ListRowPresenter; -import android.support.v17.leanback.widget.OnActionClickedListener; -import android.support.v17.leanback.widget.PresenterSelector; -import android.support.v17.leanback.widget.SparseArrayObjectAdapter; -import android.text.TextUtils; - -import com.android.tv.R; -import com.android.tv.TvApplication; -import com.android.tv.data.BaseProgram; -import com.android.tv.data.Channel; -import com.android.tv.dvr.DvrDataManager; -import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.DvrUiHelper; -import com.android.tv.dvr.DvrWatchedPositionManager; -import com.android.tv.dvr.RecordedProgram; -import com.android.tv.dvr.ScheduledRecording; -import com.android.tv.dvr.SeriesRecording; - -import java.util.Collections; -import java.util.Comparator; -import java.util.List; - -/** - * {@link DetailsFragment} for series recording in DVR. - */ -public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implements - DvrDataManager.SeriesRecordingListener, DvrDataManager.RecordedProgramListener { - private static final int ACTION_WATCH = 1; - private static final int ACTION_SERIES_SCHEDULES = 2; - private static final int ACTION_DELETE = 3; - - private DvrWatchedPositionManager mDvrWatchedPositionManager; - private DvrDataManager mDvrDataManager; - - private SeriesRecording mSeries; - // NOTICE: mRecordedPrograms should only be used in creating details fragments. - // After fragments are created, it should be cleared to save resources. - private List mRecordedPrograms; - private RecordedProgram mRecommendRecordedProgram; - private DetailsContent mDetailsContent; - private int mSeasonRowCount; - private SparseArrayObjectAdapter mActionsAdapter; - private Action mDeleteAction; - - private boolean mPaused; - private long mInitialPlaybackPositionMs; - private String mWatchLabel; - private String mResumeLabel; - private Drawable mWatchDrawable; - private RecordedProgramPresenter mRecordedProgramPresenter; - - @Override - public void onCreate(Bundle savedInstanceState) { - mDvrDataManager = TvApplication.getSingletons(getActivity()).getDvrDataManager(); - mWatchLabel = getString(R.string.dvr_detail_watch); - mResumeLabel = getString(R.string.dvr_detail_series_resume); - mWatchDrawable = getResources().getDrawable(R.drawable.lb_ic_play, null); - mRecordedProgramPresenter = new RecordedProgramPresenter(getContext(), true); - super.onCreate(savedInstanceState); - } - - @Override - protected void onCreateInternal() { - mDvrWatchedPositionManager = TvApplication.getSingletons(getActivity()) - .getDvrWatchedPositionManager(); - setDetailsOverviewRow(mDetailsContent); - setupRecordedProgramsRow(); - mDvrDataManager.addSeriesRecordingListener(this); - mDvrDataManager.addRecordedProgramListener(this); - mRecordedPrograms = null; - } - - @Override - public void onResume() { - super.onResume(); - if (mPaused) { - updateWatchAction(); - mPaused = false; - } - } - - @Override - public void onPause() { - super.onPause(); - mPaused = true; - } - - private void updateWatchAction() { - List programs = mDvrDataManager.getRecordedPrograms(mSeries.getId()); - Collections.sort(programs, RecordedProgram.EPISODE_COMPARATOR); - mRecommendRecordedProgram = getRecommendProgram(programs); - if (mRecommendRecordedProgram == null) { - mActionsAdapter.clear(ACTION_WATCH); - } else { - String episodeStatus; - if(mDvrWatchedPositionManager.getWatchedStatus(mRecommendRecordedProgram) - == DvrWatchedPositionManager.DVR_WATCHED_STATUS_WATCHING) { - episodeStatus = mResumeLabel; - mInitialPlaybackPositionMs = mDvrWatchedPositionManager - .getWatchedPosition(mRecommendRecordedProgram.getId()); - } else { - episodeStatus = mWatchLabel; - mInitialPlaybackPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME; - } - String episodeDisplayNumber = mRecommendRecordedProgram.getEpisodeDisplayNumber( - getContext()); - mActionsAdapter.set(ACTION_WATCH, new Action(ACTION_WATCH, - episodeStatus, episodeDisplayNumber, mWatchDrawable)); - } - } - - @Override - protected boolean onLoadRecordingDetails(Bundle args) { - long recordId = args.getLong(DvrDetailsActivity.RECORDING_ID); - mSeries = TvApplication.getSingletons(getActivity()).getDvrDataManager() - .getSeriesRecording(recordId); - if (mSeries == null) { - return false; - } - mRecordedPrograms = mDvrDataManager.getRecordedPrograms(mSeries.getId()); - Collections.sort(mRecordedPrograms, RecordedProgram.SEASON_REVERSED_EPISODE_COMPARATOR); - mDetailsContent = createDetailsContent(); - return true; - } - - @Override - protected PresenterSelector onCreatePresenterSelector( - DetailsOverviewRowPresenter rowPresenter) { - ClassPresenterSelector presenterSelector = new ClassPresenterSelector(); - presenterSelector.addClassPresenter(DetailsOverviewRow.class, rowPresenter); - presenterSelector.addClassPresenter(ListRow.class, new ListRowPresenter()); - return presenterSelector; - } - - private DetailsContent createDetailsContent() { - Channel channel = TvApplication.getSingletons(getContext()).getChannelDataManager() - .getChannel(mSeries.getChannelId()); - String description = TextUtils.isEmpty(mSeries.getLongDescription()) - ? mSeries.getDescription() : mSeries.getLongDescription(); - return new DetailsContent.Builder() - .setTitle(mSeries.getTitle()) - .setDescription(description) - .setImageUris(mSeries.getPosterUri(), mSeries.getPhotoUri(), channel) - .build(); - } - - @Override - protected SparseArrayObjectAdapter onCreateActionsAdapter() { - mActionsAdapter = new SparseArrayObjectAdapter(new ActionPresenterSelector()); - Resources res = getResources(); - updateWatchAction(); - mActionsAdapter.set(ACTION_SERIES_SCHEDULES, new Action(ACTION_SERIES_SCHEDULES, - getString(R.string.dvr_detail_view_schedule), null, - res.getDrawable(R.drawable.ic_schedule_32dp, null))); - mDeleteAction = new Action(ACTION_DELETE, - getString(R.string.dvr_detail_series_delete), null, - res.getDrawable(R.drawable.ic_delete_32dp, null)); - if (!mRecordedPrograms.isEmpty()) { - mActionsAdapter.set(ACTION_DELETE, mDeleteAction); - } - return mActionsAdapter; - } - - private void setupRecordedProgramsRow() { - for (RecordedProgram program : mRecordedPrograms) { - addProgram(program); - } - } - - @Override - public void onDestroy() { - super.onDestroy(); - mDvrDataManager.removeSeriesRecordingListener(this); - mDvrDataManager.removeRecordedProgramListener(this); - if (mSeries != null) { - DvrManager dvrManager = TvApplication.getSingletons(getActivity()).getDvrManager(); - if (dvrManager.canRemoveSeriesRecording(mSeries.getId())) { - dvrManager.removeSeriesRecording(mSeries.getId()); - } - } - mRecordedProgramPresenter.unbindAllViewHolders(); - } - - @Override - protected OnActionClickedListener onCreateOnActionClickedListener() { - return new OnActionClickedListener() { - @Override - public void onActionClicked(Action action) { - if (action.getId() == ACTION_WATCH) { - startPlayback(mRecommendRecordedProgram, mInitialPlaybackPositionMs); - } else if (action.getId() == ACTION_SERIES_SCHEDULES) { - DvrUiHelper.startSchedulesActivityForSeries(getContext(), mSeries); - } else if (action.getId() == ACTION_DELETE) { - DvrUiHelper.startSeriesDeletionActivity(getContext(), mSeries.getId()); - } - } - }; - } - - /** - * The programs are sorted by season number and episode number. - */ - private RecordedProgram getRecommendProgram(List programs) { - for (int i = programs.size() - 1 ; i >= 0 ; i--) { - RecordedProgram program = programs.get(i); - int watchedStatus = mDvrWatchedPositionManager.getWatchedStatus(program); - if (watchedStatus == DvrWatchedPositionManager.DVR_WATCHED_STATUS_NEW) { - continue; - } - if (watchedStatus == DvrWatchedPositionManager.DVR_WATCHED_STATUS_WATCHING) { - return program; - } - if (i == programs.size() - 1) { - return program; - } else { - return programs.get(i + 1); - } - } - return programs.isEmpty() ? null : programs.get(0); - } - - @Override - public void onSeriesRecordingAdded(SeriesRecording... seriesRecordings) { } - - @Override - public void onSeriesRecordingChanged(SeriesRecording... seriesRecordings) { - for (SeriesRecording series : seriesRecordings) { - if (mSeries.getId() == series.getId()) { - mSeries = series; - } - } - } - - @Override - public void onSeriesRecordingRemoved(SeriesRecording... seriesRecordings) { - for (SeriesRecording series : seriesRecordings) { - if (series.getId() == mSeries.getId()) { - mSeries = null; - getActivity().finish(); - return; - } - } - } - - @Override - public void onRecordedProgramsAdded(RecordedProgram... recordedPrograms) { - for (RecordedProgram recordedProgram : recordedPrograms) { - if (TextUtils.equals(recordedProgram.getSeriesId(), mSeries.getSeriesId())) { - addProgram(recordedProgram); - if (mActionsAdapter.lookup(ACTION_DELETE) == null) { - mActionsAdapter.set(ACTION_DELETE, mDeleteAction); - } - } - } - } - - @Override - public void onRecordedProgramsChanged(RecordedProgram... recordedPrograms) { - // Do nothing - } - - @Override - public void onRecordedProgramsRemoved(RecordedProgram... recordedPrograms) { - for (RecordedProgram recordedProgram : recordedPrograms) { - if (TextUtils.equals(recordedProgram.getSeriesId(), mSeries.getSeriesId())) { - ListRow row = getSeasonRow(recordedProgram.getSeasonNumber(), false); - if (row != null) { - SeasonRowAdapter adapter = (SeasonRowAdapter) row.getAdapter(); - adapter.remove(recordedProgram); - if (adapter.isEmpty()) { - getRowsAdapter().remove(row); - if (getRowsAdapter().size() == 1) { - // No season rows left. Only DetailsOverviewRow - mActionsAdapter.clear(ACTION_DELETE); - } - } - } - if (recordedProgram.getId() == mRecommendRecordedProgram.getId()) { - updateWatchAction(); - } - } - } - } - - private void addProgram(RecordedProgram program) { - String programSeasonNumber = - TextUtils.isEmpty(program.getSeasonNumber()) ? "" : program.getSeasonNumber(); - getOrCreateSeasonRowAdapter(programSeasonNumber).add(program); - } - - private SeasonRowAdapter getOrCreateSeasonRowAdapter(String seasonNumber) { - ListRow row = getSeasonRow(seasonNumber, true); - return (SeasonRowAdapter) row.getAdapter(); - } - - private ListRow getSeasonRow(String seasonNumber, boolean createNewRow) { - seasonNumber = TextUtils.isEmpty(seasonNumber) ? "" : seasonNumber; - ArrayObjectAdapter rowsAdaptor = getRowsAdapter(); - for (int i = rowsAdaptor.size() - 1; i >= 0; i--) { - Object row = rowsAdaptor.get(i); - if (row instanceof ListRow) { - int compareResult = BaseProgram.numberCompare(seasonNumber, - ((SeasonRowAdapter) ((ListRow) row).getAdapter()).mSeasonNumber); - if (compareResult == 0) { - return (ListRow) row; - } else if (compareResult < 0) { - return createNewRow ? createNewSeasonRow(seasonNumber, i + 1) : null; - } - } - } - return createNewRow ? createNewSeasonRow(seasonNumber, rowsAdaptor.size()) : null; - } - - private ListRow createNewSeasonRow(String seasonNumber, int position) { - String seasonTitle = seasonNumber.isEmpty() ? mSeries.getTitle() - : getString(R.string.dvr_detail_series_season_title, seasonNumber); - HeaderItem header = new HeaderItem(mSeasonRowCount++, seasonTitle); - ClassPresenterSelector selector = new ClassPresenterSelector(); - selector.addClassPresenter(RecordedProgram.class, mRecordedProgramPresenter); - ListRow row = new ListRow(header, new SeasonRowAdapter(selector, - new Comparator() { - @Override - public int compare(RecordedProgram lhs, RecordedProgram rhs) { - return BaseProgram.EPISODE_COMPARATOR.compare(lhs, rhs); - } - }, seasonNumber)); - getRowsAdapter().add(position, row); - return row; - } - - private class SeasonRowAdapter extends SortedArrayAdapter { - private String mSeasonNumber; - - SeasonRowAdapter(PresenterSelector selector, Comparator comparator, - String seasonNumber) { - super(selector, comparator); - mSeasonNumber = seasonNumber; - } - - @Override - public long getId(RecordedProgram program) { - return program.getId(); - } - } -} diff --git a/src/com/android/tv/dvr/ui/SeriesRecordingPresenter.java b/src/com/android/tv/dvr/ui/SeriesRecordingPresenter.java deleted file mode 100644 index c2c0f596..00000000 --- a/src/com/android/tv/dvr/ui/SeriesRecordingPresenter.java +++ /dev/null @@ -1,234 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.dvr.ui; - -import android.app.Activity; -import android.content.Context; -import android.media.tv.TvContract; -import android.media.tv.TvInputManager; -import android.text.TextUtils; -import android.view.ViewGroup; - -import com.android.tv.ApplicationSingletons; -import com.android.tv.R; -import com.android.tv.TvApplication; -import com.android.tv.data.Channel; -import com.android.tv.data.ChannelDataManager; -import com.android.tv.dvr.DvrDataManager; -import com.android.tv.dvr.DvrDataManager.RecordedProgramListener; -import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; -import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.DvrWatchedPositionManager; -import com.android.tv.dvr.DvrWatchedPositionManager.WatchedPositionChangedListener; -import com.android.tv.dvr.RecordedProgram; -import com.android.tv.dvr.ScheduledRecording; -import com.android.tv.dvr.SeriesRecording; - -import java.util.List; - -/** - * Presents a {@link SeriesRecording} in {@link DvrBrowseFragment}. - */ -public class SeriesRecordingPresenter extends DvrItemPresenter { - private final ChannelDataManager mChannelDataManager; - private final DvrDataManager mDvrDataManager; - private final DvrManager mDvrManager; - private final DvrWatchedPositionManager mWatchedPositionManager; - - private static final class SeriesRecordingViewHolder extends ViewHolder implements - WatchedPositionChangedListener, ScheduledRecordingListener, RecordedProgramListener { - private SeriesRecording mSeriesRecording; - private RecordingCardView mCardView; - private DvrDataManager mDvrDataManager; - private DvrManager mDvrManager; - private DvrWatchedPositionManager mWatchedPositionManager; - - SeriesRecordingViewHolder(RecordingCardView view, DvrDataManager dvrDataManager, - DvrManager dvrManager, DvrWatchedPositionManager watchedPositionManager) { - super(view); - mCardView = view; - mDvrDataManager = dvrDataManager; - mDvrManager = dvrManager; - mWatchedPositionManager = watchedPositionManager; - } - - @Override - public void onWatchedPositionChanged(long recordedProgramId, long positionMs) { - if (positionMs != TvInputManager.TIME_SHIFT_INVALID_TIME) { - mWatchedPositionManager.removeListener(this, recordedProgramId); - updateCardViewContent(); - } - } - - @Override - public void onScheduledRecordingAdded(ScheduledRecording... scheduledRecordings) { - for (ScheduledRecording scheduledRecording : scheduledRecordings) { - if (scheduledRecording.getSeriesRecordingId() == mSeriesRecording.getId()) { - updateCardViewContent(); - return; - } - } - } - - @Override - public void onScheduledRecordingRemoved(ScheduledRecording... scheduledRecordings) { - for (ScheduledRecording scheduledRecording : scheduledRecordings) { - if (scheduledRecording.getSeriesRecordingId() == mSeriesRecording.getId()) { - updateCardViewContent(); - return; - } - } - } - - @Override - public void onRecordedProgramsAdded(RecordedProgram... recordedPrograms) { - boolean needToUpdateCardView = false; - for (RecordedProgram recordedProgram : recordedPrograms) { - if (TextUtils.equals(recordedProgram.getSeriesId(), - mSeriesRecording.getSeriesId())) { - mDvrDataManager.removeScheduledRecordingListener(this); - mWatchedPositionManager.addListener(this, recordedProgram.getId()); - needToUpdateCardView = true; - } - } - if (needToUpdateCardView) { - updateCardViewContent(); - } - } - - @Override - public void onRecordedProgramsRemoved(RecordedProgram... recordedPrograms) { - boolean needToUpdateCardView = false; - for (RecordedProgram recordedProgram : recordedPrograms) { - if (TextUtils.equals(recordedProgram.getSeriesId(), - mSeriesRecording.getSeriesId())) { - if (mWatchedPositionManager.getWatchedPosition(recordedProgram.getId()) - == TvInputManager.TIME_SHIFT_INVALID_TIME) { - mWatchedPositionManager.removeListener(this, recordedProgram.getId()); - } - needToUpdateCardView = true; - } - } - if (needToUpdateCardView) { - updateCardViewContent(); - } - } - - @Override - public void onRecordedProgramsChanged(RecordedProgram... recordedPrograms) { - // Do nothing - } - - @Override - public void onScheduledRecordingStatusChanged(ScheduledRecording... scheduledRecordings) { - // Do nothing - } - - public void onBound(SeriesRecording seriesRecording) { - mSeriesRecording = seriesRecording; - mDvrDataManager.addScheduledRecordingListener(this); - mDvrDataManager.addRecordedProgramListener(this); - for (RecordedProgram recordedProgram : - mDvrDataManager.getRecordedPrograms(mSeriesRecording.getId())) { - if (mWatchedPositionManager.getWatchedPosition(recordedProgram.getId()) - == TvInputManager.TIME_SHIFT_INVALID_TIME) { - mWatchedPositionManager.addListener(this, recordedProgram.getId()); - } - } - updateCardViewContent(); - } - - public void onUnbound() { - mDvrDataManager.removeScheduledRecordingListener(this); - mDvrDataManager.removeRecordedProgramListener(this); - mWatchedPositionManager.removeListener(this); - } - - private void updateCardViewContent() { - int count = 0; - int quantityStringID; - List recordedPrograms = - mDvrDataManager.getRecordedPrograms(mSeriesRecording.getId()); - if (recordedPrograms.size() == 0) { - count = mDvrManager.getAvailableScheduledRecording(mSeriesRecording.getId()).size(); - quantityStringID = R.plurals.dvr_count_scheduled_recordings; - } else { - for (RecordedProgram recordedProgram : recordedPrograms) { - if (mWatchedPositionManager.getWatchedPosition(recordedProgram.getId()) - == TvInputManager.TIME_SHIFT_INVALID_TIME) { - count++; - } - } - if (count == 0) { - count = recordedPrograms.size(); - quantityStringID = R.plurals.dvr_count_recordings; - } else { - quantityStringID = R.plurals.dvr_count_new_recordings; - } - } - mCardView.setContent(mCardView.getResources() - .getQuantityString(quantityStringID, count, count), null); - } - } - - public SeriesRecordingPresenter(Context context) { - ApplicationSingletons singletons = TvApplication.getSingletons(context); - mChannelDataManager = singletons.getChannelDataManager(); - mDvrDataManager = singletons.getDvrDataManager(); - mDvrManager = singletons.getDvrManager(); - mWatchedPositionManager = singletons.getDvrWatchedPositionManager(); - } - - @Override - public ViewHolder onCreateViewHolder(ViewGroup parent) { - Context context = parent.getContext(); - RecordingCardView view = new RecordingCardView(context); - return new SeriesRecordingViewHolder(view, mDvrDataManager, mDvrManager, - mWatchedPositionManager); - } - - @Override - public void onBindViewHolder(ViewHolder baseHolder, Object o) { - final SeriesRecordingViewHolder viewHolder = (SeriesRecordingViewHolder) baseHolder; - final SeriesRecording seriesRecording = (SeriesRecording) o; - final RecordingCardView cardView = (RecordingCardView) viewHolder.view; - viewHolder.onBound(seriesRecording); - setTitleAndImage(cardView, seriesRecording); - super.onBindViewHolder(baseHolder, o); - } - - @Override - public void onUnbindViewHolder(ViewHolder viewHolder) { - ((RecordingCardView) viewHolder.view).reset(); - ((SeriesRecordingViewHolder) viewHolder).onUnbound(); - super.onUnbindViewHolder(viewHolder); - } - - private void setTitleAndImage(RecordingCardView cardView, SeriesRecording recording) { - cardView.setTitle(recording.getTitle()); - if (recording.getPosterUri() != null) { - cardView.setImageUri(recording.getPosterUri(), false); - } else { - Channel channel = mChannelDataManager.getChannel(recording.getChannelId()); - String imageUri = null; - if (channel != null) { - imageUri = TvContract.buildChannelLogoUri(channel.getId()).toString(); - } - cardView.setImageUri(imageUri, true); - } - } -} diff --git a/src/com/android/tv/dvr/ui/SeriesSettingsFragment.java b/src/com/android/tv/dvr/ui/SeriesSettingsFragment.java deleted file mode 100644 index 6c05c9c6..00000000 --- a/src/com/android/tv/dvr/ui/SeriesSettingsFragment.java +++ /dev/null @@ -1,397 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.dvr.ui; - -import android.app.FragmentManager; -import android.app.ProgressDialog; -import android.content.Context; -import android.os.Bundle; -import android.support.v17.leanback.app.GuidedStepFragment; -import android.support.v17.leanback.widget.GuidanceStylist.Guidance; -import android.support.v17.leanback.widget.GuidedAction; -import android.support.v17.leanback.widget.GuidedActionsStylist; -import android.util.Log; -import android.util.LongSparseArray; -import android.view.View; -import android.view.ViewGroup; -import android.widget.FrameLayout; -import android.widget.ProgressBar; - -import com.android.tv.R; -import com.android.tv.TvApplication; -import com.android.tv.data.Channel; -import com.android.tv.data.ChannelDataManager; -import com.android.tv.data.Program; -import com.android.tv.dvr.DvrDataManager; -import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.DvrUiHelper; -import com.android.tv.dvr.EpisodicProgramLoadTask; -import com.android.tv.dvr.SeriesRecording; -import com.android.tv.dvr.SeriesRecording.ChannelOption; -import com.android.tv.dvr.SeriesRecordingScheduler; -import com.android.tv.dvr.SeriesRecordingScheduler.OnSeriesRecordingUpdatedListener; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -/** - * Fragment for DVR series recording settings. - */ -public class SeriesSettingsFragment extends GuidedStepFragment - implements DvrDataManager.SeriesRecordingListener { - private static final String TAG = "SeriesSettingsFragment"; - private static final boolean DEBUG = false; - - private static final long ACTION_ID_PRIORITY = 10; - private static final long ACTION_ID_CHANNEL = 11; - - private static final long SUB_ACTION_ID_CHANNEL_ALL = 102; - // Each channel's action id = SUB_ACTION_ID_CHANNEL_ONE_BASE + channel id - private static final long SUB_ACTION_ID_CHANNEL_ONE_BASE = 500; - - private DvrDataManager mDvrDataManager; - private ChannelDataManager mChannelDataManager; - private DvrManager mDvrManager; - private SeriesRecording mSeriesRecording; - private long mSeriesRecordingId; - @ChannelOption int mChannelOption; - private Comparator mChannelComparator; - private long mSelectedChannelId; - private int mBackStackCount; - private boolean mShowViewScheduleOptionInDialog; - - private String mFragmentTitle; - private String mProrityActionTitle; - private String mProrityActionHighestText; - private String mProrityActionLowestText; - private String mChannelsActionTitle; - private String mChannelsActionAllText; - private LongSparseArray mId2Channel = new LongSparseArray<>(); - private List mChannels = new ArrayList<>(); - private EpisodicProgramLoadTask mEpisodicProgramLoadTask; - - private GuidedAction mPriorityGuidedAction; - private GuidedAction mChannelsGuidedAction; - - @Override - public void onAttach(Context context) { - super.onAttach(context); - mBackStackCount = getFragmentManager().getBackStackEntryCount(); - mDvrDataManager = TvApplication.getSingletons(context).getDvrDataManager(); - mSeriesRecordingId = getArguments().getLong(DvrSeriesSettingsActivity.SERIES_RECORDING_ID); - mSeriesRecording = mDvrDataManager.getSeriesRecording(mSeriesRecordingId); - if (mSeriesRecording == null) { - getActivity().finish(); - return; - } - mDvrManager = TvApplication.getSingletons(context).getDvrManager(); - mShowViewScheduleOptionInDialog = getArguments().getBoolean( - DvrSeriesSettingsActivity.SHOW_VIEW_SCHEDULE_OPTION_IN_DIALOG); - mDvrDataManager.addSeriesRecordingListener(this); - long[] channelIds = getArguments().getLongArray(DvrSeriesSettingsActivity.CHANNEL_ID_LIST); - mChannelDataManager = TvApplication.getSingletons(context).getChannelDataManager(); - if (channelIds == null) { - Channel channel = mChannelDataManager.getChannel(mSeriesRecording.getChannelId()); - if (channel != null) { - mId2Channel.put(channel.getId(), channel); - mChannels.add(channel); - } - collectChannelsInBackground(); - } else { - for (long channelId : channelIds) { - Channel channel = mChannelDataManager.getChannel(channelId); - if (channel != null) { - mId2Channel.put(channel.getId(), channel); - mChannels.add(channel); - } - } - } - mChannelOption = mSeriesRecording.getChannelOption(); - mSelectedChannelId = Channel.INVALID_ID; - if (mChannelOption == SeriesRecording.OPTION_CHANNEL_ONE) { - Channel channel = mChannelDataManager.getChannel(mSeriesRecording.getChannelId()); - if (channel != null) { - mSelectedChannelId = channel.getId(); - } else { - mChannelOption = SeriesRecording.OPTION_CHANNEL_ALL; - } - } - mChannelComparator = new Channel.DefaultComparator(context, - TvApplication.getSingletons(context).getTvInputManagerHelper()); - mChannels.sort(mChannelComparator); - mFragmentTitle = getString(R.string.dvr_series_settings_title); - mProrityActionTitle = getString(R.string.dvr_series_settings_priority); - mProrityActionHighestText = getString(R.string.dvr_series_settings_priority_highest); - mProrityActionLowestText = getString(R.string.dvr_series_settings_priority_lowest); - mChannelsActionTitle = getString(R.string.dvr_series_settings_channels); - mChannelsActionAllText = getString(R.string.dvr_series_settings_channels_all); - } - - @Override - public void onDetach() { - super.onDetach(); - mDvrDataManager.removeSeriesRecordingListener(this); - if (mEpisodicProgramLoadTask != null) { - mEpisodicProgramLoadTask.cancel(true); - mEpisodicProgramLoadTask = null; - } - } - - @Override - public void onDestroy() { - DvrManager dvrManager = TvApplication.getSingletons(getActivity()).getDvrManager(); - if (getFragmentManager().getBackStackEntryCount() == mBackStackCount - && getArguments() - .getBoolean(DvrSeriesSettingsActivity.REMOVE_EMPTY_SERIES_RECORDING) - && dvrManager.canRemoveSeriesRecording(mSeriesRecordingId)) { - dvrManager.removeSeriesRecording(mSeriesRecordingId); - } - super.onDestroy(); - } - - @Override - public Guidance onCreateGuidance(Bundle savedInstanceState) { - String breadcrumb = mSeriesRecording.getTitle(); - String title = mFragmentTitle; - return new Guidance(title, null, breadcrumb, null); - } - - @Override - public void onCreateActions(List actions, Bundle savedInstanceState) { - mPriorityGuidedAction = new GuidedAction.Builder(getActivity()) - .id(ACTION_ID_PRIORITY) - .title(mProrityActionTitle) - .build(); - updatePriorityGuidedAction(false); - actions.add(mPriorityGuidedAction); - - mChannelsGuidedAction = new GuidedAction.Builder(getActivity()) - .id(ACTION_ID_CHANNEL) - .title(mChannelsActionTitle) - .subActions(buildChannelSubAction()) - .build(); - actions.add(mChannelsGuidedAction); - updateChannelsGuidedAction(false); - } - - @Override - public void onCreateButtonActions(List actions, Bundle savedInstanceState) { - actions.add(new GuidedAction.Builder(getActivity()) - .clickAction(GuidedAction.ACTION_ID_OK) - .build()); - actions.add(new GuidedAction.Builder(getActivity()) - .clickAction(GuidedAction.ACTION_ID_CANCEL) - .build()); - } - - @Override - public void onGuidedActionClicked(GuidedAction action) { - long actionId = action.getId(); - if (actionId == GuidedAction.ACTION_ID_OK) { - if (mEpisodicProgramLoadTask != null) { - mEpisodicProgramLoadTask.cancel(true); - mEpisodicProgramLoadTask = null; - } - if (mChannelOption != mSeriesRecording.getChannelOption() - || mSeriesRecording.isStopped() - || (mChannelOption == SeriesRecording.OPTION_CHANNEL_ONE - && mSeriesRecording.getChannelId() != mSelectedChannelId)) { - SeriesRecording.Builder builder = SeriesRecording.buildFrom(mSeriesRecording) - .setChannelOption(mChannelOption) - .setState(SeriesRecording.STATE_SERIES_NORMAL); - if (mSelectedChannelId != Channel.INVALID_ID) { - builder.setChannelId(mSelectedChannelId); - } - TvApplication.getSingletons(getContext()).getDvrManager() - .updateSeriesRecording(builder.build()); - SeriesRecordingScheduler scheduler = - SeriesRecordingScheduler.getInstance(getContext()); - // Since dialog is used even after the fragment is closed, we should - // use application context. - ProgressDialog dialog = ProgressDialog.show(getContext(), null, getString( - R.string.dvr_series_schedules_progress_message_updating_programs)); - scheduler.addOnSeriesRecordingUpdatedListener( - new OnSeriesRecordingUpdatedListener() { - @Override - public void onSeriesRecordingUpdated(SeriesRecording... seriesRecordings) { - for (SeriesRecording seriesRecording : seriesRecordings) { - if (seriesRecording.getId() == mSeriesRecordingId) { - dialog.dismiss(); - scheduler.removeOnSeriesRecordingUpdatedListener(this); - showConfirmDialog(); - return; - } - } - } - }); - } else { - showConfirmDialog(); - } - } else if (actionId == GuidedAction.ACTION_ID_CANCEL) { - finishGuidedStepFragments(); - } else if (actionId == ACTION_ID_PRIORITY) { - FragmentManager fragmentManager = getFragmentManager(); - PrioritySettingsFragment fragment = new PrioritySettingsFragment(); - Bundle args = new Bundle(); - args.putLong(PrioritySettingsFragment.COME_FROM_SERIES_RECORDING_ID, - mSeriesRecording.getId()); - fragment.setArguments(args); - GuidedStepFragment.add(fragmentManager, fragment, R.id.dvr_settings_view_frame); - } - } - - @Override - public boolean onSubGuidedActionClicked(GuidedAction action) { - long actionId = action.getId(); - if (actionId == SUB_ACTION_ID_CHANNEL_ALL) { - mChannelOption = SeriesRecording.OPTION_CHANNEL_ALL; - mSelectedChannelId = Channel.INVALID_ID; - updateChannelsGuidedAction(true); - return true; - } else if (actionId > SUB_ACTION_ID_CHANNEL_ONE_BASE) { - mChannelOption = SeriesRecording.OPTION_CHANNEL_ONE; - mSelectedChannelId = actionId - SUB_ACTION_ID_CHANNEL_ONE_BASE; - updateChannelsGuidedAction(true); - return true; - } - return false; - } - - @Override - public GuidedActionsStylist onCreateButtonActionsStylist() { - return new DvrGuidedActionsStylist(true); - } - - private void updateChannelsGuidedAction(boolean notifyActionChanged) { - if (mChannelOption == SeriesRecording.OPTION_CHANNEL_ALL) { - mChannelsGuidedAction.setDescription(mChannelsActionAllText); - } else { - mChannelsGuidedAction.setDescription(mId2Channel.get(mSelectedChannelId) - .getDisplayText()); - } - if (notifyActionChanged) { - notifyActionChanged(findActionPositionById(ACTION_ID_CHANNEL)); - } - } - - private void updatePriorityGuidedAction(boolean notifyActionChanged) { - int totalSeriesCount = 0; - int priorityOrder = 0; - for (SeriesRecording seriesRecording : mDvrDataManager.getSeriesRecordings()) { - if (seriesRecording.getState() == SeriesRecording.STATE_SERIES_NORMAL - || seriesRecording.getId() == mSeriesRecording.getId()) { - ++totalSeriesCount; - } - if (seriesRecording.getState() == SeriesRecording.STATE_SERIES_NORMAL - && seriesRecording.getId() != mSeriesRecording.getId() - && seriesRecording.getPriority() > mSeriesRecording.getPriority()) { - ++priorityOrder; - } - } - if (priorityOrder == 0) { - mPriorityGuidedAction.setDescription(mProrityActionHighestText); - } else if (priorityOrder >= totalSeriesCount - 1) { - mPriorityGuidedAction.setDescription(mProrityActionLowestText); - } else { - mPriorityGuidedAction.setDescription(getString( - R.string.dvr_series_settings_priority_rank, priorityOrder + 1)); - } - if (notifyActionChanged) { - notifyActionChanged(findActionPositionById(ACTION_ID_PRIORITY)); - } - } - - private void collectChannelsInBackground() { - if (mEpisodicProgramLoadTask != null) { - mEpisodicProgramLoadTask.cancel(true); - } - mEpisodicProgramLoadTask = new EpisodicProgramLoadTask(getContext(), mSeriesRecording) { - @Override - protected void onPostExecute(List programs) { - mEpisodicProgramLoadTask = null; - Set channelIds = new HashSet<>(); - for (Program program : programs) { - channelIds.add(program.getChannelId()); - } - boolean channelAdded = false; - for (Long channelId : channelIds) { - if (mId2Channel.get(channelId) != null) { - continue; - } - Channel channel = mChannelDataManager.getChannel(channelId); - if (channel != null) { - channelAdded = true; - mId2Channel.put(channelId, channel); - mChannels.add(channel); - if (DEBUG) Log.d(TAG, "Added channel: " + channel); - } - } - if (!channelAdded) { - return; - } - mChannels.sort(mChannelComparator); - mChannelsGuidedAction.setSubActions(buildChannelSubAction()); - notifyActionChanged(findActionPositionById(ACTION_ID_CHANNEL)); - if (DEBUG) Log.d(TAG, "Complete EpisodicProgramLoadTask"); - } - }.setLoadCurrentProgram(true) - .setLoadDisallowedProgram(true) - .setLoadScheduledEpisode(true) - .setIgnoreChannelOption(true); - mEpisodicProgramLoadTask.execute(); - } - - private List buildChannelSubAction() { - List channelSubActions = new ArrayList<>(); - channelSubActions.add(new GuidedAction.Builder(getActivity()) - .id(SUB_ACTION_ID_CHANNEL_ALL) - .title(mChannelsActionAllText) - .build()); - for (Channel channel : mChannels) { - channelSubActions.add(new GuidedAction.Builder(getActivity()) - .id(SUB_ACTION_ID_CHANNEL_ONE_BASE + channel.getId()) - .title(channel.getDisplayText()) - .build()); - } - return channelSubActions; - } - - private void showConfirmDialog() { - DvrUiHelper.StartSeriesScheduledDialogActivity( - getContext(), mSeriesRecording, mShowViewScheduleOptionInDialog); - finishGuidedStepFragments(); - } - - @Override - public void onSeriesRecordingAdded(SeriesRecording... seriesRecordings) { } - - @Override - public void onSeriesRecordingRemoved(SeriesRecording... seriesRecordings) { } - - @Override - public void onSeriesRecordingChanged(SeriesRecording... seriesRecordings) { - for (SeriesRecording seriesRecording : seriesRecordings) { - if (seriesRecording.getId() == mSeriesRecordingId) { - mSeriesRecording = seriesRecording; - updatePriorityGuidedAction(true); - return; - } - } - } -} diff --git a/src/com/android/tv/dvr/ui/SortedArrayAdapter.java b/src/com/android/tv/dvr/ui/SortedArrayAdapter.java index 393a5ff3..8c0af9ed 100644 --- a/src/com/android/tv/dvr/ui/SortedArrayAdapter.java +++ b/src/com/android/tv/dvr/ui/SortedArrayAdapter.java @@ -20,11 +20,15 @@ import android.support.annotation.VisibleForTesting; import android.support.v17.leanback.widget.ArrayObjectAdapter; import android.support.v17.leanback.widget.PresenterSelector; +import com.android.tv.common.SoftPreconditions; + import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; +import java.util.HashSet; import java.util.List; +import java.util.Set; /** * Keeps a set of items sorted @@ -35,16 +39,18 @@ public abstract class SortedArrayAdapter extends ArrayObjectAdapter { private final Comparator mComparator; private final int mMaxItemCount; private int mExtraItemCount; + private final Set mIds = new HashSet<>(); - SortedArrayAdapter(PresenterSelector presenterSelector, Comparator comparator) { + public SortedArrayAdapter(PresenterSelector presenterSelector, Comparator comparator) { this(presenterSelector, comparator, Integer.MAX_VALUE); } - SortedArrayAdapter(PresenterSelector presenterSelector, Comparator comparator, + public SortedArrayAdapter(PresenterSelector presenterSelector, Comparator comparator, int maxItemCount) { super(presenterSelector); mComparator = comparator; mMaxItemCount = maxItemCount; + setHasStableIds(true); } /** @@ -56,7 +62,12 @@ public abstract class SortedArrayAdapter extends ArrayObjectAdapter { final void setInitialItems(List items) { List itemsCopy = new ArrayList<>(items); Collections.sort(itemsCopy, mComparator); - addAll(0, itemsCopy.subList(0, Math.min(mMaxItemCount, itemsCopy.size()))); + for (T item : itemsCopy) { + add(item, true); + if (size() == mMaxItemCount) { + break; + } + } } /** @@ -82,6 +93,9 @@ public abstract class SortedArrayAdapter extends ArrayObjectAdapter { * the end to save search time. */ public final void add(T item, boolean insertToEnd) { + long newItemId = getId(item); + SoftPreconditions.checkState(!mIds.contains(newItemId)); + mIds.add(newItemId); int i; if (insertToEnd) { i = findInsertPosition(item); @@ -89,8 +103,9 @@ public abstract class SortedArrayAdapter extends ArrayObjectAdapter { i = findInsertPositionBinary(item); } super.add(i, item); - if (size() > mMaxItemCount + mExtraItemCount) { - removeItems(mMaxItemCount, size() - mMaxItemCount - mExtraItemCount); + if (mMaxItemCount < Integer.MAX_VALUE && size() > mMaxItemCount + mExtraItemCount) { + Object removedItem = get(mMaxItemCount); + remove(removedItem); } } @@ -100,48 +115,97 @@ public abstract class SortedArrayAdapter extends ArrayObjectAdapter { * They will be presented in their insertion order. */ public int addExtraItem(T item) { + long newItemId = getId(item); + SoftPreconditions.checkState(!mIds.contains(newItemId)); + mIds.add(newItemId); super.add(item); return ++mExtraItemCount; } + @Override + public boolean remove(Object item) { + return removeWithId((T) item); + } + /** * Removes an item which has the same ID as {@code item}. */ public boolean removeWithId(T item) { - int index = indexWithTypeAndId(item); - return index >= 0 && index < size() && remove(get(index)); + int index = indexWithId(item); + return index >= 0 && index < size() && removeItems(index, 1) == 1; + } + + @Override + public int removeItems(int position, int count) { + int upperBound = Math.min(position + count, size()); + for (int i = position; i < upperBound; i++) { + mIds.remove(getId((T) get(i))); + } + if (upperBound > size() - mExtraItemCount) { + mExtraItemCount -= upperBound - Math.max(size() - mExtraItemCount, position); + } + return super.removeItems(position, count); + } + + @Override + public void replace(int position, Object item) { + boolean wasExtra = position >= size() - mExtraItemCount; + removeItems(position, 1); + if (!wasExtra) { + add(item); + } else { + addExtraItem((T) item); + } + } + + @Override + public void clear() { + mIds.clear(); + super.clear(); } /** - * Change an item in the list. + * Changes an item in the list. * @param item The item to change. */ public final void change(T item) { - int oldIndex = indexWithTypeAndId(item); + int oldIndex = indexWithId(item); if (oldIndex != -1) { T old = (T) get(oldIndex); if (mComparator.compare(old, item) == 0) { replace(oldIndex, item); return; } - removeItems(oldIndex, 1); + remove(old); } add(item); } + /** + * Checks whether the item is in the list. + */ + public final boolean contains(T item) { + return indexWithId(item) != -1; + } + + @Override + public long getId(int position) { + return getId((T) get(position)); + } + /** * Returns the id of the the given {@code item}, which will be used in {@link #change} to * decide if the given item is already existed in the adapter. * * The id must be stable. */ - abstract long getId(T item); + protected abstract long getId(T item); - private int indexWithTypeAndId(T item) { + private int indexWithId(T item) { long id = getId(item); for (int i = 0; i < size() - mExtraItemCount; i++) { T r = (T) get(i); - if (r.getClass() == item.getClass() && getId(r) == id) { + if (getId(r) == id) { return i; } } diff --git a/src/com/android/tv/dvr/ui/browse/ActionPresenterSelector.java b/src/com/android/tv/dvr/ui/browse/ActionPresenterSelector.java new file mode 100644 index 00000000..38a78f5d --- /dev/null +++ b/src/com/android/tv/dvr/ui/browse/ActionPresenterSelector.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tv.dvr.ui.browse; + +import android.graphics.drawable.Drawable; +import android.support.v17.leanback.R; +import android.support.v17.leanback.widget.Action; +import android.support.v17.leanback.widget.Presenter; +import android.support.v17.leanback.widget.PresenterSelector; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; + +// This class is adapted from Leanback's library, which does not support action icon with one-line +// label. This class modified its getPresenter method to support the above situation. +class ActionPresenterSelector extends PresenterSelector { + private final Presenter mOneLineActionPresenter = new OneLineActionPresenter(); + private final Presenter mTwoLineActionPresenter = new TwoLineActionPresenter(); + private final Presenter[] mPresenters = new Presenter[] { + mOneLineActionPresenter, mTwoLineActionPresenter}; + + @Override + public Presenter getPresenter(Object item) { + Action action = (Action) item; + if (TextUtils.isEmpty(action.getLabel2()) && action.getIcon() == null) { + return mOneLineActionPresenter; + } else { + return mTwoLineActionPresenter; + } + } + + @Override + public Presenter[] getPresenters() { + return mPresenters; + } + + static class ActionViewHolder extends Presenter.ViewHolder { + Action mAction; + Button mButton; + int mLayoutDirection; + + public ActionViewHolder(View view, int layoutDirection) { + super(view); + mButton = (Button) view.findViewById(R.id.lb_action_button); + mLayoutDirection = layoutDirection; + } + } + + class OneLineActionPresenter extends Presenter { + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent) { + View v = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.lb_action_1_line, parent, false); + return new ActionViewHolder(v, parent.getLayoutDirection()); + } + + @Override + public void onBindViewHolder(Presenter.ViewHolder viewHolder, Object item) { + Action action = (Action) item; + ActionViewHolder vh = (ActionViewHolder) viewHolder; + vh.mAction = action; + vh.mButton.setText(action.getLabel1()); + } + + @Override + public void onUnbindViewHolder(Presenter.ViewHolder viewHolder) { + ((ActionViewHolder) viewHolder).mAction = null; + } + } + + class TwoLineActionPresenter extends Presenter { + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent) { + View v = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.lb_action_2_lines, parent, false); + return new ActionViewHolder(v, parent.getLayoutDirection()); + } + + @Override + public void onBindViewHolder(Presenter.ViewHolder viewHolder, Object item) { + Action action = (Action) item; + ActionViewHolder vh = (ActionViewHolder) viewHolder; + Drawable icon = action.getIcon(); + vh.mAction = action; + + if (icon != null) { + final int startPadding = vh.view.getResources() + .getDimensionPixelSize(R.dimen.lb_action_with_icon_padding_start); + final int endPadding = vh.view.getResources() + .getDimensionPixelSize(R.dimen.lb_action_with_icon_padding_end); + vh.view.setPaddingRelative(startPadding, 0, endPadding, 0); + } else { + final int padding = vh.view.getResources() + .getDimensionPixelSize(R.dimen.lb_action_padding_horizontal); + vh.view.setPaddingRelative(padding, 0, padding, 0); + } + vh.mButton.setCompoundDrawablesRelativeWithIntrinsicBounds(icon, null, null, null); + + CharSequence line1 = action.getLabel1(); + CharSequence line2 = action.getLabel2(); + if (TextUtils.isEmpty(line1)) { + vh.mButton.setText(line2); + } else if (TextUtils.isEmpty(line2)) { + vh.mButton.setText(line1); + } else { + vh.mButton.setText(line1 + "\n" + line2); + } + } + + @Override + public void onUnbindViewHolder(Presenter.ViewHolder viewHolder) { + ActionViewHolder vh = (ActionViewHolder) viewHolder; + vh.mButton.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, null, null); + vh.view.setPadding(0, 0, 0, 0); + vh.mAction = null; + } + } +} \ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/browse/CurrentRecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/CurrentRecordingDetailsFragment.java new file mode 100644 index 00000000..c8f6a03f --- /dev/null +++ b/src/com/android/tv/dvr/ui/browse/CurrentRecordingDetailsFragment.java @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tv.dvr.ui.browse; + +import android.content.Context; +import android.content.res.Resources; +import android.support.v17.leanback.widget.Action; +import android.support.v17.leanback.widget.OnActionClickedListener; +import android.support.v17.leanback.widget.SparseArrayObjectAdapter; + +import com.android.tv.R; +import com.android.tv.TvApplication; +import com.android.tv.dialog.HalfSizedDialogFragment; +import com.android.tv.dvr.DvrDataManager; +import com.android.tv.dvr.DvrManager; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.ui.DvrStopRecordingFragment; +import com.android.tv.dvr.ui.DvrUiHelper; + +/** + * {@link RecordingDetailsFragment} for current recording in DVR. + */ +public class CurrentRecordingDetailsFragment extends RecordingDetailsFragment { + private static final int ACTION_STOP_RECORDING = 1; + + private DvrDataManager mDvrDataManger; + private final DvrDataManager.ScheduledRecordingListener mScheduledRecordingListener = + new DvrDataManager.ScheduledRecordingListener() { + @Override + public void onScheduledRecordingAdded(ScheduledRecording... schedules) { } + + @Override + public void onScheduledRecordingRemoved(ScheduledRecording... schedules) { + for (ScheduledRecording schedule : schedules) { + if (schedule.getId() == getRecording().getId()) { + getActivity().finish(); + return; + } + } + } + + @Override + public void onScheduledRecordingStatusChanged(ScheduledRecording... schedules) { + for (ScheduledRecording schedule : schedules) { + if (schedule.getId() == getRecording().getId() + && schedule.getState() + != ScheduledRecording.STATE_RECORDING_IN_PROGRESS) { + getActivity().finish(); + return; + } + } + } + }; + + @Override + public void onAttach(Context context) { + super.onAttach(context); + mDvrDataManger = TvApplication.getSingletons(context).getDvrDataManager(); + mDvrDataManger.addScheduledRecordingListener(mScheduledRecordingListener); + } + + @Override + protected SparseArrayObjectAdapter onCreateActionsAdapter() { + SparseArrayObjectAdapter adapter = + new SparseArrayObjectAdapter(new ActionPresenterSelector()); + Resources res = getResources(); + adapter.set(ACTION_STOP_RECORDING, new Action(ACTION_STOP_RECORDING, + res.getString(R.string.epg_dvr_dialog_message_stop_recording), null, + res.getDrawable(R.drawable.lb_ic_stop))); + return adapter; + } + + @Override + protected OnActionClickedListener onCreateOnActionClickedListener() { + return new OnActionClickedListener() { + @Override + public void onActionClicked(Action action) { + if (action.getId() == ACTION_STOP_RECORDING) { + DvrUiHelper.showStopRecordingDialog(getActivity(), + getRecording().getChannelId(), + DvrStopRecordingFragment.REASON_USER_STOP, + new HalfSizedDialogFragment.OnActionClickListener() { + @Override + public void onActionClick(long actionId) { + if (actionId == DvrStopRecordingFragment.ACTION_STOP) { + DvrManager dvrManager = + TvApplication.getSingletons(getContext()) + .getDvrManager(); + dvrManager.stopRecording(getRecording()); + getActivity().finish(); + } + } + }); + } + } + }; + } + + @Override + public void onDetach() { + if (mDvrDataManger != null) { + mDvrDataManger.removeScheduledRecordingListener(mScheduledRecordingListener); + } + super.onDetach(); + } +} diff --git a/src/com/android/tv/dvr/ui/browse/DetailsContent.java b/src/com/android/tv/dvr/ui/browse/DetailsContent.java new file mode 100644 index 00000000..b43d1f12 --- /dev/null +++ b/src/com/android/tv/dvr/ui/browse/DetailsContent.java @@ -0,0 +1,207 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tv.dvr.ui.browse; + +import android.media.tv.TvContract; +import android.support.annotation.Nullable; +import android.text.TextUtils; + +import com.android.tv.data.BaseProgram; +import com.android.tv.data.Channel; + +/** + * A class for details content. + */ +class DetailsContent { + /** Constant for invalid time. */ + public static final long INVALID_TIME = -1; + + private CharSequence mTitle; + private long mStartTimeUtcMillis; + private long mEndTimeUtcMillis; + private String mDescription; + private String mLogoImageUri; + private String mBackgroundImageUri; + + private DetailsContent() { } + + /** + * Returns title. + */ + public CharSequence getTitle() { + return mTitle; + } + + /** + * Returns start time. + */ + public long getStartTimeUtcMillis() { + return mStartTimeUtcMillis; + } + + /** + * Returns end time. + */ + public long getEndTimeUtcMillis() { + return mEndTimeUtcMillis; + } + + /** + * Returns description. + */ + public String getDescription() { + return mDescription; + } + + /** + * Returns Logo image URI as a String. + */ + public String getLogoImageUri() { + return mLogoImageUri; + } + + /** + * Returns background image URI as a String. + */ + public String getBackgroundImageUri() { + return mBackgroundImageUri; + } + + /** + * Copies other details content. + */ + public void copyFrom(DetailsContent other) { + if (this == other) { + return; + } + mTitle = other.mTitle; + mStartTimeUtcMillis = other.mStartTimeUtcMillis; + mEndTimeUtcMillis = other.mEndTimeUtcMillis; + mDescription = other.mDescription; + mLogoImageUri = other.mLogoImageUri; + mBackgroundImageUri = other.mBackgroundImageUri; + } + + /** + * A class for building details content. + */ + public static final class Builder { + private final DetailsContent mDetailsContent; + + public Builder() { + mDetailsContent = new DetailsContent(); + mDetailsContent.mStartTimeUtcMillis = INVALID_TIME; + mDetailsContent.mEndTimeUtcMillis = INVALID_TIME; + } + + /** + * Sets title. + */ + public Builder setTitle(CharSequence title) { + mDetailsContent.mTitle = title; + return this; + } + + /** + * Sets start time. + */ + public Builder setStartTimeUtcMillis(long startTimeUtcMillis) { + mDetailsContent.mStartTimeUtcMillis = startTimeUtcMillis; + return this; + } + + /** + * Sets end time. + */ + public Builder setEndTimeUtcMillis(long endTimeUtcMillis) { + mDetailsContent.mEndTimeUtcMillis = endTimeUtcMillis; + return this; + } + + /** + * Sets description. + */ + public Builder setDescription(String description) { + mDetailsContent.mDescription = description; + return this; + } + + /** + * Sets logo image URI as a String. + */ + public Builder setLogoImageUri(String logoImageUri) { + mDetailsContent.mLogoImageUri = logoImageUri; + return this; + } + + /** + * Sets background image URI as a String. + */ + public Builder setBackgroundImageUri(String backgroundImageUri) { + mDetailsContent.mBackgroundImageUri = backgroundImageUri; + return this; + } + + /** + * Sets background image and logo image URI from program and channel. + */ + public Builder setImageUris(@Nullable BaseProgram program, @Nullable Channel channel) { + if (program != null) { + return setImageUris(program.getPosterArtUri(), program.getThumbnailUri(), channel); + } else { + return setImageUris(null, null, channel); + } + } + + /** + * Sets background image and logo image URI and channel is used for fallback images. + */ + public Builder setImageUris(@Nullable String posterArtUri, + @Nullable String thumbnailUri, @Nullable Channel channel) { + mDetailsContent.mLogoImageUri = null; + mDetailsContent.mBackgroundImageUri = null; + if (!TextUtils.isEmpty(posterArtUri) && !TextUtils.isEmpty(thumbnailUri)) { + mDetailsContent.mLogoImageUri = posterArtUri; + mDetailsContent.mBackgroundImageUri = thumbnailUri; + } else if (!TextUtils.isEmpty(posterArtUri)) { + // thumbnailUri is empty + mDetailsContent.mLogoImageUri = posterArtUri; + mDetailsContent.mBackgroundImageUri = posterArtUri; + } else if (!TextUtils.isEmpty(thumbnailUri)) { + // posterArtUri is empty + mDetailsContent.mLogoImageUri = thumbnailUri; + mDetailsContent.mBackgroundImageUri = thumbnailUri; + } + if (TextUtils.isEmpty(mDetailsContent.mLogoImageUri) && channel != null) { + String channelLogoUri = TvContract.buildChannelLogoUri(channel.getId()) + .toString(); + mDetailsContent.mLogoImageUri = channelLogoUri; + mDetailsContent.mBackgroundImageUri = channelLogoUri; + } + return this; + } + + /** + * Builds details content. + */ + public DetailsContent build() { + DetailsContent detailsContent = new DetailsContent(); + detailsContent.copyFrom(mDetailsContent); + return detailsContent; + } + } +} \ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/browse/DetailsContentPresenter.java b/src/com/android/tv/dvr/ui/browse/DetailsContentPresenter.java new file mode 100644 index 00000000..a2e3fe16 --- /dev/null +++ b/src/com/android/tv/dvr/ui/browse/DetailsContentPresenter.java @@ -0,0 +1,299 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tv.dvr.ui.browse; + +import android.app.Activity; +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.animation.PropertyValuesHolder; +import android.graphics.Paint; +import android.graphics.Paint.FontMetricsInt; +import android.support.v17.leanback.widget.Presenter; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.android.tv.R; +import com.android.tv.ui.ViewUtils; +import com.android.tv.util.Utils; + +/** + * An {@link Presenter} for rendering a detailed description of an DVR item. + * Typically this Presenter will be used in a + * {@link android.support.v17.leanback.widget.DetailsOverviewRowPresenter}. + * Most codes of this class is originated from + * {@link android.support.v17.leanback.widget.AbstractDetailsDescriptionPresenter}. + * The latter class are re-used to provide a customized version of + * {@link android.support.v17.leanback.widget.DetailsOverviewRow}. + */ +class DetailsContentPresenter extends Presenter { + /** + * The ViewHolder for the {@link DetailsContentPresenter}. + */ + public static class ViewHolder extends Presenter.ViewHolder { + final TextView mTitle; + final TextView mSubtitle; + final LinearLayout mDescriptionContainer; + final TextView mBody; + final TextView mReadMoreView; + final int mTitleMargin; + final int mUnderTitleBaselineMargin; + final int mUnderSubtitleBaselineMargin; + final int mTitleLineSpacing; + final int mBodyLineSpacing; + final int mBodyMaxLines; + final int mBodyMinLines; + final FontMetricsInt mTitleFontMetricsInt; + final FontMetricsInt mSubtitleFontMetricsInt; + final FontMetricsInt mBodyFontMetricsInt; + final int mTitleMaxLines; + + private Activity mActivity; + private boolean mFullTextMode; + private int mFullTextAnimationDuration; + private boolean mIsListeningToPreDraw; + + private ViewTreeObserver.OnPreDrawListener mPreDrawListener = + new ViewTreeObserver.OnPreDrawListener() { + @Override + public boolean onPreDraw() { + if (mSubtitle.getVisibility() == View.VISIBLE + && mSubtitle.getTop() > view.getHeight() + && mTitle.getLineCount() > 1) { + mTitle.setMaxLines(mTitle.getLineCount() - 1); + return false; + } + final int bodyLines = mBody.getLineCount(); + final int maxLines = mFullTextMode ? bodyLines : + (mTitle.getLineCount() > 1 ? mBodyMinLines : mBodyMaxLines); + if (bodyLines > maxLines) { + mReadMoreView.setVisibility(View.VISIBLE); + mDescriptionContainer.setFocusable(true); + mDescriptionContainer.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + mFullTextMode = true; + mReadMoreView.setVisibility(View.GONE); + mDescriptionContainer.setFocusable(false); + mDescriptionContainer.setOnClickListener(null); + mBody.setMaxLines(bodyLines); + // Minus 1 from line difference to eliminate the space + // originally occupied by "READ MORE" + showFullText((bodyLines - maxLines - 1) * mBodyLineSpacing); + } + }); + } + if (mBody.getMaxLines() != maxLines) { + mBody.setMaxLines(maxLines); + return false; + } else { + removePreDrawListener(); + return true; + } + } + }; + + public ViewHolder(final View view) { + super(view); + view.addOnAttachStateChangeListener( + new View.OnAttachStateChangeListener() { + @Override + public void onViewAttachedToWindow(View v) { + // In case predraw listener was removed in detach, make sure + // we have the proper layout. + addPreDrawListener(); + } + + @Override + public void onViewDetachedFromWindow(View v) { + removePreDrawListener(); + } + }); + mTitle = (TextView) view.findViewById(R.id.dvr_details_description_title); + mSubtitle = (TextView) view.findViewById(R.id.dvr_details_description_subtitle); + mBody = (TextView) view.findViewById(R.id.dvr_details_description_body); + mDescriptionContainer = + (LinearLayout) view.findViewById(R.id.dvr_details_description_container); + mReadMoreView = (TextView) view.findViewById(R.id.dvr_details_description_read_more); + + FontMetricsInt titleFontMetricsInt = getFontMetricsInt(mTitle); + final int titleAscent = view.getResources().getDimensionPixelSize( + R.dimen.lb_details_description_title_baseline); + // Ascent is negative + mTitleMargin = titleAscent + titleFontMetricsInt.ascent; + + mUnderTitleBaselineMargin = view.getResources().getDimensionPixelSize( + R.dimen.lb_details_description_under_title_baseline_margin); + mUnderSubtitleBaselineMargin = view.getResources().getDimensionPixelSize( + R.dimen.lb_details_description_under_subtitle_baseline_margin); + + mTitleLineSpacing = view.getResources().getDimensionPixelSize( + R.dimen.lb_details_description_title_line_spacing); + mBodyLineSpacing = view.getResources().getDimensionPixelSize( + R.dimen.lb_details_description_body_line_spacing); + + mBodyMaxLines = view.getResources().getInteger( + R.integer.lb_details_description_body_max_lines); + mBodyMinLines = view.getResources().getInteger( + R.integer.lb_details_description_body_min_lines); + mTitleMaxLines = mTitle.getMaxLines(); + + mTitleFontMetricsInt = getFontMetricsInt(mTitle); + mSubtitleFontMetricsInt = getFontMetricsInt(mSubtitle); + mBodyFontMetricsInt = getFontMetricsInt(mBody); + } + + void addPreDrawListener() { + if (!mIsListeningToPreDraw) { + mIsListeningToPreDraw = true; + view.getViewTreeObserver().addOnPreDrawListener(mPreDrawListener); + } + } + + void removePreDrawListener() { + if (mIsListeningToPreDraw) { + view.getViewTreeObserver().removeOnPreDrawListener(mPreDrawListener); + mIsListeningToPreDraw = false; + } + } + + public TextView getTitle() { + return mTitle; + } + + public TextView getSubtitle() { + return mSubtitle; + } + + public TextView getBody() { + return mBody; + } + + private FontMetricsInt getFontMetricsInt(TextView textView) { + Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); + paint.setTextSize(textView.getTextSize()); + paint.setTypeface(textView.getTypeface()); + return paint.getFontMetricsInt(); + } + + private void showFullText(int heightDiff) { + final ViewGroup detailsFrame = (ViewGroup) mActivity.findViewById(R.id.details_frame); + int nowHeight = ViewUtils.getLayoutHeight(detailsFrame); + Animator expandAnimator = ViewUtils.createHeightAnimator( + detailsFrame, nowHeight, nowHeight + heightDiff); + expandAnimator.setDuration(mFullTextAnimationDuration); + Animator shiftAnimator = ObjectAnimator.ofPropertyValuesHolder(detailsFrame, + PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, + 0f, -(heightDiff / 2))); + shiftAnimator.setDuration(mFullTextAnimationDuration); + AnimatorSet fullTextAnimator = new AnimatorSet(); + fullTextAnimator.playTogether(expandAnimator, shiftAnimator); + fullTextAnimator.start(); + } + } + + private final Activity mActivity; + private final int mFullTextAnimationDuration; + + public DetailsContentPresenter(Activity activity) { + super(); + mActivity = activity; + mFullTextAnimationDuration = mActivity.getResources() + .getInteger(R.integer.dvr_details_full_text_animation_duration); + } + + @Override + public final ViewHolder onCreateViewHolder(ViewGroup parent) { + View v = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.dvr_details_description, parent, false); + return new ViewHolder(v); + } + + @Override + public final void onBindViewHolder(Presenter.ViewHolder viewHolder, Object item) { + final ViewHolder vh = (ViewHolder) viewHolder; + final DetailsContent detailsContent = (DetailsContent) item; + + vh.mActivity = mActivity; + vh.mFullTextAnimationDuration = mFullTextAnimationDuration; + + boolean hasTitle = true; + if (TextUtils.isEmpty(detailsContent.getTitle())) { + vh.mTitle.setVisibility(View.GONE); + hasTitle = false; + } else { + vh.mTitle.setText(detailsContent.getTitle()); + vh.mTitle.setVisibility(View.VISIBLE); + vh.mTitle.setLineSpacing(vh.mTitleLineSpacing - vh.mTitle.getLineHeight() + + vh.mTitle.getLineSpacingExtra(), vh.mTitle.getLineSpacingMultiplier()); + vh.mTitle.setMaxLines(vh.mTitleMaxLines); + } + setTopMargin(vh.mTitle, vh.mTitleMargin); + + boolean hasSubtitle = true; + if (detailsContent.getStartTimeUtcMillis() != DetailsContent.INVALID_TIME + && detailsContent.getEndTimeUtcMillis() != DetailsContent.INVALID_TIME) { + vh.mSubtitle.setText(Utils.getDurationString(viewHolder.view.getContext(), + detailsContent.getStartTimeUtcMillis(), + detailsContent.getEndTimeUtcMillis(), false)); + vh.mSubtitle.setVisibility(View.VISIBLE); + if (hasTitle) { + setTopMargin(vh.mSubtitle, vh.mUnderTitleBaselineMargin + + vh.mSubtitleFontMetricsInt.ascent - vh.mTitleFontMetricsInt.descent); + } else { + setTopMargin(vh.mSubtitle, 0); + } + } else { + vh.mSubtitle.setVisibility(View.GONE); + hasSubtitle = false; + } + + if (TextUtils.isEmpty(detailsContent.getDescription())) { + vh.mBody.setVisibility(View.GONE); + } else { + vh.mBody.setText(detailsContent.getDescription()); + vh.mBody.setVisibility(View.VISIBLE); + vh.mBody.setLineSpacing(vh.mBodyLineSpacing - vh.mBody.getLineHeight() + + vh.mBody.getLineSpacingExtra(), vh.mBody.getLineSpacingMultiplier()); + if (hasSubtitle) { + setTopMargin(vh.mDescriptionContainer, vh.mUnderSubtitleBaselineMargin + + vh.mBodyFontMetricsInt.ascent - vh.mSubtitleFontMetricsInt.descent + - vh.mBody.getPaddingTop()); + } else if (hasTitle) { + setTopMargin(vh.mDescriptionContainer, vh.mUnderTitleBaselineMargin + + vh.mBodyFontMetricsInt.ascent - vh.mTitleFontMetricsInt.descent + - vh.mBody.getPaddingTop()); + } else { + setTopMargin(vh.mDescriptionContainer, 0); + } + } + } + + @Override + public void onUnbindViewHolder(Presenter.ViewHolder viewHolder) { } + + private void setTopMargin(View view, int topMargin) { + ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) view.getLayoutParams(); + lp.topMargin = topMargin; + view.setLayoutParams(lp); + } +} \ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/browse/DetailsViewBackgroundHelper.java b/src/com/android/tv/dvr/ui/browse/DetailsViewBackgroundHelper.java new file mode 100644 index 00000000..82fe9ce3 --- /dev/null +++ b/src/com/android/tv/dvr/ui/browse/DetailsViewBackgroundHelper.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tv.dvr.ui.browse; + +import android.app.Activity; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.os.Handler; +import android.support.v17.leanback.app.BackgroundManager; + +/** + * The Background Helper. + */ +class DetailsViewBackgroundHelper { + // Background delay serves to avoid kicking off expensive bitmap loading + // in case multiple backgrounds are set in quick succession. + private static final int SET_BACKGROUND_DELAY_MS = 100; + + private final BackgroundManager mBackgroundManager; + + class LoadBackgroundRunnable implements Runnable { + final Drawable mBackGround; + + LoadBackgroundRunnable(Drawable background) { + mBackGround = background; + } + + @Override + public void run() { + if (!mBackgroundManager.isAttached()) { + return; + } + if (mBackGround instanceof BitmapDrawable) { + mBackgroundManager.setBitmap(((BitmapDrawable) mBackGround).getBitmap()); + } + mRunnable = null; + } + } + + private LoadBackgroundRunnable mRunnable; + + private final Handler mHandler = new Handler(); + + public DetailsViewBackgroundHelper(Activity activity) { + mBackgroundManager = BackgroundManager.getInstance(activity); + mBackgroundManager.attach(activity.getWindow()); + } + + /** + * Sets the given image to background. + */ + public void setBackground(Drawable background) { + if (mRunnable != null) { + mHandler.removeCallbacks(mRunnable); + } + mRunnable = new LoadBackgroundRunnable(background); + mHandler.postDelayed(mRunnable, SET_BACKGROUND_DELAY_MS); + } + + /** + * Sets the background color. + */ + public void setBackgroundColor(int color) { + if (mBackgroundManager.isAttached()) { + mBackgroundManager.setColor(color); + } + } + + /** + * Sets the background scrim. + */ + public void setScrim(int color) { + if (mBackgroundManager.isAttached()) { + mBackgroundManager.setDimLayer(new ColorDrawable(color)); + } + } +} diff --git a/src/com/android/tv/dvr/ui/browse/DvrBrowseActivity.java b/src/com/android/tv/dvr/ui/browse/DvrBrowseActivity.java new file mode 100644 index 00000000..2b3dcb25 --- /dev/null +++ b/src/com/android/tv/dvr/ui/browse/DvrBrowseActivity.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tv.dvr.ui.browse; + +import android.app.Activity; +import android.os.Bundle; + +import com.android.tv.R; +import com.android.tv.TvApplication; + +/** + * {@link android.app.Activity} for DVR UI. + */ +public class DvrBrowseActivity extends Activity { + @Override + public void onCreate(Bundle savedInstanceState) { + TvApplication.setCurrentRunningProcess(this, true); + super.onCreate(savedInstanceState); + setContentView(R.layout.dvr_main); + } +} \ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/browse/DvrBrowseFragment.java b/src/com/android/tv/dvr/ui/browse/DvrBrowseFragment.java new file mode 100644 index 00000000..803d1017 --- /dev/null +++ b/src/com/android/tv/dvr/ui/browse/DvrBrowseFragment.java @@ -0,0 +1,634 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tv.dvr.ui.browse; + +import android.content.Context; +import android.os.Bundle; +import android.os.Handler; +import android.support.v17.leanback.app.BrowseFragment; +import android.support.v17.leanback.widget.ArrayObjectAdapter; +import android.support.v17.leanback.widget.ClassPresenterSelector; +import android.support.v17.leanback.widget.HeaderItem; +import android.support.v17.leanback.widget.ListRow; +import android.support.v17.leanback.widget.Presenter; +import android.support.v17.leanback.widget.TitleViewAdapter; +import android.util.Log; +import android.view.View; +import android.view.ViewTreeObserver.OnGlobalFocusChangeListener; + +import com.android.tv.ApplicationSingletons; +import com.android.tv.R; +import com.android.tv.TvApplication; +import com.android.tv.data.GenreItems; +import com.android.tv.dvr.DvrDataManager; +import com.android.tv.dvr.DvrDataManager.OnDvrScheduleLoadFinishedListener; +import com.android.tv.dvr.DvrDataManager.OnRecordedProgramLoadFinishedListener; +import com.android.tv.dvr.DvrDataManager.RecordedProgramListener; +import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; +import com.android.tv.dvr.DvrDataManager.SeriesRecordingListener; +import com.android.tv.dvr.DvrScheduleManager; +import com.android.tv.dvr.data.RecordedProgram; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.data.SeriesRecording; +import com.android.tv.dvr.ui.SortedArrayAdapter; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; + +/** + * {@link BrowseFragment} for DVR functions. + */ +public class DvrBrowseFragment extends BrowseFragment implements + RecordedProgramListener, ScheduledRecordingListener, SeriesRecordingListener, + OnDvrScheduleLoadFinishedListener, OnRecordedProgramLoadFinishedListener { + private static final String TAG = "DvrBrowseFragment"; + private static final boolean DEBUG = false; + + private static final int MAX_RECENT_ITEM_COUNT = 10; + private static final int MAX_SCHEDULED_ITEM_COUNT = 4; + + private RecordedProgramAdapter mRecentAdapter; + private ScheduleAdapter mScheduleAdapter; + private SeriesAdapter mSeriesAdapter; + private RecordedProgramAdapter[] mGenreAdapters = + new RecordedProgramAdapter[GenreItems.getGenreCount() + 1]; + private ListRow mRecentRow; + private ListRow mSeriesRow; + private ListRow[] mGenreRows = new ListRow[GenreItems.getGenreCount() + 1]; + private List mGenreLabels; + private DvrDataManager mDvrDataManager; + private DvrScheduleManager mDvrScheudleManager; + private ArrayObjectAdapter mRowsAdapter; + private ClassPresenterSelector mPresenterSelector; + private final HashMap mSeriesId2LatestProgram = new HashMap<>(); + private final Handler mHandler = new Handler(); + private final OnGlobalFocusChangeListener mOnGlobalFocusChangeListener = + new OnGlobalFocusChangeListener() { + @Override + public void onGlobalFocusChanged(View oldFocus, View newFocus) { + if (oldFocus instanceof RecordingCardView) { + ((RecordingCardView) oldFocus).expandTitle(false, true); + } + if (newFocus instanceof RecordingCardView) { + // If the header transition is ongoing, expand cards immediately without + // animation to make a smooth transition. + ((RecordingCardView) newFocus).expandTitle(true, !isInHeadersTransition()); + } + } + }; + + private final Comparator RECORDED_PROGRAM_COMPARATOR = new Comparator() { + @Override + public int compare(Object lhs, Object rhs) { + if (lhs instanceof SeriesRecording) { + lhs = mSeriesId2LatestProgram.get(((SeriesRecording) lhs).getSeriesId()); + } + if (rhs instanceof SeriesRecording) { + rhs = mSeriesId2LatestProgram.get(((SeriesRecording) rhs).getSeriesId()); + } + if (lhs instanceof RecordedProgram) { + if (rhs instanceof RecordedProgram) { + return RecordedProgram.START_TIME_THEN_ID_COMPARATOR.reversed() + .compare((RecordedProgram) lhs, (RecordedProgram) rhs); + } else { + return -1; + } + } else if (rhs instanceof RecordedProgram) { + return 1; + } else { + return 0; + } + } + }; + + private final Comparator SCHEDULE_COMPARATOR = new Comparator() { + @Override + public int compare(Object lhs, Object rhs) { + if (lhs instanceof ScheduledRecording) { + if (rhs instanceof ScheduledRecording) { + return ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR + .compare((ScheduledRecording) lhs, (ScheduledRecording) rhs); + } else { + return -1; + } + } else if (rhs instanceof ScheduledRecording) { + return 1; + } else { + return 0; + } + } + }; + + private final DvrScheduleManager.OnConflictStateChangeListener mOnConflictStateChangeListener = + new DvrScheduleManager.OnConflictStateChangeListener() { + @Override + public void onConflictStateChange(boolean conflict, ScheduledRecording... schedules) { + if (mScheduleAdapter != null) { + for (ScheduledRecording schedule : schedules) { + onScheduledRecordingConflictStatusChanged(schedule); + } + } + } + }; + + private final Runnable mUpdateRowsRunnable = new Runnable() { + @Override + public void run() { + updateRows(); + } + }; + + @Override + public void onCreate(Bundle savedInstanceState) { + if (DEBUG) Log.d(TAG, "onCreate"); + super.onCreate(savedInstanceState); + Context context = getContext(); + ApplicationSingletons singletons = TvApplication.getSingletons(context); + mDvrDataManager = singletons.getDvrDataManager(); + mDvrScheudleManager = singletons.getDvrScheduleManager(); + mPresenterSelector = new ClassPresenterSelector() + .addClassPresenter(ScheduledRecording.class, + new ScheduledRecordingPresenter(context)) + .addClassPresenter(RecordedProgram.class, new RecordedProgramPresenter(context)) + .addClassPresenter(SeriesRecording.class, new SeriesRecordingPresenter(context)) + .addClassPresenter(FullScheduleCardHolder.class, + new FullSchedulesCardPresenter(context)); + mGenreLabels = new ArrayList<>(Arrays.asList(GenreItems.getLabels(context))); + mGenreLabels.add(getString(R.string.dvr_main_others)); + prepareUiElements(); + if (!startBrowseIfDvrInitialized()) { + if (!mDvrDataManager.isDvrScheduleLoadFinished()) { + mDvrDataManager.addDvrScheduleLoadFinishedListener(this); + } + if (!mDvrDataManager.isRecordedProgramLoadFinished()) { + mDvrDataManager.addRecordedProgramLoadFinishedListener(this); + } + } + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + view.getViewTreeObserver().addOnGlobalFocusChangeListener(mOnGlobalFocusChangeListener); + } + + @Override + public void onDestroyView() { + getView().getViewTreeObserver() + .removeOnGlobalFocusChangeListener(mOnGlobalFocusChangeListener); + super.onDestroyView(); + } + + @Override + public void onDestroy() { + if (DEBUG) Log.d(TAG, "onDestroy"); + mHandler.removeCallbacks(mUpdateRowsRunnable); + mDvrScheudleManager.removeOnConflictStateChangeListener(mOnConflictStateChangeListener); + mDvrDataManager.removeRecordedProgramListener(this); + mDvrDataManager.removeScheduledRecordingListener(this); + mDvrDataManager.removeSeriesRecordingListener(this); + mDvrDataManager.removeDvrScheduleLoadFinishedListener(this); + mDvrDataManager.removeRecordedProgramLoadFinishedListener(this); + mRowsAdapter.clear(); + mSeriesId2LatestProgram.clear(); + for (Presenter presenter : mPresenterSelector.getPresenters()) { + if (presenter instanceof DvrItemPresenter) { + ((DvrItemPresenter) presenter).unbindAllViewHolders(); + } + } + super.onDestroy(); + } + + @Override + public void onDvrScheduleLoadFinished() { + startBrowseIfDvrInitialized(); + mDvrDataManager.removeDvrScheduleLoadFinishedListener(this); + } + + @Override + public void onRecordedProgramLoadFinished() { + startBrowseIfDvrInitialized(); + mDvrDataManager.removeRecordedProgramLoadFinishedListener(this); + } + + @Override + public void onRecordedProgramsAdded(RecordedProgram... recordedPrograms) { + for (RecordedProgram recordedProgram : recordedPrograms) { + handleRecordedProgramAdded(recordedProgram, true); + } + postUpdateRows(); + } + + @Override + public void onRecordedProgramsChanged(RecordedProgram... recordedPrograms) { + for (RecordedProgram recordedProgram : recordedPrograms) { + handleRecordedProgramChanged(recordedProgram); + } + postUpdateRows(); + } + + @Override + public void onRecordedProgramsRemoved(RecordedProgram... recordedPrograms) { + for (RecordedProgram recordedProgram : recordedPrograms) { + handleRecordedProgramRemoved(recordedProgram); + } + postUpdateRows(); + } + + // No need to call updateRows() during ScheduledRecordings' change because + // the row for ScheduledRecordings is always displayed. + @Override + public void onScheduledRecordingAdded(ScheduledRecording... scheduledRecordings) { + for (ScheduledRecording scheduleRecording : scheduledRecordings) { + if (needToShowScheduledRecording(scheduleRecording)) { + mScheduleAdapter.add(scheduleRecording); + } + } + } + + @Override + public void onScheduledRecordingRemoved(ScheduledRecording... scheduledRecordings) { + for (ScheduledRecording scheduleRecording : scheduledRecordings) { + mScheduleAdapter.remove(scheduleRecording); + } + } + + @Override + public void onScheduledRecordingStatusChanged(ScheduledRecording... scheduledRecordings) { + for (ScheduledRecording scheduleRecording : scheduledRecordings) { + if (needToShowScheduledRecording(scheduleRecording)) { + mScheduleAdapter.change(scheduleRecording); + } else { + mScheduleAdapter.removeWithId(scheduleRecording); + } + } + } + + private void onScheduledRecordingConflictStatusChanged(ScheduledRecording... schedules) { + for (ScheduledRecording schedule : schedules) { + if (needToShowScheduledRecording(schedule)) { + if (mScheduleAdapter.contains(schedule)) { + mScheduleAdapter.change(schedule); + } + } else { + mScheduleAdapter.removeWithId(schedule); + } + } + } + + @Override + public void onSeriesRecordingAdded(SeriesRecording... seriesRecordings) { + handleSeriesRecordingsAdded(Arrays.asList(seriesRecordings)); + postUpdateRows(); + } + + @Override + public void onSeriesRecordingRemoved(SeriesRecording... seriesRecordings) { + handleSeriesRecordingsRemoved(Arrays.asList(seriesRecordings)); + postUpdateRows(); + } + + @Override + public void onSeriesRecordingChanged(SeriesRecording... seriesRecordings) { + handleSeriesRecordingsChanged(Arrays.asList(seriesRecordings)); + postUpdateRows(); + } + + // Workaround of b/29108300 + @Override + public void showTitle(int flags) { + flags &= ~TitleViewAdapter.SEARCH_VIEW_VISIBLE; + super.showTitle(flags); + } + + private void prepareUiElements() { + setBadgeDrawable(getActivity().getDrawable(R.drawable.ic_dvr_badge)); + setHeadersState(HEADERS_ENABLED); + setHeadersTransitionOnBackEnabled(false); + setBrandColor(getResources().getColor(R.color.program_guide_side_panel_background, null)); + mRowsAdapter = new ArrayObjectAdapter(new DvrListRowPresenter(getContext())); + setAdapter(mRowsAdapter); + prepareEntranceTransition(); + } + + private boolean startBrowseIfDvrInitialized() { + if (mDvrDataManager.isInitialized()) { + // Setup rows + mRecentAdapter = new RecordedProgramAdapter(MAX_RECENT_ITEM_COUNT); + mScheduleAdapter = new ScheduleAdapter(MAX_SCHEDULED_ITEM_COUNT); + mSeriesAdapter = new SeriesAdapter(); + for (int i = 0; i < mGenreAdapters.length; i++) { + mGenreAdapters[i] = new RecordedProgramAdapter(); + } + // Schedule Recordings. + List schedules = mDvrDataManager.getAllScheduledRecordings(); + onScheduledRecordingAdded(ScheduledRecording.toArray(schedules)); + mScheduleAdapter.addExtraItem(FullScheduleCardHolder.FULL_SCHEDULE_CARD_HOLDER); + // Recorded Programs. + for (RecordedProgram recordedProgram : mDvrDataManager.getRecordedPrograms()) { + handleRecordedProgramAdded(recordedProgram, false); + } + // Series Recordings. Series recordings should be added after recorded programs, because + // we build series recordings' latest program information while adding recorded programs. + List recordings = mDvrDataManager.getSeriesRecordings(); + handleSeriesRecordingsAdded(recordings); + mRecentRow = new ListRow(new HeaderItem( + getString(R.string.dvr_main_recent)), mRecentAdapter); + mRowsAdapter.add(new ListRow(new HeaderItem( + getString(R.string.dvr_main_scheduled)), mScheduleAdapter)); + mSeriesRow = new ListRow(new HeaderItem( + getString(R.string.dvr_main_series)), mSeriesAdapter); + updateRows(); + // Initialize listeners + mDvrDataManager.addRecordedProgramListener(this); + mDvrDataManager.addScheduledRecordingListener(this); + mDvrDataManager.addSeriesRecordingListener(this); + mDvrScheudleManager.addOnConflictStateChangeListener(mOnConflictStateChangeListener); + startEntranceTransition(); + return true; + } + return false; + } + + private void handleRecordedProgramAdded(RecordedProgram recordedProgram, + boolean updateSeriesRecording) { + mRecentAdapter.add(recordedProgram); + String seriesId = recordedProgram.getSeriesId(); + SeriesRecording seriesRecording = null; + if (seriesId != null) { + seriesRecording = mDvrDataManager.getSeriesRecording(seriesId); + RecordedProgram latestProgram = mSeriesId2LatestProgram.get(seriesId); + if (latestProgram == null || RecordedProgram.START_TIME_THEN_ID_COMPARATOR + .compare(latestProgram, recordedProgram) < 0) { + mSeriesId2LatestProgram.put(seriesId, recordedProgram); + if (updateSeriesRecording && seriesRecording != null) { + onSeriesRecordingChanged(seriesRecording); + } + } + } + if (seriesRecording == null) { + for (RecordedProgramAdapter adapter + : getGenreAdapters(recordedProgram.getCanonicalGenres())) { + adapter.add(recordedProgram); + } + } + } + + private void handleRecordedProgramRemoved(RecordedProgram recordedProgram) { + mRecentAdapter.remove(recordedProgram); + String seriesId = recordedProgram.getSeriesId(); + if (seriesId != null) { + SeriesRecording seriesRecording = mDvrDataManager.getSeriesRecording(seriesId); + RecordedProgram latestProgram = + mSeriesId2LatestProgram.get(recordedProgram.getSeriesId()); + if (latestProgram != null && latestProgram.getId() == recordedProgram.getId()) { + if (seriesRecording != null) { + updateLatestRecordedProgram(seriesRecording); + onSeriesRecordingChanged(seriesRecording); + } + } + } + for (RecordedProgramAdapter adapter + : getGenreAdapters(recordedProgram.getCanonicalGenres())) { + adapter.remove(recordedProgram); + } + } + + private void handleRecordedProgramChanged(RecordedProgram recordedProgram) { + mRecentAdapter.change(recordedProgram); + String seriesId = recordedProgram.getSeriesId(); + SeriesRecording seriesRecording = null; + if (seriesId != null) { + seriesRecording = mDvrDataManager.getSeriesRecording(seriesId); + RecordedProgram latestProgram = mSeriesId2LatestProgram.get(seriesId); + if (latestProgram == null || RecordedProgram.START_TIME_THEN_ID_COMPARATOR + .compare(latestProgram, recordedProgram) <= 0) { + mSeriesId2LatestProgram.put(seriesId, recordedProgram); + if (seriesRecording != null) { + onSeriesRecordingChanged(seriesRecording); + } + } else if (latestProgram.getId() == recordedProgram.getId()) { + if (seriesRecording != null) { + updateLatestRecordedProgram(seriesRecording); + onSeriesRecordingChanged(seriesRecording); + } + } + } + if (seriesRecording == null) { + updateGenreAdapters(getGenreAdapters( + recordedProgram.getCanonicalGenres()), recordedProgram); + } else { + updateGenreAdapters(new ArrayList<>(), recordedProgram); + } + } + + private void handleSeriesRecordingsAdded(List seriesRecordings) { + for (SeriesRecording seriesRecording : seriesRecordings) { + mSeriesAdapter.add(seriesRecording); + if (mSeriesId2LatestProgram.get(seriesRecording.getSeriesId()) != null) { + for (RecordedProgramAdapter adapter + : getGenreAdapters(seriesRecording.getCanonicalGenreIds())) { + adapter.add(seriesRecording); + } + } + } + } + + private void handleSeriesRecordingsRemoved(List seriesRecordings) { + for (SeriesRecording seriesRecording : seriesRecordings) { + mSeriesAdapter.remove(seriesRecording); + for (RecordedProgramAdapter adapter + : getGenreAdapters(seriesRecording.getCanonicalGenreIds())) { + adapter.remove(seriesRecording); + } + } + } + + private void handleSeriesRecordingsChanged(List seriesRecordings) { + for (SeriesRecording seriesRecording : seriesRecordings) { + mSeriesAdapter.change(seriesRecording); + if (mSeriesId2LatestProgram.get(seriesRecording.getSeriesId()) != null) { + updateGenreAdapters(getGenreAdapters( + seriesRecording.getCanonicalGenreIds()), seriesRecording); + } else { + // Remove series recording from all genre rows if it has no recorded program + updateGenreAdapters(new ArrayList<>(), seriesRecording); + } + } + } + + private List getGenreAdapters(String[] genres) { + List result = new ArrayList<>(); + if (genres == null || genres.length == 0) { + result.add(mGenreAdapters[mGenreAdapters.length - 1]); + } else { + for (String genre : genres) { + int genreId = GenreItems.getId(genre); + if(genreId >= mGenreAdapters.length) { + Log.d(TAG, "Wrong Genre ID: " + genreId); + } else { + result.add(mGenreAdapters[genreId]); + } + } + } + return result; + } + + private List getGenreAdapters(int[] genreIds) { + List result = new ArrayList<>(); + if (genreIds == null || genreIds.length == 0) { + result.add(mGenreAdapters[mGenreAdapters.length - 1]); + } else { + for (int genreId : genreIds) { + if(genreId >= mGenreAdapters.length) { + Log.d(TAG, "Wrong Genre ID: " + genreId); + } else { + result.add(mGenreAdapters[genreId]); + } + } + } + return result; + } + + private void updateGenreAdapters(List adapters, Object r) { + for (RecordedProgramAdapter adapter : mGenreAdapters) { + if (adapters.contains(adapter)) { + adapter.change(r); + } else { + adapter.remove(r); + } + } + } + + private void postUpdateRows() { + mHandler.removeCallbacks(mUpdateRowsRunnable); + mHandler.post(mUpdateRowsRunnable); + } + + private void updateRows() { + int visibleRowsCount = 1; // Schedule's Row will never be empty + if (mRecentAdapter.isEmpty()) { + mRowsAdapter.remove(mRecentRow); + } else { + if (mRowsAdapter.indexOf(mRecentRow) < 0) { + mRowsAdapter.add(0, mRecentRow); + } + visibleRowsCount++; + } + if (mSeriesAdapter.isEmpty()) { + mRowsAdapter.remove(mSeriesRow); + } else { + if (mRowsAdapter.indexOf(mSeriesRow) < 0) { + mRowsAdapter.add(visibleRowsCount, mSeriesRow); + } + visibleRowsCount++; + } + for (int i = 0; i < mGenreAdapters.length; i++) { + RecordedProgramAdapter adapter = mGenreAdapters[i]; + if (adapter != null) { + if (adapter.isEmpty()) { + mRowsAdapter.remove(mGenreRows[i]); + } else { + if (mGenreRows[i] == null || mRowsAdapter.indexOf(mGenreRows[i]) < 0) { + mGenreRows[i] = new ListRow(new HeaderItem(mGenreLabels.get(i)), adapter); + mRowsAdapter.add(visibleRowsCount, mGenreRows[i]); + } + visibleRowsCount++; + } + } + } + } + + private boolean needToShowScheduledRecording(ScheduledRecording recording) { + int state = recording.getState(); + return state == ScheduledRecording.STATE_RECORDING_IN_PROGRESS + || state == ScheduledRecording.STATE_RECORDING_NOT_STARTED; + } + + private void updateLatestRecordedProgram(SeriesRecording seriesRecording) { + RecordedProgram latestProgram = null; + for (RecordedProgram program : + mDvrDataManager.getRecordedPrograms(seriesRecording.getId())) { + if (latestProgram == null || RecordedProgram + .START_TIME_THEN_ID_COMPARATOR.compare(latestProgram, program) < 0) { + latestProgram = program; + } + } + mSeriesId2LatestProgram.put(seriesRecording.getSeriesId(), latestProgram); + } + + private class ScheduleAdapter extends SortedArrayAdapter { + ScheduleAdapter(int maxItemCount) { + super(mPresenterSelector, SCHEDULE_COMPARATOR, maxItemCount); + } + + @Override + public long getId(Object item) { + if (item instanceof ScheduledRecording) { + return ((ScheduledRecording) item).getId(); + } else { + return -1; + } + } + } + + private class SeriesAdapter extends SortedArrayAdapter { + SeriesAdapter() { + super(mPresenterSelector, new Comparator() { + @Override + public int compare(SeriesRecording lhs, SeriesRecording rhs) { + if (lhs.isStopped() && !rhs.isStopped()) { + return 1; + } else if (!lhs.isStopped() && rhs.isStopped()) { + return -1; + } + return SeriesRecording.PRIORITY_COMPARATOR.compare(lhs, rhs); + } + }); + } + + @Override + public long getId(SeriesRecording item) { + return item.getId(); + } + } + + private class RecordedProgramAdapter extends SortedArrayAdapter { + RecordedProgramAdapter() { + this(Integer.MAX_VALUE); + } + + RecordedProgramAdapter(int maxItemCount) { + super(mPresenterSelector, RECORDED_PROGRAM_COMPARATOR, maxItemCount); + } + + @Override + public long getId(Object item) { + // We takes the inverse number for the ID of recorded programs to make the ID stable. + if (item instanceof SeriesRecording) { + return ((SeriesRecording) item).getId(); + } else if (item instanceof RecordedProgram) { + return -((RecordedProgram) item).getId() - 1; + } else { + return -1; + } + } + } +} \ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/browse/DvrDetailsActivity.java b/src/com/android/tv/dvr/ui/browse/DvrDetailsActivity.java new file mode 100644 index 00000000..30c81e83 --- /dev/null +++ b/src/com/android/tv/dvr/ui/browse/DvrDetailsActivity.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tv.dvr.ui.browse; + +import android.app.Activity; +import android.os.Bundle; +import android.support.v17.leanback.app.DetailsFragment; + +import com.android.tv.R; +import com.android.tv.TvApplication; + +/** + * Activity to show details view in DVR. + */ +public class DvrDetailsActivity extends Activity { + /** + * Name of record id added to the Intent. + */ + public static final String RECORDING_ID = "record_id"; + + /** + * Name of flag added to the Intent to determine if details view should hide "View schedule" + * button. + */ + public static final String HIDE_VIEW_SCHEDULE = "hide_view_schedule"; + + /** + * Name of details view's type added to the intent. + */ + public static final String DETAILS_VIEW_TYPE = "details_view_type"; + + /** + * Name of shared element between activities. + */ + public static final String SHARED_ELEMENT_NAME = "shared_element"; + + /** + * CURRENT_RECORDING_VIEW refers to Current Recordings in DVR. + */ + public static final int CURRENT_RECORDING_VIEW = 1; + + /** + * SCHEDULED_RECORDING_VIEW refers to Scheduled Recordings in DVR. + */ + public static final int SCHEDULED_RECORDING_VIEW = 2; + + /** + * RECORDED_PROGRAM_VIEW refers to Recorded programs in DVR. + */ + public static final int RECORDED_PROGRAM_VIEW = 3; + + /** + * SERIES_RECORDING_VIEW refers to series recording in DVR. + */ + public static final int SERIES_RECORDING_VIEW = 4; + + @Override + public void onCreate(Bundle savedInstanceState) { + TvApplication.setCurrentRunningProcess(this, true); + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_dvr_details); + long recordId = getIntent().getLongExtra(RECORDING_ID, -1); + int detailsViewType = getIntent().getIntExtra(DETAILS_VIEW_TYPE, -1); + boolean hideViewSchedule = getIntent().getBooleanExtra(HIDE_VIEW_SCHEDULE, false); + if (recordId != -1 && detailsViewType != -1 && savedInstanceState == null) { + Bundle args = new Bundle(); + args.putLong(RECORDING_ID, recordId); + DetailsFragment detailsFragment = null; + if (detailsViewType == CURRENT_RECORDING_VIEW) { + detailsFragment = new CurrentRecordingDetailsFragment(); + } else if (detailsViewType == SCHEDULED_RECORDING_VIEW) { + args.putBoolean(HIDE_VIEW_SCHEDULE, hideViewSchedule); + detailsFragment = new ScheduledRecordingDetailsFragment(); + } else if (detailsViewType == RECORDED_PROGRAM_VIEW) { + detailsFragment = new RecordedProgramDetailsFragment(); + } else if (detailsViewType == SERIES_RECORDING_VIEW) { + detailsFragment = new SeriesRecordingDetailsFragment(); + } + detailsFragment.setArguments(args); + getFragmentManager().beginTransaction() + .replace(R.id.dvr_details_view_frame, detailsFragment).commit(); + } + } +} diff --git a/src/com/android/tv/dvr/ui/browse/DvrDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/DvrDetailsFragment.java new file mode 100644 index 00000000..4d3698ef --- /dev/null +++ b/src/com/android/tv/dvr/ui/browse/DvrDetailsFragment.java @@ -0,0 +1,344 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tv.dvr.ui.browse; + +import android.content.Context; +import android.content.Intent; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.media.tv.TvContentRating; +import android.media.tv.TvInputManager; +import android.net.Uri; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v17.leanback.app.DetailsFragment; +import android.support.v17.leanback.widget.ArrayObjectAdapter; +import android.support.v17.leanback.widget.ClassPresenterSelector; +import android.support.v17.leanback.widget.DetailsOverviewRow; +import android.support.v17.leanback.widget.DetailsOverviewRowPresenter; +import android.support.v17.leanback.widget.OnActionClickedListener; +import android.support.v17.leanback.widget.PresenterSelector; +import android.support.v17.leanback.widget.SparseArrayObjectAdapter; +import android.support.v17.leanback.widget.VerticalGridView; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.TextUtils; +import android.text.style.TextAppearanceSpan; +import android.widget.Toast; + +import com.android.tv.R; +import com.android.tv.TvApplication; +import com.android.tv.data.BaseProgram; +import com.android.tv.data.Channel; +import com.android.tv.data.ChannelDataManager; +import com.android.tv.dialog.PinDialogFragment; +import com.android.tv.dvr.data.RecordedProgram; +import com.android.tv.dvr.ui.playback.DvrPlaybackActivity; +import com.android.tv.parental.ParentalControlSettings; +import com.android.tv.util.ImageLoader; +import com.android.tv.util.ToastUtils; +import com.android.tv.util.Utils; + +import java.io.File; + +abstract class DvrDetailsFragment extends DetailsFragment { + private static final int LOAD_LOGO_IMAGE = 1; + private static final int LOAD_BACKGROUND_IMAGE = 2; + + protected DetailsViewBackgroundHelper mBackgroundHelper; + private ArrayObjectAdapter mRowsAdapter; + private DetailsOverviewRow mDetailsOverview; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (!onLoadRecordingDetails(getArguments())) { + getActivity().finish(); + return; + } + mBackgroundHelper = new DetailsViewBackgroundHelper(getActivity()); + setupAdapter(); + onCreateInternal(); + } + + @Override + public void onStart() { + super.onStart(); + // TODO: remove the workaround of b/30401180. + VerticalGridView container = (VerticalGridView) getActivity() + .findViewById(R.id.container_list); + // Need to manually modify offset. Please refer DetailsFragment.setVerticalGridViewLayout. + container.setItemAlignmentOffset(0); + container.setWindowAlignmentOffset( + getResources().getDimensionPixelSize(R.dimen.lb_details_rows_align_top)); + } + + private void setupAdapter() { + DetailsOverviewRowPresenter rowPresenter = new DetailsOverviewRowPresenter( + new DetailsContentPresenter(getActivity())); + rowPresenter.setBackgroundColor(getResources().getColor(R.color.common_tv_background, + null)); + rowPresenter.setSharedElementEnterTransition(getActivity(), + DvrDetailsActivity.SHARED_ELEMENT_NAME); + rowPresenter.setOnActionClickedListener(onCreateOnActionClickedListener()); + mRowsAdapter = new ArrayObjectAdapter(onCreatePresenterSelector(rowPresenter)); + setAdapter(mRowsAdapter); + } + + /** + * Returns details views' rows adapter. + */ + protected ArrayObjectAdapter getRowsAdapter() { + return mRowsAdapter; + } + + /** + * Sets details overview. + */ + protected void setDetailsOverviewRow(DetailsContent detailsContent) { + mDetailsOverview = new DetailsOverviewRow(detailsContent); + mDetailsOverview.setActionsAdapter(onCreateActionsAdapter()); + mRowsAdapter.add(mDetailsOverview); + onLoadLogoAndBackgroundImages(detailsContent); + } + + /** + * Creates and returns presenter selector will be used by rows adaptor. + */ + protected PresenterSelector onCreatePresenterSelector( + DetailsOverviewRowPresenter rowPresenter) { + ClassPresenterSelector presenterSelector = new ClassPresenterSelector(); + presenterSelector.addClassPresenter(DetailsOverviewRow.class, rowPresenter); + return presenterSelector; + } + + /** + * Does customized initialization of subclasses. Since {@link #onCreate(Bundle)} might finish + * activity early when it cannot fetch valid recordings, subclasses' onCreate method should not + * do anything after calling {@link #onCreate(Bundle)}. If there's something subclasses have to + * do after the super class did onCreate, it should override this method and put the codes here. + */ + protected void onCreateInternal() { } + + /** + * Updates actions of details overview. + */ + protected void updateActions() { + mDetailsOverview.setActionsAdapter(onCreateActionsAdapter()); + } + + /** + * Loads recording details according to the arguments the fragment got. + * + * @return false if cannot find valid recordings, else return true. If the return value + * is false, the detail activity and fragment will be ended. + */ + abstract boolean onLoadRecordingDetails(Bundle args); + + /** + * Creates actions users can interact with and their adaptor for this fragment. + */ + abstract SparseArrayObjectAdapter onCreateActionsAdapter(); + + /** + * Creates actions listeners to implement the behavior of the fragment after users click some + * action buttons. + */ + abstract OnActionClickedListener onCreateOnActionClickedListener(); + + /** + * Returns program title with episode number. If the program is null, returns channel name. + */ + protected CharSequence getTitleFromProgram(BaseProgram program, Channel channel) { + String titleWithEpisodeNumber = program.getTitleWithEpisodeNumber(getContext()); + SpannableString title = titleWithEpisodeNumber == null ? null + : new SpannableString(titleWithEpisodeNumber); + if (TextUtils.isEmpty(title)) { + title = new SpannableString(channel != null ? channel.getDisplayName() + : getContext().getResources().getString( + R.string.no_program_information)); + } else { + String programTitle = program.getTitle(); + title.setSpan(new TextAppearanceSpan(getContext(), + R.style.text_appearance_card_view_episode_number), programTitle == null ? 0 + : programTitle.length(), title.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + return title; + } + + /** + * Loads logo and background images for detail fragments. + */ + protected void onLoadLogoAndBackgroundImages(DetailsContent detailsContent) { + Drawable logoDrawable = null; + Drawable backgroundDrawable = null; + if (TextUtils.isEmpty(detailsContent.getLogoImageUri())) { + logoDrawable = getContext().getResources() + .getDrawable(R.drawable.dvr_default_poster, null); + mDetailsOverview.setImageDrawable(logoDrawable); + } + if (TextUtils.isEmpty(detailsContent.getBackgroundImageUri())) { + backgroundDrawable = getContext().getResources() + .getDrawable(R.drawable.dvr_default_poster, null); + mBackgroundHelper.setBackground(backgroundDrawable); + } + if (logoDrawable != null && backgroundDrawable != null) { + return; + } + if (logoDrawable == null && backgroundDrawable == null + && detailsContent.getLogoImageUri().equals( + detailsContent.getBackgroundImageUri())) { + ImageLoader.loadBitmap(getContext(), detailsContent.getLogoImageUri(), + new MyImageLoaderCallback(this, LOAD_LOGO_IMAGE | LOAD_BACKGROUND_IMAGE, + getContext())); + return; + } + if (logoDrawable == null) { + int imageWidth = getResources().getDimensionPixelSize(R.dimen.dvr_details_poster_width); + int imageHeight = getResources() + .getDimensionPixelSize(R.dimen.dvr_details_poster_height); + ImageLoader.loadBitmap(getContext(), detailsContent.getLogoImageUri(), + imageWidth, imageHeight, + new MyImageLoaderCallback(this, LOAD_LOGO_IMAGE, getContext())); + } + if (backgroundDrawable == null) { + ImageLoader.loadBitmap(getContext(), detailsContent.getBackgroundImageUri(), + new MyImageLoaderCallback(this, LOAD_BACKGROUND_IMAGE, getContext())); + } + } + + protected void startPlayback(RecordedProgram recordedProgram, long seekTimeMs) { + if (Utils.isInBundledPackageSet(recordedProgram.getPackageName()) && + !isDataUriAccessible(recordedProgram.getDataUri())) { + // Since cleaning RecordedProgram from forgotten storage will take some time, + // ignore playback until cleaning is finished. + ToastUtils.show(getContext(), + getContext().getResources().getString(R.string.dvr_toast_recording_deleted), + Toast.LENGTH_SHORT); + return; + } + ParentalControlSettings parental = TvApplication.getSingletons(getActivity()) + .getTvInputManagerHelper().getParentalControlSettings(); + if (!parental.isParentalControlsEnabled()) { + launchPlaybackActivity(recordedProgram, seekTimeMs, false); + return; + } + ChannelDataManager channelDataManager = + TvApplication.getSingletons(getActivity()).getChannelDataManager(); + Channel channel = channelDataManager.getChannel(recordedProgram.getChannelId()); + if (channel != null && channel.isLocked()) { + checkPinToPlay(recordedProgram, seekTimeMs); + return; + } + String ratingString = recordedProgram.getContentRating(); + if (TextUtils.isEmpty(ratingString)) { + launchPlaybackActivity(recordedProgram, seekTimeMs, false); + return; + } + String[] ratingList = ratingString.split(","); + TvContentRating[] programRatings = new TvContentRating[ratingList.length]; + for (int i = 0; i < ratingList.length; i++) { + programRatings[i] = TvContentRating.unflattenFromString(ratingList[i]); + } + TvContentRating blockRatings = parental.getBlockedRating(programRatings); + if (blockRatings != null) { + checkPinToPlay(recordedProgram, seekTimeMs); + } else { + launchPlaybackActivity(recordedProgram, seekTimeMs, false); + } + } + + private boolean isDataUriAccessible(Uri dataUri) { + if (dataUri == null || dataUri.getPath() == null) { + return false; + } + try { + File recordedProgramPath = new File(dataUri.getPath()); + if (recordedProgramPath.exists()) { + return true; + } + } catch (SecurityException e) { + } + return false; + } + + private void checkPinToPlay(RecordedProgram recordedProgram, long seekTimeMs) { + new PinDialogFragment(PinDialogFragment.PIN_DIALOG_TYPE_UNLOCK_PROGRAM, + new PinDialogFragment.ResultListener() { + @Override + public void done(boolean success) { + if (success) { + launchPlaybackActivity(recordedProgram, seekTimeMs, true); + } + } + }).show(getActivity().getFragmentManager(), PinDialogFragment.DIALOG_TAG); + } + + private void launchPlaybackActivity(RecordedProgram mRecordedProgram, long seekTimeMs, + boolean pinChecked) { + Intent intent = new Intent(getActivity(), DvrPlaybackActivity.class); + intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_ID, mRecordedProgram.getId()); + if (seekTimeMs != TvInputManager.TIME_SHIFT_INVALID_TIME) { + intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_SEEK_TIME, seekTimeMs); + } + intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_PIN_CHECKED, pinChecked); + getActivity().startActivity(intent); + } + + private static class MyImageLoaderCallback extends + ImageLoader.ImageLoaderCallback { + private final Context mContext; + private final int mLoadType; + + public MyImageLoaderCallback(DvrDetailsFragment fragment, + int loadType, Context context) { + super(fragment); + mLoadType = loadType; + mContext = context; + } + + @Override + public void onBitmapLoaded(DvrDetailsFragment fragment, + @Nullable Bitmap bitmap) { + Drawable drawable; + int loadType = mLoadType; + if (bitmap == null) { + Resources res = mContext.getResources(); + drawable = res.getDrawable(R.drawable.dvr_default_poster, null); + if ((loadType & LOAD_BACKGROUND_IMAGE) != 0 && !fragment.isDetached()) { + loadType &= ~LOAD_BACKGROUND_IMAGE; + fragment.mBackgroundHelper.setBackgroundColor( + res.getColor(R.color.dvr_detail_default_background)); + fragment.mBackgroundHelper.setScrim( + res.getColor(R.color.dvr_detail_default_background_scrim)); + } + } else { + drawable = new BitmapDrawable(mContext.getResources(), bitmap); + } + if (!fragment.isDetached()) { + if ((loadType & LOAD_LOGO_IMAGE) != 0) { + fragment.mDetailsOverview.setImageDrawable(drawable); + } + if ((loadType & LOAD_BACKGROUND_IMAGE) != 0) { + fragment.mBackgroundHelper.setBackground(drawable); + } + } + } + } +} diff --git a/src/com/android/tv/dvr/ui/browse/DvrItemPresenter.java b/src/com/android/tv/dvr/ui/browse/DvrItemPresenter.java new file mode 100644 index 00000000..317b6af3 --- /dev/null +++ b/src/com/android/tv/dvr/ui/browse/DvrItemPresenter.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.dvr.ui.browse; + +import android.app.Activity; +import android.support.annotation.CallSuper; +import android.support.v17.leanback.widget.Presenter; +import android.view.View; +import android.view.View.OnClickListener; + +import com.android.tv.dvr.ui.DvrUiHelper; + +import java.util.HashSet; +import java.util.Set; + +/** + * An abstract class to present DVR items in {@link RecordingCardView}, which is mainly used in + * {@link DvrBrowseFragment}. DVR items might include: + * {@link com.android.tv.dvr.data.ScheduledRecording}, + * {@link com.android.tv.dvr.data.RecordedProgram}, and + * {@link com.android.tv.dvr.data.SeriesRecording}. + */ +public abstract class DvrItemPresenter extends Presenter { + private final Set mBoundViewHolders = new HashSet<>(); + private final OnClickListener mOnClickListener = onCreateOnClickListener(); + + @Override + @CallSuper + public void onBindViewHolder(ViewHolder viewHolder, Object o) { + viewHolder.view.setTag(o); + viewHolder.view.setOnClickListener(mOnClickListener); + mBoundViewHolders.add(viewHolder); + } + + @Override + @CallSuper + public void onUnbindViewHolder(ViewHolder viewHolder) { + mBoundViewHolders.remove(viewHolder); + viewHolder.view.setTag(null); + viewHolder.view.setOnClickListener(null); + } + + /** + * Unbinds all bound view holders. + */ + public void unbindAllViewHolders() { + // When browse fragments are destroyed, RecyclerView would not call presenters' + // onUnbindViewHolder(). We should handle it by ourselves to prevent resources leaks. + for (ViewHolder viewHolder : new HashSet<>(mBoundViewHolders)) { + onUnbindViewHolder(viewHolder); + } + } + + /** + * Creates {@link OnClickListener} for DVR library's card views. + */ + protected OnClickListener onCreateOnClickListener() { + return new OnClickListener() { + @Override + public void onClick(View view) { + if (view instanceof RecordingCardView) { + RecordingCardView v = (RecordingCardView) view; + DvrUiHelper.startDetailsActivity((Activity) v.getContext(), + v.getTag(), v.getImageView(), false); + } + } + }; + } +} \ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/browse/DvrListRowPresenter.java b/src/com/android/tv/dvr/ui/browse/DvrListRowPresenter.java new file mode 100644 index 00000000..37a72eaf --- /dev/null +++ b/src/com/android/tv/dvr/ui/browse/DvrListRowPresenter.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.dvr.ui.browse; + +import android.content.Context; +import android.support.v17.leanback.widget.ListRowPresenter; +import android.view.ViewGroup; + +import com.android.tv.R; + +/** A list row presenter to display expand/fold card views list. */ +public class DvrListRowPresenter extends ListRowPresenter { + public DvrListRowPresenter(Context context) { + super(); + setRowHeight(ViewGroup.LayoutParams.WRAP_CONTENT); + setExpandedRowHeight( + context.getResources() + .getDimensionPixelSize(R.dimen.dvr_library_expanded_row_height)); + } +} diff --git a/src/com/android/tv/dvr/ui/browse/FullScheduleCardHolder.java b/src/com/android/tv/dvr/ui/browse/FullScheduleCardHolder.java new file mode 100644 index 00000000..311137a9 --- /dev/null +++ b/src/com/android/tv/dvr/ui/browse/FullScheduleCardHolder.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.dvr.ui.browse; + +/** + * Special object for schedule preview; + */ +final class FullScheduleCardHolder { + /** + * Full schedule card holder. + */ + static final FullScheduleCardHolder FULL_SCHEDULE_CARD_HOLDER = new FullScheduleCardHolder(); + + private FullScheduleCardHolder() { } +} diff --git a/src/com/android/tv/dvr/ui/browse/FullSchedulesCardPresenter.java b/src/com/android/tv/dvr/ui/browse/FullSchedulesCardPresenter.java new file mode 100644 index 00000000..6d4763d4 --- /dev/null +++ b/src/com/android/tv/dvr/ui/browse/FullSchedulesCardPresenter.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.dvr.ui.browse; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.view.View; +import android.view.ViewGroup; + +import com.android.tv.R; +import com.android.tv.TvApplication; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.ui.DvrUiHelper; +import com.android.tv.util.Utils; + +import java.util.Collections; +import java.util.List; + +/** + * Presents a {@link ScheduledRecording} in the {@link DvrBrowseFragment}. + */ +class FullSchedulesCardPresenter extends DvrItemPresenter { + private Context mContext; + private final Drawable mIconDrawable; + private final String mCardTitleText; + + public FullSchedulesCardPresenter(Context context) { + mContext = context; + mIconDrawable = mContext.getDrawable(R.drawable.dvr_full_schedule); + mCardTitleText = mContext.getString(R.string.dvr_full_schedule_card_view_title); + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent) { + Context context = parent.getContext(); + RecordingCardView view = new RecordingCardView(context); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(ViewHolder vh, Object o) { + final RecordingCardView cardView = (RecordingCardView) vh.view; + + cardView.setImage(mIconDrawable); + cardView.setTitle(mCardTitleText); + List scheduledRecordings = TvApplication.getSingletons(mContext) + .getDvrDataManager().getAvailableScheduledRecordings(); + int fullDays = 0; + if (!scheduledRecordings.isEmpty()) { + fullDays = Utils.computeDateDifference(System.currentTimeMillis(), + Collections.max(scheduledRecordings, ScheduledRecording.START_TIME_COMPARATOR) + .getStartTimeMs()) + 1; + } + cardView.setContent(mContext.getResources().getQuantityString( + R.plurals.dvr_full_schedule_card_view_content, fullDays, fullDays), null); + super.onBindViewHolder(vh, o); + } + + @Override + public void onUnbindViewHolder(ViewHolder vh) { + ((RecordingCardView) vh.view).reset(); + super.onUnbindViewHolder(vh); + } + + @Override + protected View.OnClickListener onCreateOnClickListener() { + return new View.OnClickListener() { + @Override + public void onClick(View view) { + DvrUiHelper.startSchedulesActivity(mContext, null); + } + }; + } +} \ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/browse/RecordedProgramDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/RecordedProgramDetailsFragment.java new file mode 100644 index 00000000..fe9b9de5 --- /dev/null +++ b/src/com/android/tv/dvr/ui/browse/RecordedProgramDetailsFragment.java @@ -0,0 +1,170 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tv.dvr.ui.browse; + +import android.content.res.Resources; +import android.media.tv.TvInputManager; +import android.os.Bundle; +import android.support.v17.leanback.widget.Action; +import android.support.v17.leanback.widget.OnActionClickedListener; +import android.support.v17.leanback.widget.SparseArrayObjectAdapter; +import android.text.TextUtils; + +import com.android.tv.R; +import com.android.tv.TvApplication; +import com.android.tv.data.Channel; +import com.android.tv.dvr.DvrDataManager; +import com.android.tv.dvr.DvrManager; +import com.android.tv.dvr.DvrWatchedPositionManager; +import com.android.tv.dvr.data.RecordedProgram; + +/** + * {@link android.support.v17.leanback.app.DetailsFragment} for recorded program in DVR. + */ +public class RecordedProgramDetailsFragment extends DvrDetailsFragment + implements DvrDataManager.RecordedProgramListener { + private static final int ACTION_RESUME_PLAYING = 1; + private static final int ACTION_PLAY_FROM_BEGINNING = 2; + private static final int ACTION_DELETE_RECORDING = 3; + + private DvrWatchedPositionManager mDvrWatchedPositionManager; + + private RecordedProgram mRecordedProgram; + private DetailsContent mDetailsContent; + private boolean mPaused; + private DvrDataManager mDvrDataManager; + + @Override + public void onCreate(Bundle savedInstanceState) { + mDvrDataManager = TvApplication.getSingletons(getContext()).getDvrDataManager(); + mDvrDataManager.addRecordedProgramListener(this); + super.onCreate(savedInstanceState); + } + + @Override + public void onCreateInternal() { + mDvrWatchedPositionManager = TvApplication.getSingletons(getActivity()) + .getDvrWatchedPositionManager(); + setDetailsOverviewRow(mDetailsContent); + } + + @Override + public void onResume() { + super.onResume(); + if (mPaused) { + updateActions(); + mPaused = false; + } + } + + @Override + public void onPause() { + super.onPause(); + mPaused = true; + } + + @Override + public void onDestroy() { + mDvrDataManager.removeRecordedProgramListener(this); + super.onDestroy(); + } + + @Override + protected boolean onLoadRecordingDetails(Bundle args) { + long recordedProgramId = args.getLong(DvrDetailsActivity.RECORDING_ID); + mRecordedProgram = mDvrDataManager.getRecordedProgram(recordedProgramId); + if (mRecordedProgram == null) { + // notify super class to end activity before initializing anything + return false; + } + mDetailsContent = createDetailsContent(); + return true; + } + + private DetailsContent createDetailsContent() { + Channel channel = TvApplication.getSingletons(getContext()).getChannelDataManager() + .getChannel(mRecordedProgram.getChannelId()); + String description = TextUtils.isEmpty(mRecordedProgram.getLongDescription()) + ? mRecordedProgram.getDescription() : mRecordedProgram.getLongDescription(); + return new DetailsContent.Builder() + .setTitle(getTitleFromProgram(mRecordedProgram, channel)) + .setStartTimeUtcMillis(mRecordedProgram.getStartTimeUtcMillis()) + .setEndTimeUtcMillis(mRecordedProgram.getEndTimeUtcMillis()) + .setDescription(description) + .setImageUris(mRecordedProgram, channel) + .build(); + } + + @Override + protected SparseArrayObjectAdapter onCreateActionsAdapter() { + SparseArrayObjectAdapter adapter = + new SparseArrayObjectAdapter(new ActionPresenterSelector()); + Resources res = getResources(); + if (mDvrWatchedPositionManager.getWatchedStatus(mRecordedProgram) + == DvrWatchedPositionManager.DVR_WATCHED_STATUS_WATCHING) { + adapter.set(ACTION_RESUME_PLAYING, new Action(ACTION_RESUME_PLAYING, + res.getString(R.string.dvr_detail_resume_play), null, + res.getDrawable(R.drawable.lb_ic_play))); + adapter.set(ACTION_PLAY_FROM_BEGINNING, new Action(ACTION_PLAY_FROM_BEGINNING, + res.getString(R.string.dvr_detail_play_from_beginning), null, + res.getDrawable(R.drawable.lb_ic_replay))); + } else { + adapter.set(ACTION_PLAY_FROM_BEGINNING, new Action(ACTION_PLAY_FROM_BEGINNING, + res.getString(R.string.dvr_detail_watch), null, + res.getDrawable(R.drawable.lb_ic_play))); + } + adapter.set(ACTION_DELETE_RECORDING, new Action(ACTION_DELETE_RECORDING, + res.getString(R.string.dvr_detail_delete), null, + res.getDrawable(R.drawable.ic_delete_32dp))); + return adapter; + } + + @Override + protected OnActionClickedListener onCreateOnActionClickedListener() { + return new OnActionClickedListener() { + @Override + public void onActionClicked(Action action) { + if (action.getId() == ACTION_PLAY_FROM_BEGINNING) { + startPlayback(mRecordedProgram, TvInputManager.TIME_SHIFT_INVALID_TIME); + } else if (action.getId() == ACTION_RESUME_PLAYING) { + startPlayback(mRecordedProgram, mDvrWatchedPositionManager + .getWatchedPosition(mRecordedProgram.getId())); + } else if (action.getId() == ACTION_DELETE_RECORDING) { + DvrManager dvrManager = TvApplication + .getSingletons(getActivity()).getDvrManager(); + dvrManager.removeRecordedProgram(mRecordedProgram); + getActivity().finish(); + } + } + }; + } + + @Override + public void onRecordedProgramsAdded(RecordedProgram... recordedPrograms) { } + + @Override + public void onRecordedProgramsChanged(RecordedProgram... recordedPrograms) { } + + @Override + public void onRecordedProgramsRemoved(RecordedProgram... recordedPrograms) { + for (RecordedProgram recordedProgram : recordedPrograms) { + if (recordedProgram.getId() == mRecordedProgram.getId()) { + getActivity().finish(); + } + } + } +} diff --git a/src/com/android/tv/dvr/ui/browse/RecordedProgramPresenter.java b/src/com/android/tv/dvr/ui/browse/RecordedProgramPresenter.java new file mode 100644 index 00000000..ee978797 --- /dev/null +++ b/src/com/android/tv/dvr/ui/browse/RecordedProgramPresenter.java @@ -0,0 +1,179 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.dvr.ui.browse; + +import android.content.Context; +import android.media.tv.TvContract; +import android.media.tv.TvInputManager; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.TextUtils; +import android.text.style.TextAppearanceSpan; +import android.view.ViewGroup; + +import com.android.tv.R; +import com.android.tv.TvApplication; +import com.android.tv.data.Channel; +import com.android.tv.data.ChannelDataManager; +import com.android.tv.dvr.DvrWatchedPositionManager; +import com.android.tv.dvr.DvrWatchedPositionManager.WatchedPositionChangedListener; +import com.android.tv.dvr.data.RecordedProgram; +import com.android.tv.util.Utils; + +/** + * Presents a {@link RecordedProgram} in the {@link DvrBrowseFragment}. + */ +public class RecordedProgramPresenter extends DvrItemPresenter { + private final ChannelDataManager mChannelDataManager; + private final DvrWatchedPositionManager mDvrWatchedPositionManager; + private final Context mContext; + private String mTodayString; + private String mYesterdayString; + private final int mProgressBarColor; + private final boolean mShowEpisodeTitle; + private final boolean mExpandTitleWhenFocused; + + private static final class RecordedProgramViewHolder extends ViewHolder + implements WatchedPositionChangedListener { + private RecordedProgram mProgram; + + RecordedProgramViewHolder(RecordingCardView view, int progressColor) { + super(view); + view.setProgressBarColor(progressColor); + } + + private void setProgram(RecordedProgram program) { + mProgram = program; + } + + private void setProgressBar(long watchedPositionMs) { + ((RecordingCardView) view).setProgressBar( + (watchedPositionMs == TvInputManager.TIME_SHIFT_INVALID_TIME) ? null + : Math.min(100, (int) (100.0f * watchedPositionMs + / mProgram.getDurationMillis()))); + } + + @Override + public void onWatchedPositionChanged(long programId, long positionMs) { + if (programId == mProgram.getId()) { + setProgressBar(positionMs); + } + } + } + + public RecordedProgramPresenter(Context context, boolean showEpisodeTitle, + boolean expandTitleWhenFocused) { + mContext = context; + mChannelDataManager = TvApplication.getSingletons(mContext).getChannelDataManager(); + mTodayString = mContext.getString(R.string.dvr_date_today); + mYesterdayString = mContext.getString(R.string.dvr_date_yesterday); + mDvrWatchedPositionManager = + TvApplication.getSingletons(mContext).getDvrWatchedPositionManager(); + mProgressBarColor = mContext.getResources() + .getColor(R.color.play_controls_progress_bar_watched); + mShowEpisodeTitle = showEpisodeTitle; + mExpandTitleWhenFocused = expandTitleWhenFocused; + } + + public RecordedProgramPresenter(Context context) { + this(context, false, false); + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent) { + RecordingCardView view = new RecordingCardView(mContext, mExpandTitleWhenFocused); + return new RecordedProgramViewHolder(view, mProgressBarColor); + } + + @Override + public void onBindViewHolder(ViewHolder viewHolder, Object o) { + final RecordedProgram program = (RecordedProgram) o; + final RecordingCardView cardView = (RecordingCardView) viewHolder.view; + Channel channel = mChannelDataManager.getChannel(program.getChannelId()); + String titleString = mShowEpisodeTitle ? program.getEpisodeDisplayTitle(mContext) + : program.getTitleWithEpisodeNumber(mContext); + SpannableString title = titleString == null ? null : new SpannableString(titleString); + if (TextUtils.isEmpty(title)) { + title = new SpannableString(channel != null ? channel.getDisplayName() + : mContext.getResources().getString(R.string.no_program_information)); + } else if (!mShowEpisodeTitle) { + // TODO: Some translation may add delimiters in-between program titles, we should use + // a more robust way to get the span range. + String programTitle = program.getTitle(); + title.setSpan(new TextAppearanceSpan(mContext, + R.style.text_appearance_card_view_episode_number), programTitle == null ? 0 + : programTitle.length(), title.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + cardView.setTitle(title); + String imageUri = null; + boolean isChannelLogo = false; + if (program.getPosterArtUri() != null) { + imageUri = program.getPosterArtUri(); + } else if (program.getThumbnailUri() != null) { + imageUri = program.getThumbnailUri(); + } else if (channel != null) { + imageUri = TvContract.buildChannelLogoUri(channel.getId()).toString(); + isChannelLogo = true; + } + cardView.setImageUri(imageUri, isChannelLogo); + int durationMinutes = Math.max(1, Utils.getRoundOffMinsFromMs(program.getDurationMillis())); + String durationString = getContext().getResources().getQuantityString( + R.plurals.dvr_program_duration, durationMinutes, durationMinutes); + cardView.setContent(getDescription(program), durationString); + if (viewHolder instanceof RecordedProgramViewHolder) { + RecordedProgramViewHolder cardViewHolder = (RecordedProgramViewHolder) viewHolder; + cardViewHolder.setProgram(program); + mDvrWatchedPositionManager.addListener(cardViewHolder, program.getId()); + cardViewHolder + .setProgressBar(mDvrWatchedPositionManager.getWatchedPosition(program.getId())); + } + super.onBindViewHolder(viewHolder, o); + } + + @Override + public void onUnbindViewHolder(ViewHolder viewHolder) { + if (viewHolder instanceof RecordedProgramViewHolder) { + mDvrWatchedPositionManager.removeListener((RecordedProgramViewHolder) viewHolder, + ((RecordedProgramViewHolder) viewHolder).mProgram.getId()); + } + ((RecordingCardView) viewHolder.view).reset(); + super.onUnbindViewHolder(viewHolder); + } + + /** + * Returns description would be used in its card view. + */ + protected String getDescription(RecordedProgram recording) { + int dateDifference = Utils.computeDateDifference(recording.getStartTimeUtcMillis(), + System.currentTimeMillis()); + if (dateDifference == 0) { + return mTodayString; + } else if (dateDifference == 1) { + return mYesterdayString; + } else { + return Utils.getDurationString(mContext, recording.getStartTimeUtcMillis(), + recording.getStartTimeUtcMillis(), false, true, false, 0); + } + } + + /** + * Returns context. + */ + protected Context getContext() { + return mContext; + } +} diff --git a/src/com/android/tv/dvr/ui/browse/RecordingCardView.java b/src/com/android/tv/dvr/ui/browse/RecordingCardView.java new file mode 100644 index 00000000..7b0a8cb9 --- /dev/null +++ b/src/com/android/tv/dvr/ui/browse/RecordingCardView.java @@ -0,0 +1,264 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.dvr.ui.browse; + +import android.animation.ValueAnimator; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Rect; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.support.annotation.Nullable; +import android.support.v17.leanback.widget.BaseCardView; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.ProgressBar; +import android.widget.TextView; + +import com.android.tv.R; +import com.android.tv.dvr.data.RecordedProgram; +import com.android.tv.ui.ViewUtils; +import com.android.tv.util.ImageLoader; + +/** + * A CardView for displaying info about a {@link com.android.tv.dvr.data.ScheduledRecording} + * or {@link RecordedProgram} or {@link com.android.tv.dvr.data.SeriesRecording}. + */ +public class RecordingCardView extends BaseCardView { + // This value should be the same with + // android.support.v17.leanback.widget.FocusHighlightHelper.BrowseItemFocusHighlight.DURATION_MS + private final static int ANIMATION_DURATION = 150; + private final ImageView mImageView; + private final int mImageWidth; + private final int mImageHeight; + private String mImageUri; + private final TextView mMajorContentView; + private final TextView mMinorContentView; + private final ProgressBar mProgressBar; + private final View mAffiliatedIconContainer; + private final ImageView mAffiliatedIcon; + private final Drawable mDefaultImage; + private final FrameLayout mTitleArea; + private final TextView mFoldedTitleView; + private final TextView mExpandedTitleView; + private final ValueAnimator mExpandTitleAnimator; + private final int mFoldedTitleHeight; + private final int mExpandedTitleHeight; + private final boolean mExpandTitleWhenFocused; + private boolean mExpanded; + + public RecordingCardView(Context context) { + this(context, false); + } + + public RecordingCardView(Context context, boolean expandTitleWhenFocused) { + this(context, context.getResources().getDimensionPixelSize( + R.dimen.dvr_library_card_image_layout_width), context.getResources() + .getDimensionPixelSize(R.dimen.dvr_library_card_image_layout_height), + expandTitleWhenFocused); + } + + public RecordingCardView(Context context, int imageWidth, int imageHeight, + boolean expandTitleWhenFocused) { + super(context); + //TODO(dvr): move these to the layout XML. + setCardType(BaseCardView.CARD_TYPE_INFO_UNDER_WITH_EXTRA); + setInfoVisibility(BaseCardView.CARD_REGION_VISIBLE_ALWAYS); + setFocusable(true); + setFocusableInTouchMode(true); + mDefaultImage = getResources().getDrawable(R.drawable.dvr_default_poster, null); + + LayoutInflater inflater = LayoutInflater.from(getContext()); + inflater.inflate(R.layout.dvr_recording_card_view, this); + mImageView = (ImageView) findViewById(R.id.image); + mImageWidth = imageWidth; + mImageHeight = imageHeight; + mProgressBar = (ProgressBar) findViewById(R.id.recording_progress); + mAffiliatedIconContainer = findViewById(R.id.affiliated_icon_container); + mAffiliatedIcon = (ImageView) findViewById(R.id.affiliated_icon); + mMajorContentView = (TextView) findViewById(R.id.content_major); + mMinorContentView = (TextView) findViewById(R.id.content_minor); + mTitleArea = (FrameLayout) findViewById(R.id.title_area); + mFoldedTitleView = (TextView) findViewById(R.id.title_one_line); + mExpandedTitleView = (TextView) findViewById(R.id.title_two_lines); + mFoldedTitleHeight = getResources() + .getDimensionPixelSize(R.dimen.dvr_library_card_folded_title_height); + mExpandedTitleHeight = getResources() + .getDimensionPixelSize(R.dimen.dvr_library_card_expanded_title_height); + mExpandTitleAnimator = ValueAnimator.ofFloat(0.0f, 1.0f).setDuration(ANIMATION_DURATION); + mExpandTitleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator valueAnimator) { + float value = (Float) valueAnimator.getAnimatedValue(); + mExpandedTitleView.setAlpha(value); + mFoldedTitleView.setAlpha(1.0f - value); + ViewUtils.setLayoutHeight(mTitleArea, (int) (mFoldedTitleHeight + + (mExpandedTitleHeight - mFoldedTitleHeight) * value)); + } + }); + mExpandTitleWhenFocused = expandTitleWhenFocused; + } + + @Override + protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { + super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); + if (mExpandTitleWhenFocused) { + if (gainFocus) { + expandTitle(true, true); + } else { + expandTitle(false, true); + } + } + } + + /** + * Expands/folds the title area to show program title with two/one lines. + * + * @param expand {@code true} to expand the title area, or {@code false} to fold it. + * @param withAnimation {@code true} to expand/fold with animation. + */ + public void expandTitle(boolean expand, boolean withAnimation) { + if (expand != mExpanded && mFoldedTitleView.getLayout().getEllipsisCount(0) > 0) { + if (withAnimation) { + if (expand) { + mExpandTitleAnimator.start(); + } else { + mExpandTitleAnimator.reverse(); + } + } else { + if (expand) { + mFoldedTitleView.setAlpha(0.0f); + mExpandedTitleView.setAlpha(1.0f); + ViewUtils.setLayoutHeight(mTitleArea, mExpandedTitleHeight); + } else { + mFoldedTitleView.setAlpha(1.0f); + mExpandedTitleView.setAlpha(0.0f); + ViewUtils.setLayoutHeight(mTitleArea, mFoldedTitleHeight); + } + } + mExpanded = expand; + } + } + + void setTitle(CharSequence title) { + mFoldedTitleView.setText(title); + mExpandedTitleView.setText(title); + } + + void setContent(CharSequence majorContent, CharSequence minorContent) { + if (!TextUtils.isEmpty(majorContent)) { + mMajorContentView.setText(majorContent); + mMajorContentView.setVisibility(View.VISIBLE); + } else { + mMajorContentView.setVisibility(View.GONE); + } + if (!TextUtils.isEmpty(minorContent)) { + mMinorContentView.setText(minorContent); + mMinorContentView.setVisibility(View.VISIBLE); + } else { + mMinorContentView.setVisibility(View.GONE); + } + } + + /** + * Sets progress bar. If progress is {@code null}, hides progress bar. + */ + void setProgressBar(Integer progress) { + if (progress == null) { + mProgressBar.setVisibility(View.GONE); + } else { + mProgressBar.setProgress(progress); + mProgressBar.setVisibility(View.VISIBLE); + } + } + + /** + * Sets the color of progress bar. + */ + void setProgressBarColor(int color) { + mProgressBar.getProgressDrawable().setTint(color); + } + + void setImageUri(String uri, boolean isChannelLogo) { + if (isChannelLogo) { + mImageView.setScaleType(ImageView.ScaleType.CENTER_INSIDE); + } else { + mImageView.setScaleType(ImageView.ScaleType.CENTER_CROP); + } + mImageUri = uri; + if (TextUtils.isEmpty(uri)) { + mImageView.setImageDrawable(mDefaultImage); + } else { + ImageLoader.loadBitmap(getContext(), uri, mImageWidth, mImageHeight, + new RecordingCardImageLoaderCallback(this, uri)); + } + } + + /** + * Set image to card view. + */ + public void setImage(Drawable image) { + if (image != null) { + mImageView.setImageDrawable(image); + } + } + + public void setAffiliatedIcon(int imageResId) { + if (imageResId > 0) { + mAffiliatedIconContainer.setVisibility(View.VISIBLE); + mAffiliatedIcon.setImageResource(imageResId); + } else { + mAffiliatedIconContainer.setVisibility(View.INVISIBLE); + } + } + + /** + * Returns image view. + */ + public ImageView getImageView() { + return mImageView; + } + + private static class RecordingCardImageLoaderCallback + extends ImageLoader.ImageLoaderCallback { + private final String mUri; + + RecordingCardImageLoaderCallback(RecordingCardView referent, String uri) { + super(referent); + mUri = uri; + } + + @Override + public void onBitmapLoaded(RecordingCardView view, @Nullable Bitmap bitmap) { + if (bitmap == null || !mUri.equals(view.mImageUri)) { + view.mImageView.setImageDrawable(view.mDefaultImage); + } else { + view.mImageView.setImageDrawable(new BitmapDrawable(view.getResources(), bitmap)); + } + } + } + + public void reset() { + mFoldedTitleView.setText(null); + mExpandedTitleView.setText(null); + setContent(null, null); + mImageView.setImageDrawable(mDefaultImage); + } +} \ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/browse/RecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/RecordingDetailsFragment.java new file mode 100644 index 00000000..a877e05f --- /dev/null +++ b/src/com/android/tv/dvr/ui/browse/RecordingDetailsFragment.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tv.dvr.ui.browse; + +import android.os.Bundle; +import android.support.v17.leanback.app.DetailsFragment; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.TextUtils; +import android.text.style.TextAppearanceSpan; + +import com.android.tv.R; +import com.android.tv.TvApplication; +import com.android.tv.data.Channel; +import com.android.tv.dvr.data.ScheduledRecording; + +/** + * {@link DetailsFragment} for recordings in DVR. + */ +abstract class RecordingDetailsFragment extends DvrDetailsFragment { + private ScheduledRecording mRecording; + + @Override + protected void onCreateInternal() { + setDetailsOverviewRow(createDetailsContent()); + } + + @Override + protected boolean onLoadRecordingDetails(Bundle args) { + long scheduledRecordingId = args.getLong(DvrDetailsActivity.RECORDING_ID); + mRecording = TvApplication.getSingletons(getContext()).getDvrDataManager() + .getScheduledRecording(scheduledRecordingId); + return mRecording != null; + } + + /** + * Returns {@link ScheduledRecording} for the current fragment. + */ + public ScheduledRecording getRecording() { + return mRecording; + } + + private DetailsContent createDetailsContent() { + Channel channel = TvApplication.getSingletons(getContext()).getChannelDataManager() + .getChannel(mRecording.getChannelId()); + SpannableString title = mRecording.getProgramTitleWithEpisodeNumber(getContext()) == null ? + null : new SpannableString(mRecording + .getProgramTitleWithEpisodeNumber(getContext())); + if (TextUtils.isEmpty(title)) { + title = new SpannableString(channel != null ? channel.getDisplayName() + : getContext().getResources().getString( + R.string.no_program_information)); + } else { + String programTitle = mRecording.getProgramTitle(); + title.setSpan(new TextAppearanceSpan(getContext(), + R.style.text_appearance_card_view_episode_number), programTitle == null ? 0 + : programTitle.length(), title.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + String description = !TextUtils.isEmpty(mRecording.getProgramDescription()) ? + mRecording.getProgramDescription() : mRecording.getProgramLongDescription(); + if (TextUtils.isEmpty(description)) { + description = channel != null ? channel.getDescription() : null; + } + return new DetailsContent.Builder() + .setTitle(title) + .setStartTimeUtcMillis(mRecording.getStartTimeMs()) + .setEndTimeUtcMillis(mRecording.getEndTimeMs()) + .setDescription(description) + .setImageUris(mRecording.getProgramPosterArtUri(), + mRecording.getProgramThumbnailUri(), channel) + .build(); + } +} diff --git a/src/com/android/tv/dvr/ui/browse/ScheduledRecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/ScheduledRecordingDetailsFragment.java new file mode 100644 index 00000000..eb0f4f0d --- /dev/null +++ b/src/com/android/tv/dvr/ui/browse/ScheduledRecordingDetailsFragment.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tv.dvr.ui.browse; + +import android.content.res.Resources; +import android.os.Bundle; +import android.support.v17.leanback.widget.Action; +import android.support.v17.leanback.widget.OnActionClickedListener; +import android.support.v17.leanback.widget.SparseArrayObjectAdapter; +import android.text.TextUtils; + +import com.android.tv.R; +import com.android.tv.TvApplication; +import com.android.tv.dvr.DvrManager; +import com.android.tv.dvr.ui.DvrUiHelper; + +/** + * {@link RecordingDetailsFragment} for scheduled recording in DVR. + */ +public class ScheduledRecordingDetailsFragment extends RecordingDetailsFragment { + private static final int ACTION_VIEW_SCHEDULE = 1; + private static final int ACTION_CANCEL = 2; + + private DvrManager mDvrManager; + private Action mScheduleAction; + private boolean mHideViewSchedule; + + @Override + public void onCreate(Bundle savedInstance) { + mDvrManager = TvApplication.getSingletons(getContext()).getDvrManager(); + mHideViewSchedule = getArguments().getBoolean(DvrDetailsActivity.HIDE_VIEW_SCHEDULE); + super.onCreate(savedInstance); + } + + @Override + public void onResume() { + super.onResume(); + if (mScheduleAction != null) { + mScheduleAction.setIcon(getResources().getDrawable(getScheduleIconId())); + } + } + + @Override + protected SparseArrayObjectAdapter onCreateActionsAdapter() { + SparseArrayObjectAdapter adapter = + new SparseArrayObjectAdapter(new ActionPresenterSelector()); + Resources res = getResources(); + if (!mHideViewSchedule) { + mScheduleAction = new Action(ACTION_VIEW_SCHEDULE, + res.getString(R.string.dvr_detail_view_schedule), null, + res.getDrawable(getScheduleIconId())); + adapter.set(ACTION_VIEW_SCHEDULE, mScheduleAction); + } + adapter.set(ACTION_CANCEL, new Action(ACTION_CANCEL, + res.getString(R.string.epg_dvr_dialog_message_remove_recording_schedule), null, + res.getDrawable(R.drawable.ic_dvr_cancel_32dp))); + return adapter; + } + + @Override + protected OnActionClickedListener onCreateOnActionClickedListener() { + return new OnActionClickedListener() { + @Override + public void onActionClicked(Action action) { + long actionId = action.getId(); + if (actionId == ACTION_VIEW_SCHEDULE) { + DvrUiHelper.startSchedulesActivity(getContext(), getRecording()); + } else if (actionId == ACTION_CANCEL) { + mDvrManager.removeScheduledRecording(getRecording()); + getActivity().finish(); + } + } + }; + } + + private int getScheduleIconId() { + if (mDvrManager.isConflicting(getRecording())) { + return R.drawable.ic_warning_white_32dp; + } else { + return R.drawable.ic_schedule_32dp; + } + } +} diff --git a/src/com/android/tv/dvr/ui/browse/ScheduledRecordingPresenter.java b/src/com/android/tv/dvr/ui/browse/ScheduledRecordingPresenter.java new file mode 100644 index 00000000..efc8785a --- /dev/null +++ b/src/com/android/tv/dvr/ui/browse/ScheduledRecordingPresenter.java @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.dvr.ui.browse; + +import android.content.Context; +import android.media.tv.TvContract; +import android.os.Handler; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.TextUtils; +import android.text.style.TextAppearanceSpan; +import android.view.ViewGroup; + +import com.android.tv.ApplicationSingletons; +import com.android.tv.R; +import com.android.tv.TvApplication; +import com.android.tv.data.Channel; +import com.android.tv.data.ChannelDataManager; +import com.android.tv.dvr.DvrManager; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.util.Utils; + +import java.util.concurrent.TimeUnit; + +/** + * Presents a {@link ScheduledRecording} in the {@link DvrBrowseFragment}. + */ +class ScheduledRecordingPresenter extends DvrItemPresenter { + private static final long PROGRESS_UPDATE_INTERVAL_MS = TimeUnit.SECONDS.toMillis(5); + + private final ChannelDataManager mChannelDataManager; + private final DvrManager mDvrManager; + private final Context mContext; + private final int mProgressBarColor; + + private static final class ScheduledRecordingViewHolder extends ViewHolder { + private final Handler mHandler = new Handler(); + private ScheduledRecording mScheduledRecording; + private final Runnable mProgressBarUpdater = new Runnable() { + @Override + public void run() { + updateProgressBar(); + mHandler.postDelayed(this, PROGRESS_UPDATE_INTERVAL_MS); + } + }; + + ScheduledRecordingViewHolder(RecordingCardView view, int progressBarColor) { + super(view); + view.setProgressBarColor(progressBarColor); + } + + private void updateProgressBar() { + if (mScheduledRecording == null) { + return; + } + int recordingState = mScheduledRecording.getState(); + RecordingCardView cardView = (RecordingCardView) view; + if (recordingState == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) { + cardView.setProgressBar(Math.max(0, Math.min((int) (100 * + (System.currentTimeMillis() - mScheduledRecording.getStartTimeMs()) + / mScheduledRecording.getDuration()), 100))); + } else if (recordingState == ScheduledRecording.STATE_RECORDING_FINISHED) { + cardView.setProgressBar(100); + } else { + // Hides progress bar. + cardView.setProgressBar(null); + } + } + + private void startUpdateProgressBar() { + mHandler.post(mProgressBarUpdater); + } + + private void stopUpdateProgressBar() { + mHandler.removeCallbacks(mProgressBarUpdater); + } + } + + public ScheduledRecordingPresenter(Context context) { + mContext = context; + ApplicationSingletons singletons = TvApplication.getSingletons(mContext); + mChannelDataManager = singletons.getChannelDataManager(); + mDvrManager = singletons.getDvrManager(); + mProgressBarColor = mContext.getResources() + .getColor(R.color.play_controls_recording_icon_color_on_focus); + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent) { + RecordingCardView view = new RecordingCardView(mContext); + return new ScheduledRecordingViewHolder(view, mProgressBarColor); + } + + @Override + public void onBindViewHolder(ViewHolder baseHolder, Object o) { + final ScheduledRecordingViewHolder viewHolder = (ScheduledRecordingViewHolder) baseHolder; + final ScheduledRecording recording = (ScheduledRecording) o; + final RecordingCardView cardView = (RecordingCardView) viewHolder.view; + final Context context = viewHolder.view.getContext(); + + setTitleAndImage(cardView, recording); + int dateDifference = Utils.computeDateDifference(System.currentTimeMillis(), + recording.getStartTimeMs()); + if (dateDifference <= 0) { + cardView.setContent(mContext.getString(R.string.dvr_date_today_time, + Utils.getDurationString(context, recording.getStartTimeMs(), + recording.getEndTimeMs(), false, false, true, 0)), null); + } else if (dateDifference == 1) { + cardView.setContent(mContext.getString(R.string.dvr_date_tomorrow_time, + Utils.getDurationString(context, recording.getStartTimeMs(), + recording.getEndTimeMs(), false, false, true, 0)), null); + } else { + cardView.setContent(Utils.getDurationString(context, recording.getStartTimeMs(), + recording.getStartTimeMs(), false, true, false, 0), null); + } + if (mDvrManager.isConflicting(recording)) { + cardView.setAffiliatedIcon(R.drawable.ic_warning_white_32dp); + } else { + cardView.setAffiliatedIcon(0); + } + viewHolder.updateProgressBar(); + viewHolder.mScheduledRecording = recording; + viewHolder.startUpdateProgressBar(); + super.onBindViewHolder(viewHolder, o); + } + + @Override + public void onUnbindViewHolder(ViewHolder baseHolder) { + ScheduledRecordingViewHolder viewHolder = (ScheduledRecordingViewHolder) baseHolder; + viewHolder.stopUpdateProgressBar(); + viewHolder.mScheduledRecording = null; + ((RecordingCardView) viewHolder.view).reset(); + super.onUnbindViewHolder(viewHolder); + } + + private void setTitleAndImage(RecordingCardView cardView, ScheduledRecording recording) { + Channel channel = mChannelDataManager.getChannel(recording.getChannelId()); + SpannableString title = recording.getProgramTitleWithEpisodeNumber(mContext) == null ? + null : new SpannableString(recording.getProgramTitleWithEpisodeNumber(mContext)); + if (TextUtils.isEmpty(title)) { + title = new SpannableString(channel != null ? channel.getDisplayName() + : mContext.getResources().getString(R.string.no_program_information)); + } else { + String programTitle = recording.getProgramTitle(); + title.setSpan(new TextAppearanceSpan(mContext, + R.style.text_appearance_card_view_episode_number), + programTitle == null ? 0 : programTitle.length(), title.length(), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + String imageUri = recording.getProgramPosterArtUri(); + boolean isChannelLogo = false; + if (TextUtils.isEmpty(imageUri)) { + imageUri = channel != null ? + TvContract.buildChannelLogoUri(channel.getId()).toString() : null; + isChannelLogo = true; + } + cardView.setTitle(title); + cardView.setImageUri(imageUri, isChannelLogo); + } +} \ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/browse/SeriesRecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/SeriesRecordingDetailsFragment.java new file mode 100644 index 00000000..f7b60b50 --- /dev/null +++ b/src/com/android/tv/dvr/ui/browse/SeriesRecordingDetailsFragment.java @@ -0,0 +1,369 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tv.dvr.ui.browse; + +import android.content.res.Resources; +import android.graphics.drawable.Drawable; +import android.media.tv.TvInputManager; +import android.os.Bundle; +import android.support.v17.leanback.app.DetailsFragment; +import android.support.v17.leanback.widget.Action; +import android.support.v17.leanback.widget.ArrayObjectAdapter; +import android.support.v17.leanback.widget.ClassPresenterSelector; +import android.support.v17.leanback.widget.DetailsOverviewRow; +import android.support.v17.leanback.widget.DetailsOverviewRowPresenter; +import android.support.v17.leanback.widget.HeaderItem; +import android.support.v17.leanback.widget.ListRow; +import android.support.v17.leanback.widget.OnActionClickedListener; +import android.support.v17.leanback.widget.PresenterSelector; +import android.support.v17.leanback.widget.SparseArrayObjectAdapter; +import android.text.TextUtils; + +import com.android.tv.R; +import com.android.tv.TvApplication; +import com.android.tv.data.BaseProgram; +import com.android.tv.data.Channel; +import com.android.tv.dvr.DvrDataManager; +import com.android.tv.dvr.DvrWatchedPositionManager; +import com.android.tv.dvr.data.RecordedProgram; +import com.android.tv.dvr.data.SeriesRecording; +import com.android.tv.dvr.ui.DvrUiHelper; +import com.android.tv.dvr.ui.SortedArrayAdapter; + +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +/** + * {@link DetailsFragment} for series recording in DVR. + */ +public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implements + DvrDataManager.SeriesRecordingListener, DvrDataManager.RecordedProgramListener { + private static final int ACTION_WATCH = 1; + private static final int ACTION_SERIES_SCHEDULES = 2; + private static final int ACTION_DELETE = 3; + + private DvrWatchedPositionManager mDvrWatchedPositionManager; + private DvrDataManager mDvrDataManager; + + private SeriesRecording mSeries; + // NOTICE: mRecordedPrograms should only be used in creating details fragments. + // After fragments are created, it should be cleared to save resources. + private List mRecordedPrograms; + private RecordedProgram mRecommendRecordedProgram; + private DetailsContent mDetailsContent; + private int mSeasonRowCount; + private SparseArrayObjectAdapter mActionsAdapter; + private Action mDeleteAction; + + private boolean mPaused; + private long mInitialPlaybackPositionMs; + private String mWatchLabel; + private String mResumeLabel; + private Drawable mWatchDrawable; + private RecordedProgramPresenter mRecordedProgramPresenter; + + @Override + public void onCreate(Bundle savedInstanceState) { + mDvrDataManager = TvApplication.getSingletons(getActivity()).getDvrDataManager(); + mWatchLabel = getString(R.string.dvr_detail_watch); + mResumeLabel = getString(R.string.dvr_detail_series_resume); + mWatchDrawable = getResources().getDrawable(R.drawable.lb_ic_play, null); + mRecordedProgramPresenter = new RecordedProgramPresenter(getContext(), true, true); + super.onCreate(savedInstanceState); + } + + @Override + protected void onCreateInternal() { + mDvrWatchedPositionManager = TvApplication.getSingletons(getActivity()) + .getDvrWatchedPositionManager(); + setDetailsOverviewRow(mDetailsContent); + setupRecordedProgramsRow(); + mDvrDataManager.addSeriesRecordingListener(this); + mDvrDataManager.addRecordedProgramListener(this); + mRecordedPrograms = null; + } + + @Override + public void onResume() { + super.onResume(); + if (mPaused) { + updateWatchAction(); + mPaused = false; + } + } + + @Override + public void onPause() { + super.onPause(); + mPaused = true; + } + + private void updateWatchAction() { + List programs = mDvrDataManager.getRecordedPrograms(mSeries.getId()); + Collections.sort(programs, RecordedProgram.EPISODE_COMPARATOR); + mRecommendRecordedProgram = getRecommendProgram(programs); + if (mRecommendRecordedProgram == null) { + mActionsAdapter.clear(ACTION_WATCH); + } else { + String episodeStatus; + if(mDvrWatchedPositionManager.getWatchedStatus(mRecommendRecordedProgram) + == DvrWatchedPositionManager.DVR_WATCHED_STATUS_WATCHING) { + episodeStatus = mResumeLabel; + mInitialPlaybackPositionMs = mDvrWatchedPositionManager + .getWatchedPosition(mRecommendRecordedProgram.getId()); + } else { + episodeStatus = mWatchLabel; + mInitialPlaybackPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME; + } + String episodeDisplayNumber = mRecommendRecordedProgram.getEpisodeDisplayNumber( + getContext()); + mActionsAdapter.set(ACTION_WATCH, new Action(ACTION_WATCH, + episodeStatus, episodeDisplayNumber, mWatchDrawable)); + } + } + + @Override + protected boolean onLoadRecordingDetails(Bundle args) { + long recordId = args.getLong(DvrDetailsActivity.RECORDING_ID); + mSeries = TvApplication.getSingletons(getActivity()).getDvrDataManager() + .getSeriesRecording(recordId); + if (mSeries == null) { + return false; + } + mRecordedPrograms = mDvrDataManager.getRecordedPrograms(mSeries.getId()); + Collections.sort(mRecordedPrograms, RecordedProgram.SEASON_REVERSED_EPISODE_COMPARATOR); + mDetailsContent = createDetailsContent(); + return true; + } + + @Override + protected PresenterSelector onCreatePresenterSelector( + DetailsOverviewRowPresenter rowPresenter) { + ClassPresenterSelector presenterSelector = new ClassPresenterSelector(); + presenterSelector.addClassPresenter(DetailsOverviewRow.class, rowPresenter); + presenterSelector.addClassPresenter(ListRow.class, new DvrListRowPresenter(getContext())); + return presenterSelector; + } + + private DetailsContent createDetailsContent() { + Channel channel = TvApplication.getSingletons(getContext()).getChannelDataManager() + .getChannel(mSeries.getChannelId()); + String description = TextUtils.isEmpty(mSeries.getLongDescription()) + ? mSeries.getDescription() : mSeries.getLongDescription(); + return new DetailsContent.Builder() + .setTitle(mSeries.getTitle()) + .setDescription(description) + .setImageUris(mSeries.getPosterUri(), mSeries.getPhotoUri(), channel) + .build(); + } + + @Override + protected SparseArrayObjectAdapter onCreateActionsAdapter() { + mActionsAdapter = new SparseArrayObjectAdapter(new ActionPresenterSelector()); + Resources res = getResources(); + updateWatchAction(); + mActionsAdapter.set(ACTION_SERIES_SCHEDULES, new Action(ACTION_SERIES_SCHEDULES, + getString(R.string.dvr_detail_view_schedule), null, + res.getDrawable(R.drawable.ic_schedule_32dp, null))); + mDeleteAction = new Action(ACTION_DELETE, + getString(R.string.dvr_detail_series_delete), null, + res.getDrawable(R.drawable.ic_delete_32dp, null)); + if (!mRecordedPrograms.isEmpty()) { + mActionsAdapter.set(ACTION_DELETE, mDeleteAction); + } + return mActionsAdapter; + } + + private void setupRecordedProgramsRow() { + for (RecordedProgram program : mRecordedPrograms) { + addProgram(program); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + mDvrDataManager.removeSeriesRecordingListener(this); + mDvrDataManager.removeRecordedProgramListener(this); + if (mSeries != null) { + mDvrDataManager.checkAndRemoveEmptySeriesRecording(mSeries.getId()); + } + mRecordedProgramPresenter.unbindAllViewHolders(); + } + + @Override + protected OnActionClickedListener onCreateOnActionClickedListener() { + return new OnActionClickedListener() { + @Override + public void onActionClicked(Action action) { + if (action.getId() == ACTION_WATCH) { + startPlayback(mRecommendRecordedProgram, mInitialPlaybackPositionMs); + } else if (action.getId() == ACTION_SERIES_SCHEDULES) { + DvrUiHelper.startSchedulesActivityForSeries(getContext(), mSeries); + } else if (action.getId() == ACTION_DELETE) { + DvrUiHelper.startSeriesDeletionActivity(getContext(), mSeries.getId()); + } + } + }; + } + + /** + * The programs are sorted by season number and episode number. + */ + private RecordedProgram getRecommendProgram(List programs) { + for (int i = programs.size() - 1 ; i >= 0 ; i--) { + RecordedProgram program = programs.get(i); + int watchedStatus = mDvrWatchedPositionManager.getWatchedStatus(program); + if (watchedStatus == DvrWatchedPositionManager.DVR_WATCHED_STATUS_NEW) { + continue; + } + if (watchedStatus == DvrWatchedPositionManager.DVR_WATCHED_STATUS_WATCHING) { + return program; + } + if (i == programs.size() - 1) { + return program; + } else { + return programs.get(i + 1); + } + } + return programs.isEmpty() ? null : programs.get(0); + } + + @Override + public void onSeriesRecordingAdded(SeriesRecording... seriesRecordings) { } + + @Override + public void onSeriesRecordingChanged(SeriesRecording... seriesRecordings) { + for (SeriesRecording series : seriesRecordings) { + if (mSeries.getId() == series.getId()) { + mSeries = series; + } + } + } + + @Override + public void onSeriesRecordingRemoved(SeriesRecording... seriesRecordings) { + for (SeriesRecording series : seriesRecordings) { + if (series.getId() == mSeries.getId()) { + getActivity().finish(); + return; + } + } + } + + @Override + public void onRecordedProgramsAdded(RecordedProgram... recordedPrograms) { + for (RecordedProgram recordedProgram : recordedPrograms) { + if (TextUtils.equals(recordedProgram.getSeriesId(), mSeries.getSeriesId())) { + addProgram(recordedProgram); + if (mActionsAdapter.lookup(ACTION_DELETE) == null) { + mActionsAdapter.set(ACTION_DELETE, mDeleteAction); + } + } + } + } + + @Override + public void onRecordedProgramsChanged(RecordedProgram... recordedPrograms) { + // Do nothing + } + + @Override + public void onRecordedProgramsRemoved(RecordedProgram... recordedPrograms) { + for (RecordedProgram recordedProgram : recordedPrograms) { + if (TextUtils.equals(recordedProgram.getSeriesId(), mSeries.getSeriesId())) { + ListRow row = getSeasonRow(recordedProgram.getSeasonNumber(), false); + if (row != null) { + SeasonRowAdapter adapter = (SeasonRowAdapter) row.getAdapter(); + adapter.remove(recordedProgram); + if (adapter.isEmpty()) { + getRowsAdapter().remove(row); + if (getRowsAdapter().size() == 1) { + // No season rows left. Only DetailsOverviewRow + mActionsAdapter.clear(ACTION_DELETE); + } + } + } + if (recordedProgram.getId() == mRecommendRecordedProgram.getId()) { + updateWatchAction(); + } + } + } + } + + private void addProgram(RecordedProgram program) { + String programSeasonNumber = + TextUtils.isEmpty(program.getSeasonNumber()) ? "" : program.getSeasonNumber(); + getOrCreateSeasonRowAdapter(programSeasonNumber).add(program); + } + + private SeasonRowAdapter getOrCreateSeasonRowAdapter(String seasonNumber) { + ListRow row = getSeasonRow(seasonNumber, true); + return (SeasonRowAdapter) row.getAdapter(); + } + + private ListRow getSeasonRow(String seasonNumber, boolean createNewRow) { + seasonNumber = TextUtils.isEmpty(seasonNumber) ? "" : seasonNumber; + ArrayObjectAdapter rowsAdaptor = getRowsAdapter(); + for (int i = rowsAdaptor.size() - 1; i >= 0; i--) { + Object row = rowsAdaptor.get(i); + if (row instanceof ListRow) { + int compareResult = BaseProgram.numberCompare(seasonNumber, + ((SeasonRowAdapter) ((ListRow) row).getAdapter()).mSeasonNumber); + if (compareResult == 0) { + return (ListRow) row; + } else if (compareResult < 0) { + return createNewRow ? createNewSeasonRow(seasonNumber, i + 1) : null; + } + } + } + return createNewRow ? createNewSeasonRow(seasonNumber, rowsAdaptor.size()) : null; + } + + private ListRow createNewSeasonRow(String seasonNumber, int position) { + String seasonTitle = seasonNumber.isEmpty() ? mSeries.getTitle() + : getString(R.string.dvr_detail_series_season_title, seasonNumber); + HeaderItem header = new HeaderItem(mSeasonRowCount++, seasonTitle); + ClassPresenterSelector selector = new ClassPresenterSelector(); + selector.addClassPresenter(RecordedProgram.class, mRecordedProgramPresenter); + ListRow row = new ListRow(header, new SeasonRowAdapter(selector, + new Comparator() { + @Override + public int compare(RecordedProgram lhs, RecordedProgram rhs) { + return BaseProgram.EPISODE_COMPARATOR.compare(lhs, rhs); + } + }, seasonNumber)); + getRowsAdapter().add(position, row); + return row; + } + + private class SeasonRowAdapter extends SortedArrayAdapter { + private String mSeasonNumber; + + SeasonRowAdapter(PresenterSelector selector, Comparator comparator, + String seasonNumber) { + super(selector, comparator); + mSeasonNumber = seasonNumber; + } + + @Override + public long getId(RecordedProgram program) { + return program.getId(); + } + } +} \ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/browse/SeriesRecordingPresenter.java b/src/com/android/tv/dvr/ui/browse/SeriesRecordingPresenter.java new file mode 100644 index 00000000..af6ecc19 --- /dev/null +++ b/src/com/android/tv/dvr/ui/browse/SeriesRecordingPresenter.java @@ -0,0 +1,233 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.dvr.ui.browse; + +import android.content.Context; +import android.media.tv.TvContract; +import android.media.tv.TvInputManager; +import android.text.TextUtils; +import android.view.ViewGroup; + +import com.android.tv.ApplicationSingletons; +import com.android.tv.R; +import com.android.tv.TvApplication; +import com.android.tv.data.Channel; +import com.android.tv.data.ChannelDataManager; +import com.android.tv.dvr.DvrDataManager; +import com.android.tv.dvr.DvrDataManager.RecordedProgramListener; +import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; +import com.android.tv.dvr.DvrManager; +import com.android.tv.dvr.DvrWatchedPositionManager; +import com.android.tv.dvr.DvrWatchedPositionManager.WatchedPositionChangedListener; +import com.android.tv.dvr.data.RecordedProgram; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.data.SeriesRecording; + +import java.util.List; + +/** + * Presents a {@link SeriesRecording} in {@link DvrBrowseFragment}. + */ +class SeriesRecordingPresenter extends DvrItemPresenter { + private final ChannelDataManager mChannelDataManager; + private final DvrDataManager mDvrDataManager; + private final DvrManager mDvrManager; + private final DvrWatchedPositionManager mWatchedPositionManager; + + private static final class SeriesRecordingViewHolder extends ViewHolder implements + WatchedPositionChangedListener, ScheduledRecordingListener, RecordedProgramListener { + private SeriesRecording mSeriesRecording; + private RecordingCardView mCardView; + private DvrDataManager mDvrDataManager; + private DvrManager mDvrManager; + private DvrWatchedPositionManager mWatchedPositionManager; + + SeriesRecordingViewHolder(RecordingCardView view, DvrDataManager dvrDataManager, + DvrManager dvrManager, DvrWatchedPositionManager watchedPositionManager) { + super(view); + mCardView = view; + mDvrDataManager = dvrDataManager; + mDvrManager = dvrManager; + mWatchedPositionManager = watchedPositionManager; + } + + @Override + public void onWatchedPositionChanged(long recordedProgramId, long positionMs) { + if (positionMs != TvInputManager.TIME_SHIFT_INVALID_TIME) { + mWatchedPositionManager.removeListener(this, recordedProgramId); + updateCardViewContent(); + } + } + + @Override + public void onScheduledRecordingAdded(ScheduledRecording... scheduledRecordings) { + for (ScheduledRecording scheduledRecording : scheduledRecordings) { + if (scheduledRecording.getSeriesRecordingId() == mSeriesRecording.getId()) { + updateCardViewContent(); + return; + } + } + } + + @Override + public void onScheduledRecordingRemoved(ScheduledRecording... scheduledRecordings) { + for (ScheduledRecording scheduledRecording : scheduledRecordings) { + if (scheduledRecording.getSeriesRecordingId() == mSeriesRecording.getId()) { + updateCardViewContent(); + return; + } + } + } + + @Override + public void onRecordedProgramsAdded(RecordedProgram... recordedPrograms) { + boolean needToUpdateCardView = false; + for (RecordedProgram recordedProgram : recordedPrograms) { + if (TextUtils.equals(recordedProgram.getSeriesId(), + mSeriesRecording.getSeriesId())) { + mDvrDataManager.removeScheduledRecordingListener(this); + mWatchedPositionManager.addListener(this, recordedProgram.getId()); + needToUpdateCardView = true; + } + } + if (needToUpdateCardView) { + updateCardViewContent(); + } + } + + @Override + public void onRecordedProgramsRemoved(RecordedProgram... recordedPrograms) { + boolean needToUpdateCardView = false; + for (RecordedProgram recordedProgram : recordedPrograms) { + if (TextUtils.equals(recordedProgram.getSeriesId(), + mSeriesRecording.getSeriesId())) { + if (mWatchedPositionManager.getWatchedPosition(recordedProgram.getId()) + == TvInputManager.TIME_SHIFT_INVALID_TIME) { + mWatchedPositionManager.removeListener(this, recordedProgram.getId()); + } + needToUpdateCardView = true; + } + } + if (needToUpdateCardView) { + updateCardViewContent(); + } + } + + @Override + public void onRecordedProgramsChanged(RecordedProgram... recordedPrograms) { + // Do nothing + } + + @Override + public void onScheduledRecordingStatusChanged(ScheduledRecording... scheduledRecordings) { + // Do nothing + } + + public void onBound(SeriesRecording seriesRecording) { + mSeriesRecording = seriesRecording; + mDvrDataManager.addScheduledRecordingListener(this); + mDvrDataManager.addRecordedProgramListener(this); + for (RecordedProgram recordedProgram : + mDvrDataManager.getRecordedPrograms(mSeriesRecording.getId())) { + if (mWatchedPositionManager.getWatchedPosition(recordedProgram.getId()) + == TvInputManager.TIME_SHIFT_INVALID_TIME) { + mWatchedPositionManager.addListener(this, recordedProgram.getId()); + } + } + updateCardViewContent(); + } + + public void onUnbound() { + mDvrDataManager.removeScheduledRecordingListener(this); + mDvrDataManager.removeRecordedProgramListener(this); + mWatchedPositionManager.removeListener(this); + } + + private void updateCardViewContent() { + int count = 0; + int quantityStringID; + List recordedPrograms = + mDvrDataManager.getRecordedPrograms(mSeriesRecording.getId()); + if (recordedPrograms.size() == 0) { + count = mDvrManager.getAvailableScheduledRecording(mSeriesRecording.getId()).size(); + quantityStringID = R.plurals.dvr_count_scheduled_recordings; + } else { + for (RecordedProgram recordedProgram : recordedPrograms) { + if (mWatchedPositionManager.getWatchedPosition(recordedProgram.getId()) + == TvInputManager.TIME_SHIFT_INVALID_TIME) { + count++; + } + } + if (count == 0) { + count = recordedPrograms.size(); + quantityStringID = R.plurals.dvr_count_recordings; + } else { + quantityStringID = R.plurals.dvr_count_new_recordings; + } + } + mCardView.setContent(mCardView.getResources() + .getQuantityString(quantityStringID, count, count), null); + } + } + + public SeriesRecordingPresenter(Context context) { + ApplicationSingletons singletons = TvApplication.getSingletons(context); + mChannelDataManager = singletons.getChannelDataManager(); + mDvrDataManager = singletons.getDvrDataManager(); + mDvrManager = singletons.getDvrManager(); + mWatchedPositionManager = singletons.getDvrWatchedPositionManager(); + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent) { + Context context = parent.getContext(); + RecordingCardView view = new RecordingCardView(context); + return new SeriesRecordingViewHolder(view, mDvrDataManager, mDvrManager, + mWatchedPositionManager); + } + + @Override + public void onBindViewHolder(ViewHolder baseHolder, Object o) { + final SeriesRecordingViewHolder viewHolder = (SeriesRecordingViewHolder) baseHolder; + final SeriesRecording seriesRecording = (SeriesRecording) o; + final RecordingCardView cardView = (RecordingCardView) viewHolder.view; + viewHolder.onBound(seriesRecording); + setTitleAndImage(cardView, seriesRecording); + super.onBindViewHolder(baseHolder, o); + } + + @Override + public void onUnbindViewHolder(ViewHolder viewHolder) { + ((RecordingCardView) viewHolder.view).reset(); + ((SeriesRecordingViewHolder) viewHolder).onUnbound(); + super.onUnbindViewHolder(viewHolder); + } + + private void setTitleAndImage(RecordingCardView cardView, SeriesRecording recording) { + cardView.setTitle(recording.getTitle()); + if (recording.getPosterUri() != null) { + cardView.setImageUri(recording.getPosterUri(), false); + } else { + Channel channel = mChannelDataManager.getChannel(recording.getChannelId()); + String imageUri = null; + if (channel != null) { + imageUri = TvContract.buildChannelLogoUri(channel.getId()).toString(); + } + cardView.setImageUri(imageUri, true); + } + } +} diff --git a/src/com/android/tv/dvr/ui/list/BaseDvrSchedulesFragment.java b/src/com/android/tv/dvr/ui/list/BaseDvrSchedulesFragment.java index d28f026c..5abd52a1 100644 --- a/src/com/android/tv/dvr/ui/list/BaseDvrSchedulesFragment.java +++ b/src/com/android/tv/dvr/ui/list/BaseDvrSchedulesFragment.java @@ -29,7 +29,7 @@ import com.android.tv.R; import com.android.tv.TvApplication; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrScheduleManager; -import com.android.tv.dvr.ScheduledRecording; +import com.android.tv.dvr.data.ScheduledRecording; /** * A base fragment to show the list of schedule recordings. diff --git a/src/com/android/tv/dvr/ui/list/DvrSchedulesActivity.java b/src/com/android/tv/dvr/ui/list/DvrSchedulesActivity.java new file mode 100644 index 00000000..a0410bb3 --- /dev/null +++ b/src/com/android/tv/dvr/ui/list/DvrSchedulesActivity.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tv.dvr.ui.list; + +import android.app.Activity; +import android.app.ProgressDialog; +import android.os.Bundle; +import android.support.annotation.IntDef; + +import com.android.tv.R; +import com.android.tv.TvApplication; +import com.android.tv.data.Program; +import com.android.tv.dvr.data.SeriesRecording; +import com.android.tv.dvr.provider.EpisodicProgramLoadTask; +import com.android.tv.dvr.recorder.SeriesRecordingScheduler; +import com.android.tv.dvr.ui.BigArguments; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Collections; +import java.util.List; + +/** + * Activity to show the list of recording schedules. + */ +public class DvrSchedulesActivity extends Activity { + /** + * The key for the type of the schedules which will be listed in the list. The type of the value + * should be {@link ScheduleListType}. + */ + public static final String KEY_SCHEDULES_TYPE = "schedules_type"; + + @Retention(RetentionPolicy.SOURCE) + @IntDef({TYPE_FULL_SCHEDULE, TYPE_SERIES_SCHEDULE}) + public @interface ScheduleListType {} + /** + * A type which means the activity will display the full scheduled recordings. + */ + public static final int TYPE_FULL_SCHEDULE = 0; + /** + * A type which means the activity will display a scheduled recording list of a series + * recording. + */ + public static final int TYPE_SERIES_SCHEDULE = 1; + + @Override + public void onCreate(final Bundle savedInstanceState) { + TvApplication.setCurrentRunningProcess(this, true); + // Pass null to prevent automatically re-creating fragments + super.onCreate(null); + setContentView(R.layout.activity_dvr_schedules); + int scheduleType = getIntent().getIntExtra(KEY_SCHEDULES_TYPE, TYPE_FULL_SCHEDULE); + if (scheduleType == TYPE_FULL_SCHEDULE) { + DvrSchedulesFragment schedulesFragment = new DvrSchedulesFragment(); + schedulesFragment.setArguments(getIntent().getExtras()); + getFragmentManager().beginTransaction().add( + R.id.fragment_container, schedulesFragment).commit(); + } else if (scheduleType == TYPE_SERIES_SCHEDULE) { + if (BigArguments.getArgument(DvrSeriesSchedulesFragment + .SERIES_SCHEDULES_KEY_SERIES_PROGRAMS) != null) { + // The programs will be passed to the DvrSeriesSchedulesFragment, so don't need + // to reset the BigArguments. + showDvrSeriesSchedulesFragment(getIntent().getExtras()); + } else { + final ProgressDialog dialog = ProgressDialog.show(this, null, getString( + R.string.dvr_series_progress_message_reading_programs)); + SeriesRecording seriesRecording = getIntent().getExtras() + .getParcelable(DvrSeriesSchedulesFragment + .SERIES_SCHEDULES_KEY_SERIES_RECORDING); + // To get programs faster, hold the update of the series schedules. + SeriesRecordingScheduler.getInstance(this).pauseUpdate(); + new EpisodicProgramLoadTask(this, Collections.singletonList(seriesRecording)) { + @Override + protected void onPostExecute(List programs) { + SeriesRecordingScheduler.getInstance(DvrSchedulesActivity.this) + .resumeUpdate(); + dialog.dismiss(); + Bundle args = getIntent().getExtras(); + BigArguments.reset(); + BigArguments.setArgument( + DvrSeriesSchedulesFragment.SERIES_SCHEDULES_KEY_SERIES_PROGRAMS, + programs == null ? Collections.EMPTY_LIST : programs); + showDvrSeriesSchedulesFragment(args); + } + }.setLoadCurrentProgram(true) + .setLoadDisallowedProgram(true) + .setLoadScheduledEpisode(true) + .setIgnoreChannelOption(true) + .execute(); + } + } else { + finish(); + } + } + + private void showDvrSeriesSchedulesFragment(Bundle args) { + DvrSeriesSchedulesFragment schedulesFragment = new DvrSeriesSchedulesFragment(); + schedulesFragment.setArguments(args); + getFragmentManager().beginTransaction().add( + R.id.fragment_container, schedulesFragment).commit(); + } +} diff --git a/src/com/android/tv/dvr/ui/list/DvrSchedulesFragment.java b/src/com/android/tv/dvr/ui/list/DvrSchedulesFragment.java index 722c9b6e..3cbb500a 100644 --- a/src/com/android/tv/dvr/ui/list/DvrSchedulesFragment.java +++ b/src/com/android/tv/dvr/ui/list/DvrSchedulesFragment.java @@ -18,12 +18,9 @@ package com.android.tv.dvr.ui.list; import android.os.Bundle; import android.support.v17.leanback.widget.ClassPresenterSelector; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; import com.android.tv.R; -import com.android.tv.dvr.ScheduledRecording; +import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.dvr.ui.list.SchedulesHeaderRowPresenter.DateHeaderRowPresenter; /** diff --git a/src/com/android/tv/dvr/ui/list/DvrSeriesSchedulesFragment.java b/src/com/android/tv/dvr/ui/list/DvrSeriesSchedulesFragment.java index 42a1e72b..57e7a88f 100644 --- a/src/com/android/tv/dvr/ui/list/DvrSeriesSchedulesFragment.java +++ b/src/com/android/tv/dvr/ui/list/DvrSeriesSchedulesFragment.java @@ -17,6 +17,7 @@ package com.android.tv.dvr.ui.list; import android.annotation.TargetApi; +import android.content.Context; import android.database.ContentObserver; import android.media.tv.TvContract.Programs; import android.net.Uri; @@ -35,11 +36,13 @@ import com.android.tv.R; import com.android.tv.TvApplication; import com.android.tv.data.ChannelDataManager; import com.android.tv.data.Program; +import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrDataManager.SeriesRecordingListener; -import com.android.tv.dvr.EpisodicProgramLoadTask; -import com.android.tv.dvr.SeriesRecording; -import com.android.tv.dvr.ui.list.SchedulesHeaderRowPresenter.SeriesRecordingHeaderRowPresenter; +import com.android.tv.dvr.data.SeriesRecording; +import com.android.tv.dvr.provider.EpisodicProgramLoadTask; +import com.android.tv.dvr.ui.BigArguments; +import java.util.Collections; import java.util.List; /** @@ -47,20 +50,22 @@ import java.util.List; */ @TargetApi(Build.VERSION_CODES.N) public class DvrSeriesSchedulesFragment extends BaseDvrSchedulesFragment { - private static final String TAG = "DvrSeriesSchedulesFragment"; /** * The key for series recording whose scheduled recording list will be displayed. + * Type: {@link SeriesRecording} */ public static final String SERIES_SCHEDULES_KEY_SERIES_RECORDING = "series_schedules_key_series_recording"; /** - * The key for programs belong to the series recording whose scheduled recording - * list will be displayed. + * The key for programs which belong to the series recording whose scheduled recording list + * will be displayed. + * Type: List<{@link Program}> */ public static final String SERIES_SCHEDULES_KEY_SERIES_PROGRAMS = "series_schedules_key_series_programs"; private ChannelDataManager mChannelDataManager; + private DvrDataManager mDvrDataManager; private SeriesRecording mSeriesRecording; private List mPrograms; private EpisodicProgramLoadTask mProgramLoadTask; @@ -87,20 +92,22 @@ public class DvrSeriesSchedulesFragment extends BaseDvrSchedulesFragment { && getRowsAdapter() instanceof SeriesScheduleRowAdapter) { ((SeriesScheduleRowAdapter) getRowsAdapter()) .onSeriesRecordingUpdated(r); + mSeriesRecording = r; + updateEmptyMessage(); return; } } } }; - private final ContentObserver mContentObserver = - new ContentObserver(new Handler(Looper.getMainLooper())) { - @Override - public void onChange(boolean selfChange, Uri uri) { - super.onChange(selfChange, uri); - executeProgramLoadingTask(); - } - }; + private final Handler mHandler = new Handler(Looper.getMainLooper()); + private final ContentObserver mContentObserver = new ContentObserver(mHandler) { + @Override + public void onChange(boolean selfChange, Uri uri) { + super.onChange(selfChange, uri); + executeProgramLoadingTask(); + } + }; private final ChannelDataManager.Listener mChannelListener = new ChannelDataManager.Listener() { @Override @@ -120,17 +127,28 @@ public class DvrSeriesSchedulesFragment extends BaseDvrSchedulesFragment { } @Override - public void onCreate(Bundle savedInstanceState) { + public void onAttach(Context context) { + super.onAttach(context); Bundle args = getArguments(); if (args != null) { mSeriesRecording = args.getParcelable(SERIES_SCHEDULES_KEY_SERIES_RECORDING); - mPrograms = args.getParcelableArrayList(SERIES_SCHEDULES_KEY_SERIES_PROGRAMS); + mPrograms = (List) BigArguments.getArgument( + SERIES_SCHEDULES_KEY_SERIES_PROGRAMS); + BigArguments.reset(); } + if (args == null || mPrograms == null) { + getActivity().finish(); + } + } + + @Override + public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ApplicationSingletons singletons = TvApplication.getSingletons(getContext()); - singletons.getDvrDataManager().addSeriesRecordingListener(mSeriesRecordingListener); mChannelDataManager = singletons.getChannelDataManager(); mChannelDataManager.addListener(mChannelListener); + mDvrDataManager = singletons.getDvrDataManager(); + mDvrDataManager.addSeriesRecordingListener(mSeriesRecordingListener); getContext().getContentResolver().registerContentObserver(Programs.CONTENT_URI, true, mContentObserver); } @@ -144,8 +162,16 @@ public class DvrSeriesSchedulesFragment extends BaseDvrSchedulesFragment { private void onProgramsUpdated() { ((SeriesScheduleRowAdapter) getRowsAdapter()).setPrograms(mPrograms); + updateEmptyMessage(); + } + + private void updateEmptyMessage() { if (mPrograms == null || mPrograms.isEmpty()) { - showEmptyMessage(R.string.dvr_series_schedules_empty_state); + if (mSeriesRecording.getState() == SeriesRecording.STATE_SERIES_STOPPED) { + showEmptyMessage(R.string.dvr_series_schedules_stopped_empty_state); + } else { + showEmptyMessage(R.string.dvr_series_schedules_empty_state); + } } else { hideEmptyMessage(); } @@ -158,15 +184,15 @@ public class DvrSeriesSchedulesFragment extends BaseDvrSchedulesFragment { mProgramLoadTask = null; } getContext().getContentResolver().unregisterContentObserver(mContentObserver); + mHandler.removeCallbacksAndMessages(null); mChannelDataManager.removeListener(mChannelListener); - TvApplication.getSingletons(getContext()).getDvrDataManager() - .removeSeriesRecordingListener(mSeriesRecordingListener); + mDvrDataManager.removeSeriesRecordingListener(mSeriesRecordingListener); super.onDestroy(); } @Override public SchedulesHeaderRowPresenter onCreateHeaderRowPresenter() { - return new SeriesRecordingHeaderRowPresenter(getContext()); + return new SchedulesHeaderRowPresenter.SeriesRecordingHeaderRowPresenter(getContext()); } @Override @@ -195,7 +221,7 @@ public class DvrSeriesSchedulesFragment extends BaseDvrSchedulesFragment { mProgramLoadTask = new EpisodicProgramLoadTask(getContext(), mSeriesRecording) { @Override protected void onPostExecute(List programs) { - mPrograms = programs; + mPrograms = programs == null ? Collections.EMPTY_LIST : programs; onProgramsUpdated(); } }; @@ -205,4 +231,4 @@ public class DvrSeriesSchedulesFragment extends BaseDvrSchedulesFragment { .setIgnoreChannelOption(true) .execute(); } -} \ No newline at end of file +} diff --git a/src/com/android/tv/dvr/ui/list/EpisodicProgramRow.java b/src/com/android/tv/dvr/ui/list/EpisodicProgramRow.java index 23aebf59..9b0ad105 100644 --- a/src/com/android/tv/dvr/ui/list/EpisodicProgramRow.java +++ b/src/com/android/tv/dvr/ui/list/EpisodicProgramRow.java @@ -19,13 +19,13 @@ package com.android.tv.dvr.ui.list; import android.content.Context; import com.android.tv.data.Program; -import com.android.tv.dvr.ScheduledRecording; -import com.android.tv.dvr.ScheduledRecording.Builder; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.data.ScheduledRecording.Builder; /** * A class for the episodic program. */ -public class EpisodicProgramRow extends ScheduleRow { +class EpisodicProgramRow extends ScheduleRow { private final String mInputId; private final Program mProgram; diff --git a/src/com/android/tv/dvr/ui/list/ScheduleRow.java b/src/com/android/tv/dvr/ui/list/ScheduleRow.java index 3fc92e8a..66e96f94 100644 --- a/src/com/android/tv/dvr/ui/list/ScheduleRow.java +++ b/src/com/android/tv/dvr/ui/list/ScheduleRow.java @@ -20,12 +20,12 @@ import android.content.Context; import android.support.annotation.Nullable; import com.android.tv.common.SoftPreconditions; -import com.android.tv.dvr.ScheduledRecording; +import com.android.tv.dvr.data.ScheduledRecording; /** * A class for schedule recording row. */ -public class ScheduleRow { +class ScheduleRow { private final SchedulesHeaderRow mHeaderRow; @Nullable private ScheduledRecording mSchedule; private boolean mStopRecordingRequested; diff --git a/src/com/android/tv/dvr/ui/list/ScheduleRowAdapter.java b/src/com/android/tv/dvr/ui/list/ScheduleRowAdapter.java index 9cc82653..97d60473 100644 --- a/src/com/android/tv/dvr/ui/list/ScheduleRowAdapter.java +++ b/src/com/android/tv/dvr/ui/list/ScheduleRowAdapter.java @@ -30,8 +30,8 @@ import com.android.tv.R; import com.android.tv.TvApplication; import com.android.tv.common.SoftPreconditions; import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.ScheduledRecording; import com.android.tv.dvr.ui.list.SchedulesHeaderRow.DateHeaderRow; +import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.util.Utils; import java.util.ArrayList; @@ -43,7 +43,7 @@ import java.util.concurrent.TimeUnit; /** * An adapter for {@link ScheduleRow}. */ -public class ScheduleRowAdapter extends ArrayObjectAdapter { +class ScheduleRowAdapter extends ArrayObjectAdapter { private static final String TAG = "ScheduleRowAdapter"; private static final boolean DEBUG = false; diff --git a/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java b/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java index 1257e725..dc4e3c41 100644 --- a/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java +++ b/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java @@ -42,25 +42,24 @@ import com.android.tv.R; import com.android.tv.TvApplication; import com.android.tv.common.SoftPreconditions; import com.android.tv.data.Channel; +import com.android.tv.dialog.HalfSizedDialogFragment; import com.android.tv.dvr.DvrManager; import com.android.tv.dvr.DvrScheduleManager; -import com.android.tv.dvr.DvrUiHelper; -import com.android.tv.dvr.ScheduledRecording; +import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.dvr.ui.DvrStopRecordingFragment; -import com.android.tv.dvr.ui.HalfSizedDialogFragment; +import com.android.tv.dvr.ui.DvrUiHelper; import com.android.tv.util.ToastUtils; import com.android.tv.util.Utils; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.List; -import java.util.concurrent.TimeUnit; /** * A RowPresenter for {@link ScheduleRow}. */ @TargetApi(Build.VERSION_CODES.N) -public class ScheduleRowPresenter extends RowPresenter { +class ScheduleRowPresenter extends RowPresenter { private static final String TAG = "ScheduleRowPresenter"; @Retention(RetentionPolicy.SOURCE) @@ -345,7 +344,9 @@ public class ScheduleRowPresenter extends RowPresenter { viewHolder.mInfoContainer.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { - onInfoClicked(row); + if (isInfoClickable(row)) { + onInfoClicked(row); + } } }); @@ -366,8 +367,7 @@ public class ScheduleRowPresenter extends RowPresenter { viewHolder.mTimeView.setText(onGetRecordingTimeText(row)); String programInfoText = onGetProgramInfoText(row); if (TextUtils.isEmpty(programInfoText)) { - int durationMins = - Math.max((int) TimeUnit.MILLISECONDS.toMinutes(row.getDuration()), 1); + int durationMins = Math.max(1, Utils.getRoundOffMinsFromMs(row.getDuration())); programInfoText = mContext.getResources().getQuantityString( R.plurals.dvr_schedules_recording_duration, durationMins, durationMins); } @@ -403,6 +403,7 @@ public class ScheduleRowPresenter extends RowPresenter { } else { viewHolder.whiteBackInfo(); } + viewHolder.mInfoContainer.setFocusable(isInfoClickable(row)); updateActionContainer(viewHolder, viewHolder.isSelected()); } @@ -454,11 +455,13 @@ public class ScheduleRowPresenter extends RowPresenter { /** * Called when user click Info in {@link ScheduleRow}. */ - protected void onInfoClicked(ScheduleRow scheduleRow) { - ScheduledRecording schedule = scheduleRow.getSchedule(); - if (schedule != null) { - DvrUiHelper.startDetailsActivity((Activity) mContext, schedule, null, true); - } + protected void onInfoClicked(ScheduleRow row) { + DvrUiHelper.startDetailsActivity((Activity) mContext, row.getSchedule(), null, true); + } + + private boolean isInfoClickable(ScheduleRow row) { + return row.getSchedule() != null + && (row.getSchedule().isNotStarted() || row.getSchedule().isInProgress()); } /** @@ -545,7 +548,7 @@ public class ScheduleRowPresenter extends RowPresenter { // This row has been deleted. return; } - if (row.isOnAir() && row.isRecordingInProgress() && !row.isStopRecordingRequested()) { + if (row.isRecordingInProgress() && !row.isStopRecordingRequested()) { row.setStopRecordingRequested(true); mDvrManager.stopRecording(row.getSchedule()); CharSequence deletedInfo = onGetProgramInfoText(row); @@ -670,10 +673,9 @@ public class ScheduleRowPresenter extends RowPresenter { hideActionView(viewHolder.mFirstActionContainer, View.GONE); } }; - if (mLastFocusedViewId == R.id.action_first_container - || mLastFocusedViewId == R.id.action_second_container) { - mLastFocusedViewId = R.id.info_container; - } + mLastFocusedViewId = R.id.info_container; + SoftPreconditions.checkState(viewHolder.mInfoContainer.isFocusable(), TAG, + "No focusable view in this row: " + viewHolder); break; } View view = viewHolder.view.findViewById(mLastFocusedViewId); @@ -683,8 +685,10 @@ public class ScheduleRowPresenter extends RowPresenter { // requestFocus() explicitly. if (view.hasFocus()) { viewHolder.mPendingAnimationRunnable.run(); - } else { + } else if (view.isFocusable()){ view.requestFocus(); + } else { + viewHolder.view.requestFocus(); } } } else { @@ -737,10 +741,10 @@ public class ScheduleRowPresenter extends RowPresenter { @ScheduleRowAction protected int[] getAvailableActions(ScheduleRow row) { if (row.getSchedule() != null) { - if (row.isOnAir()) { - if (row.isRecordingInProgress()) { - return new int[] {ACTION_STOP_RECORDING}; - } else if (row.isRecordingNotStarted()) { + if (row.isRecordingInProgress()) { + return new int[]{ACTION_STOP_RECORDING}; + } else if (row.isOnAir()) { + if (row.isRecordingNotStarted()) { if (canResolveConflict()) { // The "START" action can change the conflict states. return new int[] {ACTION_REMOVE_SCHEDULE, ACTION_START_RECORDING}; diff --git a/src/com/android/tv/dvr/ui/list/SchedulesHeaderRow.java b/src/com/android/tv/dvr/ui/list/SchedulesHeaderRow.java index 0fb0924d..715ecb8c 100644 --- a/src/com/android/tv/dvr/ui/list/SchedulesHeaderRow.java +++ b/src/com/android/tv/dvr/ui/list/SchedulesHeaderRow.java @@ -16,12 +16,15 @@ package com.android.tv.dvr.ui.list; -import com.android.tv.dvr.SeriesRecording; +import com.android.tv.data.Program; +import com.android.tv.dvr.data.SeriesRecording; + +import java.util.List; /** * A base class for the rows for schedules' header. */ -public abstract class SchedulesHeaderRow { +abstract class SchedulesHeaderRow { private String mTitle; private String mDescription; private int mItemCount; @@ -98,11 +101,20 @@ public abstract class SchedulesHeaderRow { */ public static class SeriesRecordingHeaderRow extends SchedulesHeaderRow { private SeriesRecording mSeriesRecording; + private List mPrograms; public SeriesRecordingHeaderRow(String title, String description, int itemCount, - SeriesRecording series) { + SeriesRecording series, List programs) { super(title, description, itemCount); mSeriesRecording = series; + mPrograms = programs; + } + + /** + * Returns the list of programs which belong to the series. + */ + public List getPrograms() { + return mPrograms; } /** @@ -119,4 +131,4 @@ public abstract class SchedulesHeaderRow { mSeriesRecording = seriesRecording; } } -} +} \ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java b/src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java index 69c33a96..fe2033ba 100644 --- a/src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java +++ b/src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java @@ -30,15 +30,14 @@ import android.widget.TextView; import com.android.tv.R; import com.android.tv.TvApplication; -import com.android.tv.dvr.DvrUiHelper; -import com.android.tv.dvr.SeriesRecording; -import com.android.tv.dvr.ui.DvrSchedulesActivity; +import com.android.tv.dvr.data.SeriesRecording; +import com.android.tv.dvr.ui.DvrUiHelper; import com.android.tv.dvr.ui.list.SchedulesHeaderRow.SeriesRecordingHeaderRow; /** * A base class for RowPresenter for {@link SchedulesHeaderRow} */ -public abstract class SchedulesHeaderRowPresenter extends RowPresenter { +abstract class SchedulesHeaderRowPresenter extends RowPresenter { private Context mContext; public SchedulesHeaderRowPresenter(Context context) { @@ -79,7 +78,7 @@ public abstract class SchedulesHeaderRowPresenter extends RowPresenter { } /** - * A presenter for {@link com.android.tv.dvr.ui.list.SchedulesHeaderRow.DateHeaderRow}. + * A presenter for {@link SchedulesHeaderRow.DateHeaderRow}. */ public static class DateHeaderRowPresenter extends SchedulesHeaderRowPresenter { public DateHeaderRowPresenter(Context context) { @@ -93,7 +92,7 @@ public abstract class SchedulesHeaderRowPresenter extends RowPresenter { /** * A ViewHolder for - * {@link com.android.tv.dvr.ui.list.SchedulesHeaderRow.DateHeaderRow}. + * {@link SchedulesHeaderRow.DateHeaderRow}. */ public static class DateHeaderRowViewHolder extends SchedulesHeaderRowViewHolder { public DateHeaderRowViewHolder(Context context, ViewGroup parent) { @@ -152,9 +151,9 @@ public abstract class SchedulesHeaderRowPresenter extends RowPresenter { headerViewHolder.mSeriesSettingsButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View view) { - // TODO: pass channel list for settings. DvrUiHelper.startSeriesSettingsActivity(getContext(), - header.getSeriesRecording().getId(), null, false, false, false); + header.getSeriesRecording().getId(), + header.getPrograms(), false, false, false, null); } }); headerViewHolder.mToggleStartStopButton.setOnClickListener(new OnClickListener() { @@ -169,9 +168,9 @@ public abstract class SchedulesHeaderRowPresenter extends RowPresenter { .build(); TvApplication.getSingletons(getContext()).getDvrManager() .updateSeriesRecording(seriesRecording); - // TODO: pass channel list for settings. DvrUiHelper.startSeriesSettingsActivity(getContext(), - header.getSeriesRecording().getId(), null, false, false, false); + header.getSeriesRecording().getId(), + header.getPrograms(), false, false, false, null); } else { DvrUiHelper.showCancelAllSeriesRecordingDialog( (DvrSchedulesActivity) view.getContext(), @@ -182,11 +181,8 @@ public abstract class SchedulesHeaderRowPresenter extends RowPresenter { } private void setTextDrawable(TextView textView, Drawable drawableStart) { - if (mLtr) { - textView.setCompoundDrawablesWithIntrinsicBounds(drawableStart, null, null, null); - } else { - textView.setCompoundDrawablesWithIntrinsicBounds(null, null, drawableStart, null); - } + textView.setCompoundDrawablesRelativeWithIntrinsicBounds(drawableStart, null, null, + null); } /** diff --git a/src/com/android/tv/dvr/ui/list/SeriesScheduleRowAdapter.java b/src/com/android/tv/dvr/ui/list/SeriesScheduleRowAdapter.java index 3b493774..6b6de8b8 100644 --- a/src/com/android/tv/dvr/ui/list/SeriesScheduleRowAdapter.java +++ b/src/com/android/tv/dvr/ui/list/SeriesScheduleRowAdapter.java @@ -31,8 +31,8 @@ import com.android.tv.common.SoftPreconditions; import com.android.tv.data.Program; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.ScheduledRecording; -import com.android.tv.dvr.SeriesRecording; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.data.SeriesRecording; import com.android.tv.dvr.ui.list.SchedulesHeaderRow.SeriesRecordingHeaderRow; import com.android.tv.util.Utils; @@ -46,7 +46,7 @@ import java.util.Map; * An adapter for series schedule row. */ @TargetApi(Build.VERSION_CODES.N) -public class SeriesScheduleRowAdapter extends ScheduleRowAdapter { +class SeriesScheduleRowAdapter extends ScheduleRowAdapter { private static final String TAG = "SeriesRowAdapter"; private static final boolean DEBUG = false; @@ -96,7 +96,7 @@ public class SeriesScheduleRowAdapter extends ScheduleRowAdapter { Collections.sort(sortedPrograms); List rows = new ArrayList<>(); mHeaderRow = new SeriesRecordingHeaderRow(mSeriesRecording.getTitle(), - null, sortedPrograms.size(), mSeriesRecording); + null, sortedPrograms.size(), mSeriesRecording, programs); for (Program program : sortedPrograms) { ScheduledRecording schedule = mDataManager.getScheduledRecordingForProgramId(program.getId()); @@ -145,7 +145,7 @@ public class SeriesScheduleRowAdapter extends ScheduleRowAdapter { if (index != -1) { EpisodicProgramRow row = (EpisodicProgramRow) get(index); if (!row.isStartRecordingRequested()) { - row.setSchedule(schedule); + setScheduleToRow(row, schedule); notifyArrayItemRangeChanged(index, 1); } } @@ -195,12 +195,10 @@ public class SeriesScheduleRowAdapter extends ScheduleRowAdapter { if (!isStartOrStopRequested()) { executePendingUpdate(); } - row.setSchedule(schedule); + setScheduleToRow(row, schedule); } - } else if (willBeKept(schedule)) { - row.setSchedule(schedule); } else { - row.setSchedule(null); + setScheduleToRow(row, schedule); } notifyArrayItemRangeChanged(index, 1); } @@ -213,6 +211,14 @@ public class SeriesScheduleRowAdapter extends ScheduleRowAdapter { } } + private void setScheduleToRow(ScheduleRow row, ScheduledRecording schedule) { + if (schedule != null && willBeKept(schedule)) { + row.setSchedule(schedule); + } else { + row.setSchedule(null); + } + } + private int findRowIndexByProgramId(long programId) { for (int i = 0; i < size(); i++) { Object item = get(i); diff --git a/src/com/android/tv/dvr/ui/list/SeriesScheduleRowPresenter.java b/src/com/android/tv/dvr/ui/list/SeriesScheduleRowPresenter.java index 5d88579a..c8503e0d 100644 --- a/src/com/android/tv/dvr/ui/list/SeriesScheduleRowPresenter.java +++ b/src/com/android/tv/dvr/ui/list/SeriesScheduleRowPresenter.java @@ -22,13 +22,13 @@ import android.view.ViewGroup; import com.android.tv.R; import com.android.tv.common.SoftPreconditions; -import com.android.tv.dvr.DvrUiHelper; +import com.android.tv.dvr.ui.DvrUiHelper; import com.android.tv.util.Utils; /** * A RowPresenter for series schedule row. */ -public class SeriesScheduleRowPresenter extends ScheduleRowPresenter { +class SeriesScheduleRowPresenter extends ScheduleRowPresenter { private static final String TAG = "SeriesRowPresenter"; private boolean mLtr; @@ -74,13 +74,8 @@ public class SeriesScheduleRowPresenter extends ScheduleRowPresenter { viewHolder.getProgramTitleView().setCompoundDrawablePadding(getContext() .getResources().getDimensionPixelOffset( R.dimen.dvr_schedules_warning_icon_padding)); - if (mLtr) { - viewHolder.getProgramTitleView().setCompoundDrawablesWithIntrinsicBounds( - R.drawable.ic_warning_gray600_36dp, 0, 0, 0); - } else { - viewHolder.getProgramTitleView().setCompoundDrawablesWithIntrinsicBounds( - 0, 0, R.drawable.ic_warning_gray600_36dp, 0); - } + viewHolder.getProgramTitleView().setCompoundDrawablesRelativeWithIntrinsicBounds( + R.drawable.ic_warning_gray600_36dp, 0, 0, 0); } else { viewHolder.getProgramTitleView().setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0); } @@ -88,9 +83,7 @@ public class SeriesScheduleRowPresenter extends ScheduleRowPresenter { @Override protected void onInfoClicked(ScheduleRow row) { - if (row.getSchedule() != null) { - DvrUiHelper.startSchedulesActivity(getContext(), row.getSchedule()); - } + DvrUiHelper.startSchedulesActivity(getContext(), row.getSchedule()); } @Override diff --git a/src/com/android/tv/dvr/ui/playback/DvrPlaybackActivity.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackActivity.java new file mode 100644 index 00000000..2437d1f5 --- /dev/null +++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackActivity.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tv.dvr.ui.playback; + +import android.app.Activity; +import android.content.Intent; +import android.content.res.Configuration; +import android.os.Bundle; +import android.util.Log; + +import com.android.tv.R; +import com.android.tv.TvApplication; +import com.android.tv.dvr.data.RecordedProgram; + +/** + * Activity to play a {@link RecordedProgram}. + */ +public class DvrPlaybackActivity extends Activity { + private static final String TAG = "DvrPlaybackActivity"; + private static final boolean DEBUG = false; + + private DvrPlaybackOverlayFragment mOverlayFragment; + + @Override + public void onCreate(Bundle savedInstanceState) { + TvApplication.setCurrentRunningProcess(this, true); + if (DEBUG) Log.d(TAG, "onCreate"); + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_dvr_playback); + mOverlayFragment = (DvrPlaybackOverlayFragment) getFragmentManager() + .findFragmentById(R.id.dvr_playback_controls_fragment); + } + + @Override + public void onVisibleBehindCanceled() { + if (DEBUG) Log.d(TAG, "onVisibleBehindCanceled"); + super.onVisibleBehindCanceled(); + finish(); + } + + @Override + protected void onNewIntent(Intent intent) { + mOverlayFragment.onNewIntent(intent); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + float density = getResources().getDisplayMetrics().density; + mOverlayFragment.onWindowSizeChanged((int) (newConfig.screenWidthDp * density), + (int) (newConfig.screenHeightDp * density)); + } +} \ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/playback/DvrPlaybackCardPresenter.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackCardPresenter.java new file mode 100644 index 00000000..4bd121b1 --- /dev/null +++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackCardPresenter.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.dvr.ui.playback; + +import android.content.Context; +import android.content.Intent; +import android.text.TextUtils; +import android.util.Log; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; + +import com.android.tv.R; +import com.android.tv.dvr.data.RecordedProgram; +import com.android.tv.dvr.ui.browse.RecordedProgramPresenter; +import com.android.tv.dvr.ui.browse.RecordingCardView; +import com.android.tv.util.Utils; + +/** + * This class is used to generate Views and bind Objects for related recordings in DVR playback. + */ +class DvrPlaybackCardPresenter extends RecordedProgramPresenter { + private static final String TAG = "DvrPlaybackCardPresenter"; + private static final boolean DEBUG = false; + + private final int mRelatedRecordingCardWidth; + private final int mRelatedRecordingCardHeight; + private final DvrPlaybackOverlayFragment mFragment; + + DvrPlaybackCardPresenter(Context context, DvrPlaybackOverlayFragment fragment) { + super(context); + mFragment = fragment; + mRelatedRecordingCardWidth = + context.getResources().getDimensionPixelSize(R.dimen.dvr_related_recordings_width); + mRelatedRecordingCardHeight = + context.getResources().getDimensionPixelSize(R.dimen.dvr_related_recordings_height); + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent) { + RecordingCardView view = new RecordingCardView( + getContext(), mRelatedRecordingCardWidth, mRelatedRecordingCardHeight, true); + return new ViewHolder(view); + } + + @Override + protected OnClickListener onCreateOnClickListener() { + return new OnClickListener() { + @Override + public void onClick(View v) { + // Disable fading of overlay fragment to prevent the layout blinking while updating + // new playback states and info. The fading enabled status will be reset during + // playback state changing, in DvrPlaybackControlHelper.onStateChanged(). + mFragment.setFadingEnabled(false); + long programId = ((RecordedProgram) v.getTag()).getId(); + if (DEBUG) Log.d(TAG, "Play Related Recording:" + programId); + Intent intent = new Intent(getContext(), DvrPlaybackActivity.class); + intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_ID, programId); + getContext().startActivity(intent); + } + }; + } +} \ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/playback/DvrPlaybackControlHelper.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackControlHelper.java new file mode 100644 index 00000000..4658a328 --- /dev/null +++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackControlHelper.java @@ -0,0 +1,399 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tv.dvr.ui.playback; + +import android.app.Activity; +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.media.MediaMetadata; +import android.media.session.MediaController; +import android.media.session.MediaController.TransportControls; +import android.media.session.PlaybackState; +import android.media.tv.TvTrackInfo; +import android.os.Bundle; +import android.support.v17.leanback.app.PlaybackControlGlue; +import android.support.v17.leanback.widget.AbstractDetailsDescriptionPresenter; +import android.support.v17.leanback.widget.Action; +import android.support.v17.leanback.widget.ArrayObjectAdapter; +import android.support.v17.leanback.widget.ControlButtonPresenterSelector; +import android.support.v17.leanback.widget.OnActionClickedListener; +import android.support.v17.leanback.widget.PlaybackControlsRow; +import android.support.v17.leanback.widget.PlaybackControlsRow.ClosedCaptioningAction; +import android.support.v17.leanback.widget.PlaybackControlsRow.MultiAction; +import android.support.v17.leanback.widget.PlaybackControlsRowPresenter; +import android.support.v17.leanback.widget.RowPresenter; +import android.text.TextUtils; +import android.util.Log; +import android.view.KeyEvent; +import android.view.View; + +import com.android.tv.R; +import com.android.tv.util.TimeShiftUtils; + +import java.util.ArrayList; + +/** + * A helper class to assist {@link DvrPlaybackOverlayFragment} to manage its controls row and + * send command to the media controller. It also helps to update playback states displayed in the + * fragment according to information the media session provides. + */ +class DvrPlaybackControlHelper extends PlaybackControlGlue { + private static final String TAG = "DvrPlaybackControlHelper"; + private static final boolean DEBUG = false; + + private static final int AUDIO_ACTION_ID = 1001; + + private int mPlaybackState = PlaybackState.STATE_NONE; + private int mPlaybackSpeedLevel; + private int mPlaybackSpeedId; + private boolean mReadyToControl; + + private final MediaController mMediaController; + private final MediaController.Callback mMediaControllerCallback = new MediaControllerCallback(); + private final TransportControls mTransportControls; + private final int mExtraPaddingTopForNoDescription; + private final ArrayObjectAdapter mSecondaryActionsAdapter; + private final MultiAction mClosedCaptioningAction; + private final MultiAction mMultiAudioAction; + + public DvrPlaybackControlHelper(Activity activity, DvrPlaybackOverlayFragment overlayFragment) { + super(activity, overlayFragment, new int[TimeShiftUtils.MAX_SPEED_LEVEL + 1]); + mMediaController = activity.getMediaController(); + mMediaController.registerCallback(mMediaControllerCallback); + mTransportControls = mMediaController.getTransportControls(); + mExtraPaddingTopForNoDescription = activity.getResources() + .getDimensionPixelOffset(R.dimen.dvr_playback_controls_extra_padding_top); + mSecondaryActionsAdapter = new ArrayObjectAdapter(new ControlButtonPresenterSelector()); + mClosedCaptioningAction = new ClosedCaptioningAction(activity); + mMultiAudioAction = new MultiAudioAction(activity); + } + + @Override + public PlaybackControlsRowPresenter createControlsRowAndPresenter() { + PlaybackControlsRow controlsRow = new PlaybackControlsRow(this); + controlsRow.setSecondaryActionsAdapter(mSecondaryActionsAdapter); + setControlsRow(controlsRow); + AbstractDetailsDescriptionPresenter detailsPresenter = + new AbstractDetailsDescriptionPresenter() { + @Override + protected void onBindDescription( + AbstractDetailsDescriptionPresenter.ViewHolder viewHolder, Object object) { + PlaybackControlGlue glue = (PlaybackControlGlue) object; + if (glue.hasValidMedia()) { + viewHolder.getTitle().setText(glue.getMediaTitle()); + viewHolder.getSubtitle().setText(glue.getMediaSubtitle()); + } else { + viewHolder.getTitle().setText(""); + viewHolder.getSubtitle().setText(""); + } + if (TextUtils.isEmpty(viewHolder.getSubtitle().getText())) { + viewHolder.view.setPadding(viewHolder.view.getPaddingLeft(), + mExtraPaddingTopForNoDescription, + viewHolder.view.getPaddingRight(), viewHolder.view.getPaddingBottom()); + } + } + }; + PlaybackControlsRowPresenter presenter = + new PlaybackControlsRowPresenter(detailsPresenter) { + @Override + protected void onBindRowViewHolder(RowPresenter.ViewHolder vh, Object item) { + super.onBindRowViewHolder(vh, item); + vh.setOnKeyListener(DvrPlaybackControlHelper.this); + } + + @Override + protected void onUnbindRowViewHolder(RowPresenter.ViewHolder vh) { + super.onUnbindRowViewHolder(vh); + vh.setOnKeyListener(null); + } + }; + presenter.setProgressColor(getContext().getResources() + .getColor(R.color.play_controls_progress_bar_watched)); + presenter.setBackgroundColor(getContext().getResources() + .getColor(R.color.play_controls_body_background_enabled)); + presenter.setOnActionClickedListener(new OnActionClickedListener() { + @Override + public void onActionClicked(Action action) { + if (mReadyToControl) { + int trackType; + if (action.getId() == mClosedCaptioningAction.getId()) { + trackType = TvTrackInfo.TYPE_SUBTITLE; + } else if (action.getId() == AUDIO_ACTION_ID) { + trackType = TvTrackInfo.TYPE_AUDIO; + } else { + DvrPlaybackControlHelper.super.onActionClicked(action); + return; + } + ArrayList trackInfos = + ((DvrPlaybackOverlayFragment) getFragment()).getTracks(trackType); + if (!trackInfos.isEmpty()) { + showSideFragment(trackInfos, ((DvrPlaybackOverlayFragment) + getFragment()).getSelectedTrackId(trackType)); + } + } + } + }); + return presenter; + } + + @Override + public boolean onKey(View v, int keyCode, KeyEvent event) { + if (mReadyToControl) { + if (keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE && event.getAction() == KeyEvent.ACTION_DOWN + && (mPlaybackState == PlaybackState.STATE_FAST_FORWARDING + || mPlaybackState == PlaybackState.STATE_REWINDING)) { + // Workaround of b/31489271. Clicks play/pause button first to reset play controls + // to "play" state. Then we can pass MEDIA_PAUSE to let playback be paused. + onActionClicked(getControlsRow().getActionForKeyCode(keyCode)); + } + return super.onKey(v, keyCode, event); + } + return false; + } + + @Override + public boolean hasValidMedia() { + PlaybackState playbackState = mMediaController.getPlaybackState(); + return playbackState != null; + } + + @Override + public boolean isMediaPlaying() { + PlaybackState playbackState = mMediaController.getPlaybackState(); + if (playbackState == null) { + return false; + } + int state = playbackState.getState(); + return state != PlaybackState.STATE_NONE && state != PlaybackState.STATE_CONNECTING + && state != PlaybackState.STATE_PAUSED; + } + + /** + * Returns the ID of the media under playback. + */ + public String getMediaId() { + MediaMetadata mediaMetadata = mMediaController.getMetadata(); + return mediaMetadata == null ? null + : mediaMetadata.getString(MediaMetadata.METADATA_KEY_MEDIA_ID); + } + + @Override + public CharSequence getMediaTitle() { + MediaMetadata mediaMetadata = mMediaController.getMetadata(); + return mediaMetadata == null ? "" + : mediaMetadata.getString(MediaMetadata.METADATA_KEY_TITLE); + } + + @Override + public CharSequence getMediaSubtitle() { + MediaMetadata mediaMetadata = mMediaController.getMetadata(); + return mediaMetadata == null ? "" + : mediaMetadata.getString(MediaMetadata.METADATA_KEY_DISPLAY_SUBTITLE); + } + + @Override + public int getMediaDuration() { + MediaMetadata mediaMetadata = mMediaController.getMetadata(); + return mediaMetadata == null ? 0 + : (int) mediaMetadata.getLong(MediaMetadata.METADATA_KEY_DURATION); + } + + @Override + public Drawable getMediaArt() { + // Do not show the poster art on control row. + return null; + } + + @Override + public long getSupportedActions() { + return ACTION_PLAY_PAUSE | ACTION_FAST_FORWARD | ACTION_REWIND; + } + + @Override + public int getCurrentSpeedId() { + return mPlaybackSpeedId; + } + + @Override + public int getCurrentPosition() { + PlaybackState playbackState = mMediaController.getPlaybackState(); + if (playbackState == null) { + return 0; + } + return (int) playbackState.getPosition(); + } + + /** + * Unregister media controller's callback. + */ + public void unregisterCallback() { + mMediaController.unregisterCallback(mMediaControllerCallback); + } + + /** + * Update the secondary controls row. + * @param hasClosedCaption {@code true} to show the closed caption selection button, + * {@code false} to hide it. + * @param hasMultiAudio {@code true} to show the audio track selection button, + * {@code false} to hide it. + */ + public void updateSecondaryRow(boolean hasClosedCaption, boolean hasMultiAudio) { + if (hasClosedCaption) { + if (mSecondaryActionsAdapter.indexOf(mClosedCaptioningAction) < 0) { + mSecondaryActionsAdapter.add(0, mClosedCaptioningAction); + } + } else { + mSecondaryActionsAdapter.remove(mClosedCaptioningAction); + } + if (hasMultiAudio) { + if (mSecondaryActionsAdapter.indexOf(mMultiAudioAction) < 0) { + mSecondaryActionsAdapter.add(mMultiAudioAction); + } + } else { + mSecondaryActionsAdapter.remove(mMultiAudioAction); + } + } + + /** + * Returns if the secondary controls row has any buttons and thus should be shown. + */ + public boolean hasSecondaryRow() { + return mSecondaryActionsAdapter.size() != 0; + } + + @Override + protected void startPlayback(int speedId) { + if (getCurrentSpeedId() == speedId) { + return; + } + if (speedId == PLAYBACK_SPEED_NORMAL) { + mTransportControls.play(); + } else if (speedId <= -PLAYBACK_SPEED_FAST_L0) { + mTransportControls.rewind(); + } else if (speedId >= PLAYBACK_SPEED_FAST_L0){ + mTransportControls.fastForward(); + } + } + + @Override + protected void pausePlayback() { + mTransportControls.pause(); + } + + @Override + protected void skipToNext() { + // Do nothing. + } + + @Override + protected void skipToPrevious() { + // Do nothing. + } + + @Override + protected void onRowChanged(PlaybackControlsRow row) { + // Do nothing. + } + + /** + * Notifies closed caption being enabled/disabled to update related UI. + */ + void onSubtitleTrackStateChanged(boolean enabled) { + mClosedCaptioningAction.setIndex(enabled ? + ClosedCaptioningAction.ON : ClosedCaptioningAction.OFF); + } + + private void onStateChanged(int state, long positionMs, int speedLevel) { + if (DEBUG) Log.d(TAG, "onStateChanged"); + getControlsRow().setCurrentTime((int) positionMs); + if (state == mPlaybackState && mPlaybackSpeedLevel == speedLevel) { + // Only position is changed, no need to update controls row + return; + } + // NOTICE: The below two variables should only be used in this method. + // The only usage of them is to confirm if the state is changed or not. + mPlaybackState = state; + mPlaybackSpeedLevel = speedLevel; + switch (state) { + case PlaybackState.STATE_PLAYING: + mPlaybackSpeedId = PLAYBACK_SPEED_NORMAL; + setFadingEnabled(true); + mReadyToControl = true; + break; + case PlaybackState.STATE_PAUSED: + mPlaybackSpeedId = PLAYBACK_SPEED_PAUSED; + setFadingEnabled(true); + mReadyToControl = true; + break; + case PlaybackState.STATE_FAST_FORWARDING: + mPlaybackSpeedId = PLAYBACK_SPEED_FAST_L0 + speedLevel; + setFadingEnabled(false); + mReadyToControl = true; + break; + case PlaybackState.STATE_REWINDING: + mPlaybackSpeedId = -PLAYBACK_SPEED_FAST_L0 - speedLevel; + setFadingEnabled(false); + mReadyToControl = true; + break; + case PlaybackState.STATE_CONNECTING: + setFadingEnabled(false); + mReadyToControl = false; + break; + case PlaybackState.STATE_NONE: + mReadyToControl = false; + break; + default: + setFadingEnabled(true); + break; + } + onStateChanged(); + } + + private void showSideFragment(ArrayList trackInfos, String selectedTrackId) { + Bundle args = new Bundle(); + args.putParcelableArrayList(DvrPlaybackSideFragment.TRACK_INFOS, trackInfos); + args.putString(DvrPlaybackSideFragment.SELECTED_TRACK_ID, selectedTrackId); + DvrPlaybackSideFragment sideFragment = new DvrPlaybackSideFragment(); + sideFragment.setArguments(args); + getFragment().getFragmentManager().beginTransaction() + .hide(getFragment()) + .replace(R.id.dvr_playback_side_fragment, sideFragment) + .addToBackStack(null) + .commit(); + } + + private class MediaControllerCallback extends MediaController.Callback { + @Override + public void onPlaybackStateChanged(PlaybackState state) { + if (DEBUG) Log.d(TAG, "Playback state changed: " + state.getState()); + onStateChanged(state.getState(), state.getPosition(), (int) state.getPlaybackSpeed()); + } + + @Override + public void onMetadataChanged(MediaMetadata metadata) { + DvrPlaybackControlHelper.this.onMetadataChanged(); + ((DvrPlaybackOverlayFragment) getFragment()).onMediaControllerUpdated(); + } + } + + private static class MultiAudioAction extends MultiAction { + MultiAudioAction(Context context) { + super(AUDIO_ACTION_ID); + setDrawables(new Drawable[]{context.getDrawable(R.drawable.ic_tvoption_multi_track)}); + } + } +} \ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/playback/DvrPlaybackMediaSessionHelper.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackMediaSessionHelper.java new file mode 100644 index 00000000..843d2dbe --- /dev/null +++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackMediaSessionHelper.java @@ -0,0 +1,335 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tv.dvr.ui.playback; + +import android.app.Activity; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.media.MediaMetadata; +import android.media.session.MediaController; +import android.media.session.MediaSession; +import android.media.session.PlaybackState; +import android.media.tv.TvContract; +import android.os.AsyncTask; +import android.support.annotation.Nullable; +import android.text.TextUtils; + +import com.android.tv.R; +import com.android.tv.TvApplication; +import com.android.tv.common.SoftPreconditions; +import com.android.tv.data.Channel; +import com.android.tv.data.ChannelDataManager; +import com.android.tv.dvr.DvrWatchedPositionManager; +import com.android.tv.dvr.data.RecordedProgram; +import com.android.tv.util.ImageLoader; +import com.android.tv.util.TimeShiftUtils; +import com.android.tv.util.Utils; + +class DvrPlaybackMediaSessionHelper { + private static final String TAG = "DvrPlaybackMediaSessionHelper"; + private static final boolean DEBUG = false; + + private int mNowPlayingCardWidth; + private int mNowPlayingCardHeight; + private int mSpeedLevel; + private long mProgramDurationMs; + + private Activity mActivity; + private DvrPlayer mDvrPlayer; + private MediaSession mMediaSession; + private final DvrWatchedPositionManager mDvrWatchedPositionManager; + private final ChannelDataManager mChannelDataManager; + + public DvrPlaybackMediaSessionHelper(Activity activity, String mediaSessionTag, + DvrPlayer dvrPlayer, DvrPlaybackOverlayFragment overlayFragment) { + mActivity = activity; + mDvrPlayer = dvrPlayer; + mDvrWatchedPositionManager = + TvApplication.getSingletons(activity).getDvrWatchedPositionManager(); + mChannelDataManager = TvApplication.getSingletons(activity).getChannelDataManager(); + mDvrPlayer.setCallback(new DvrPlayer.DvrPlayerCallback() { + @Override + public void onPlaybackStateChanged(int playbackState, int playbackSpeed) { + updateMediaSessionPlaybackState(); + } + + @Override + public void onPlaybackPositionChanged(long positionMs) { + updateMediaSessionPlaybackState(); + if (mDvrPlayer.isPlaybackPrepared()) { + mDvrWatchedPositionManager + .setWatchedPosition(mDvrPlayer.getProgram().getId(), positionMs); + } + } + + @Override + public void onPlaybackEnded() { + // TODO: Deal with watched over recordings in DVR library + RecordedProgram nextEpisode = + overlayFragment.getNextEpisode(mDvrPlayer.getProgram()); + if (nextEpisode == null) { + mDvrPlayer.reset(); + mActivity.finish(); + } else { + Intent intent = new Intent(activity, DvrPlaybackActivity.class); + intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_ID, nextEpisode.getId()); + mActivity.startActivity(intent); + } + } + }); + initializeMediaSession(mediaSessionTag); + } + + /** + * Stops DVR player and release media session. + */ + public void release() { + if (mDvrPlayer != null) { + mDvrPlayer.reset(); + } + if (mMediaSession != null) { + mMediaSession.release(); + mMediaSession = null; + } + } + + /** + * Updates media session's playback state and speed. + */ + public void updateMediaSessionPlaybackState() { + mMediaSession.setPlaybackState(new PlaybackState.Builder() + .setState(mDvrPlayer.getPlaybackState(), mDvrPlayer.getPlaybackPosition(), + mSpeedLevel).build()); + } + + /** + * Sets the recorded program for playback. + * + * @param program The recorded program to play. {@code null} to reset the DVR player. + */ + public void setupPlayback(RecordedProgram program, long seekPositionMs) { + if (program != null) { + mDvrPlayer.setProgram(program, seekPositionMs); + setupMediaSession(program); + } else { + mDvrPlayer.reset(); + mMediaSession.setActive(false); + } + } + + /** + * Returns the recorded program now playing. + */ + public RecordedProgram getProgram() { + return mDvrPlayer.getProgram(); + } + + /** + * Checks if the recorded program is the same as now playing one. + */ + public boolean isCurrentProgram(RecordedProgram program) { + return program != null && program.equals(getProgram()); + } + + /** + * Returns playback state. + */ + public int getPlaybackState() { + return mDvrPlayer.getPlaybackState(); + } + + /** + * Returns the underlying DVR player. + */ + public DvrPlayer getDvrPlayer() { + return mDvrPlayer; + } + + private void initializeMediaSession(String mediaSessionTag) { + mMediaSession = new MediaSession(mActivity, mediaSessionTag); + mMediaSession.setFlags(MediaSession.FLAG_HANDLES_MEDIA_BUTTONS + | MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS); + mNowPlayingCardWidth = mActivity.getResources() + .getDimensionPixelSize(R.dimen.notif_card_img_max_width); + mNowPlayingCardHeight = mActivity.getResources() + .getDimensionPixelSize(R.dimen.notif_card_img_height); + mMediaSession.setCallback(new MediaSessionCallback()); + mActivity.setMediaController( + new MediaController(mActivity, mMediaSession.getSessionToken())); + updateMediaSessionPlaybackState(); + } + + private void setupMediaSession(RecordedProgram program) { + mProgramDurationMs = program.getDurationMillis(); + String cardTitleText = program.getTitle(); + if (TextUtils.isEmpty(cardTitleText)) { + Channel channel = mChannelDataManager.getChannel(program.getChannelId()); + cardTitleText = (channel != null) ? channel.getDisplayName() + : mActivity.getString(R.string.no_program_information); + } + final MediaMetadata currentMetadata = updateMetadataTextInfo(program.getId(), cardTitleText, + program.getDescription(), mProgramDurationMs); + String posterArtUri = program.getPosterArtUri(); + if (posterArtUri == null) { + posterArtUri = TvContract.buildChannelLogoUri(program.getChannelId()).toString(); + } + updatePosterArt(program, currentMetadata, null, posterArtUri); + mMediaSession.setActive(true); + } + + private void updatePosterArt(RecordedProgram program, MediaMetadata currentMetadata, + @Nullable Bitmap posterArt, @Nullable String posterArtUri) { + if (posterArt != null) { + updateMetadataImageInfo(program, currentMetadata, posterArt, 0); + } else if (posterArtUri != null) { + ImageLoader.loadBitmap(mActivity, posterArtUri, mNowPlayingCardWidth, + mNowPlayingCardHeight, + new ProgramPosterArtCallback(mActivity, program, currentMetadata)); + } else { + updateMetadataImageInfo(program, currentMetadata, null, R.drawable.default_now_card); + } + } + + private class ProgramPosterArtCallback extends + ImageLoader.ImageLoaderCallback { + private final RecordedProgram mRecordedProgram; + private final MediaMetadata mCurrentMetadata; + + public ProgramPosterArtCallback(Activity activity, RecordedProgram program, + MediaMetadata metadata) { + super(activity); + mRecordedProgram = program; + mCurrentMetadata = metadata; + } + + @Override + public void onBitmapLoaded(Activity activity, @Nullable Bitmap posterArt) { + if (isCurrentProgram(mRecordedProgram)) { + updatePosterArt(mRecordedProgram, mCurrentMetadata, posterArt, null); + } + } + } + + private MediaMetadata updateMetadataTextInfo(final long programId, final String title, + final String subtitle, final long duration) { + MediaMetadata.Builder builder = new MediaMetadata.Builder(); + builder.putString(MediaMetadata.METADATA_KEY_MEDIA_ID, Long.toString(programId)) + .putString(MediaMetadata.METADATA_KEY_TITLE, title) + .putLong(MediaMetadata.METADATA_KEY_DURATION, duration); + if (subtitle != null) { + builder.putString(MediaMetadata.METADATA_KEY_DISPLAY_SUBTITLE, subtitle); + } + MediaMetadata metadata = builder.build(); + mMediaSession.setMetadata(metadata); + return metadata; + } + + private void updateMetadataImageInfo(final RecordedProgram program, + final MediaMetadata currentMetadata, final Bitmap posterArt, final int imageResId) { + if (mMediaSession != null && (posterArt != null || imageResId != 0)) { + MediaMetadata.Builder builder = new MediaMetadata.Builder(currentMetadata); + if (posterArt != null) { + builder.putBitmap(MediaMetadata.METADATA_KEY_ART, posterArt); + mMediaSession.setMetadata(builder.build()); + } else { + new AsyncTask() { + @Override + protected Bitmap doInBackground(Void... arg0) { + return BitmapFactory.decodeResource(mActivity.getResources(), imageResId); + } + + @Override + protected void onPostExecute(Bitmap programPosterArt) { + if (mMediaSession != null && programPosterArt != null + && isCurrentProgram(program)) { + builder.putBitmap(MediaMetadata.METADATA_KEY_ART, programPosterArt); + mMediaSession.setMetadata(builder.build()); + } + } + }.execute(); + } + } + } + + // An event was triggered by MediaController.TransportControls and must be handled here. + // Here we update the media itself to act on the event that was triggered. + private class MediaSessionCallback extends MediaSession.Callback { + @Override + public void onPrepare() { + if (!mDvrPlayer.isPlaybackPrepared()) { + mDvrPlayer.prepare(true); + } + } + + @Override + public void onPlay() { + if (mDvrPlayer.isPlaybackPrepared()) { + mDvrPlayer.play(); + } + } + + @Override + public void onPause() { + if (mDvrPlayer.isPlaybackPrepared()) { + mDvrPlayer.pause(); + } + } + + @Override + public void onFastForward() { + if (!mDvrPlayer.isPlaybackPrepared()) { + return; + } + if (mDvrPlayer.getPlaybackState() == PlaybackState.STATE_FAST_FORWARDING) { + if (mSpeedLevel < TimeShiftUtils.MAX_SPEED_LEVEL) { + mSpeedLevel++; + } else { + return; + } + } else { + mSpeedLevel = 0; + } + mDvrPlayer.fastForward( + TimeShiftUtils.getPlaybackSpeed(mSpeedLevel, mProgramDurationMs)); + } + + @Override + public void onRewind() { + if (!mDvrPlayer.isPlaybackPrepared()) { + return; + } + if (mDvrPlayer.getPlaybackState() == PlaybackState.STATE_REWINDING) { + if (mSpeedLevel < TimeShiftUtils.MAX_SPEED_LEVEL) { + mSpeedLevel++; + } else { + return; + } + } else { + mSpeedLevel = 0; + } + mDvrPlayer.rewind(TimeShiftUtils.getPlaybackSpeed(mSpeedLevel, mProgramDurationMs)); + } + + @Override + public void onSeekTo(long positionMs) { + if (mDvrPlayer.isPlaybackPrepared()) { + mDvrPlayer.seekTo(positionMs); + } + } + } +} diff --git a/src/com/android/tv/dvr/ui/playback/DvrPlaybackOverlayFragment.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackOverlayFragment.java new file mode 100644 index 00000000..ff907182 --- /dev/null +++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackOverlayFragment.java @@ -0,0 +1,431 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tv.dvr.ui.playback; + +import android.content.Context; +import android.content.Intent; +import android.graphics.Point; +import android.hardware.display.DisplayManager; +import android.media.tv.TvContentRating; +import android.media.tv.TvTrackInfo; +import android.os.Bundle; +import android.media.session.PlaybackState; +import android.media.tv.TvInputManager; +import android.media.tv.TvView; +import android.support.v17.leanback.app.PlaybackOverlayFragment; +import android.support.v17.leanback.widget.ArrayObjectAdapter; +import android.support.v17.leanback.widget.ClassPresenterSelector; +import android.support.v17.leanback.widget.HeaderItem; +import android.support.v17.leanback.widget.ListRow; +import android.support.v17.leanback.widget.PlaybackControlsRow; +import android.support.v17.leanback.widget.PlaybackControlsRowPresenter; +import android.support.v17.leanback.widget.SinglePresenterSelector; +import android.view.Display; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Toast; +import android.util.Log; + +import com.android.tv.R; +import com.android.tv.TvApplication; +import com.android.tv.data.BaseProgram; +import com.android.tv.dialog.PinDialogFragment; +import com.android.tv.dvr.DvrDataManager; +import com.android.tv.dvr.data.RecordedProgram; +import com.android.tv.dvr.data.SeriesRecording; +import com.android.tv.dvr.ui.SortedArrayAdapter; +import com.android.tv.dvr.ui.browse.DvrListRowPresenter; +import com.android.tv.parental.ContentRatingsManager; +import com.android.tv.util.TvSettings; +import com.android.tv.util.TvTrackInfoUtils; +import com.android.tv.util.Utils; + +import java.util.List; +import java.util.ArrayList; + +public class DvrPlaybackOverlayFragment extends PlaybackOverlayFragment { + // TODO: Handles audio focus. Deals with block and ratings. + private static final String TAG = "DvrPlaybackOverlayFragment"; + private static final boolean DEBUG = false; + + private static final String MEDIA_SESSION_TAG = "com.android.tv.dvr.mediasession"; + private static final float DISPLAY_ASPECT_RATIO_EPSILON = 0.01f; + + // mProgram is only used to store program from intent. Don't use it elsewhere. + private RecordedProgram mProgram; + private DvrPlayer mDvrPlayer; + private DvrPlaybackMediaSessionHelper mMediaSessionHelper; + private DvrPlaybackControlHelper mPlaybackControlHelper; + private ArrayObjectAdapter mRowsAdapter; + private SortedArrayAdapter mRelatedRecordingsRowAdapter; + private DvrPlaybackCardPresenter mRelatedRecordingCardPresenter; + private DvrDataManager mDvrDataManager; + private ContentRatingsManager mContentRatingsManager; + private TvView mTvView; + private View mBlockScreenView; + private ListRow mRelatedRecordingsRow; + private int mPaddingWithoutRelatedRow; + private int mPaddingWithoutSecondaryRow; + private int mWindowWidth; + private int mWindowHeight; + private float mAppliedAspectRatio; + private float mWindowAspectRatio; + private boolean mPinChecked; + private DvrPlayer.OnTrackSelectedListener mOnSubtitleTrackSelectedListener = + new DvrPlayer.OnTrackSelectedListener() { + @Override + public void onTrackSelected(String selectedTrackId) { + mPlaybackControlHelper.onSubtitleTrackStateChanged(selectedTrackId != null); + mRowsAdapter.notifyArrayItemRangeChanged(0, 1); + } + }; + + @Override + public void onCreate(Bundle savedInstanceState) { + if (DEBUG) Log.d(TAG, "onCreate"); + super.onCreate(savedInstanceState); + mPaddingWithoutRelatedRow = getActivity().getResources() + .getDimensionPixelOffset(R.dimen.dvr_playback_overlay_padding_top_no_related_row); + mPaddingWithoutSecondaryRow = getActivity().getResources() + .getDimensionPixelOffset(R.dimen.dvr_playback_overlay_padding_top_no_secondary_row); + mDvrDataManager = TvApplication.getSingletons(getActivity()).getDvrDataManager(); + mContentRatingsManager = TvApplication.getSingletons(getContext()) + .getTvInputManagerHelper().getContentRatingsManager(); + mProgram = getProgramFromIntent(getActivity().getIntent()); + if (mProgram == null) { + Toast.makeText(getActivity(), getString(R.string.dvr_program_not_found), + Toast.LENGTH_SHORT).show(); + getActivity().finish(); + return; + } + Point size = new Point(); + ((DisplayManager) getContext().getSystemService(Context.DISPLAY_SERVICE)) + .getDisplay(Display.DEFAULT_DISPLAY).getSize(size); + mWindowWidth = size.x; + mWindowHeight = size.y; + mWindowAspectRatio = mAppliedAspectRatio = (float) mWindowWidth / mWindowHeight; + setBackgroundType(PlaybackOverlayFragment.BG_LIGHT); + setFadingEnabled(true); + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + mTvView = (TvView) getActivity().findViewById(R.id.dvr_tv_view); + mBlockScreenView = getActivity().findViewById(R.id.block_screen); + mDvrPlayer = new DvrPlayer(mTvView); + mMediaSessionHelper = new DvrPlaybackMediaSessionHelper( + getActivity(), MEDIA_SESSION_TAG, mDvrPlayer, this); + mPlaybackControlHelper = new DvrPlaybackControlHelper(getActivity(), this); + setUpRows(); + mDvrPlayer.setOnTracksAvailabilityChangedListener( + new DvrPlayer.OnTracksAvailabilityChangedListener() { + @Override + public void onTracksAvailabilityChanged(boolean hasClosedCaption, + boolean hasMultiAudio) { + mPlaybackControlHelper.updateSecondaryRow(hasClosedCaption, hasMultiAudio); + if (hasClosedCaption) { + mDvrPlayer.setOnTrackSelectedListener(TvTrackInfo.TYPE_SUBTITLE, + mOnSubtitleTrackSelectedListener); + selectBestMatchedTrack(TvTrackInfo.TYPE_SUBTITLE); + } else { + mDvrPlayer.setOnTrackSelectedListener(TvTrackInfo.TYPE_SUBTITLE, null); + } + if (hasMultiAudio) { + selectBestMatchedTrack(TvTrackInfo.TYPE_AUDIO); + } + onMediaControllerUpdated(); + } + }); + mDvrPlayer.setOnAspectRatioChangedListener(new DvrPlayer.OnAspectRatioChangedListener() { + @Override + public void onAspectRatioChanged(float videoAspectRatio) { + updateAspectRatio(videoAspectRatio); + } + }); + mPinChecked = getActivity().getIntent() + .getBooleanExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_PIN_CHECKED, false); + mDvrPlayer.setOnContentBlockedListener(new DvrPlayer.OnContentBlockedListener() { + @Override + public void onContentBlocked(TvContentRating rating) { + if (mPinChecked) { + mTvView.unblockContent(rating); + return; + } + mBlockScreenView.setVisibility(View.VISIBLE); + getActivity().getMediaController().getTransportControls().pause(); + new PinDialogFragment(PinDialogFragment.PIN_DIALOG_TYPE_UNLOCK_DVR, + new PinDialogFragment.ResultListener() { + @Override + public void done(boolean success) { + if (success) { + mPinChecked = true; + mTvView.unblockContent(rating); + mBlockScreenView.setVisibility(View.GONE); + getActivity().getMediaController() + .getTransportControls().play(); + } + } + }, mContentRatingsManager.getDisplayNameForRating(rating)) + .show(getActivity().getFragmentManager(), PinDialogFragment.DIALOG_TAG); + } + }); + preparePlayback(getActivity().getIntent()); + } + + @Override + public void onPause() { + if (DEBUG) Log.d(TAG, "onPause"); + super.onPause(); + if (mMediaSessionHelper.getPlaybackState() == PlaybackState.STATE_FAST_FORWARDING + || mMediaSessionHelper.getPlaybackState() == PlaybackState.STATE_REWINDING) { + getActivity().getMediaController().getTransportControls().pause(); + } + if (mMediaSessionHelper.getPlaybackState() == PlaybackState.STATE_NONE) { + getActivity().requestVisibleBehind(false); + } else { + getActivity().requestVisibleBehind(true); + } + } + + @Override + public void onDestroy() { + if (DEBUG) Log.d(TAG, "onDestroy"); + mPlaybackControlHelper.unregisterCallback(); + mMediaSessionHelper.release(); + mRelatedRecordingCardPresenter.unbindAllViewHolders(); + super.onDestroy(); + } + + /** + * Passes the intent to the fragment. + */ + public void onNewIntent(Intent intent) { + mProgram = getProgramFromIntent(intent); + if (mProgram == null) { + Toast.makeText(getActivity(), getString(R.string.dvr_program_not_found), + Toast.LENGTH_SHORT).show(); + // Continue playing the original program + return; + } + preparePlayback(intent); + } + + /** + * Should be called when windows' size is changed in order to notify DVR player + * to update it's view width/height and position. + */ + public void onWindowSizeChanged(final int windowWidth, final int windowHeight) { + mWindowWidth = windowWidth; + mWindowHeight = windowHeight; + mWindowAspectRatio = (float) mWindowWidth / mWindowHeight; + updateAspectRatio(mAppliedAspectRatio); + } + + /** + * Returns next recorded episode in the same series as now playing program. + */ + public RecordedProgram getNextEpisode(RecordedProgram program) { + int position = mRelatedRecordingsRowAdapter.findInsertPosition(program); + if (position == mRelatedRecordingsRowAdapter.size()) { + return null; + } else { + return (RecordedProgram) mRelatedRecordingsRowAdapter.get(position); + } + } + + /** + * Returns the tracks of the give type of the current playback. + + * @param trackType Should be {@link TvTrackInfo#TYPE_SUBTITLE} + * or {@link TvTrackInfo#TYPE_AUDIO}. Or returns {@code null}. + */ + public ArrayList getTracks(int trackType) { + if (trackType == TvTrackInfo.TYPE_AUDIO) { + return mDvrPlayer.getAudioTracks(); + } else if (trackType == TvTrackInfo.TYPE_SUBTITLE) { + return mDvrPlayer.getSubtitleTracks(); + } + return null; + } + + /** + * Returns the ID of the selected track of the given type. + */ + public String getSelectedTrackId(int trackType) { + return mDvrPlayer.getSelectedTrackId(trackType); + } + + /** + * Returns the language setting of the given track type. + + * @param trackType Should be {@link TvTrackInfo#TYPE_SUBTITLE} + * or {@link TvTrackInfo#TYPE_AUDIO}. + * @return {@code null} if no language has been set for the given track type. + */ + TvTrackInfo getTrackSetting(int trackType) { + return TvSettings.getDvrPlaybackTrackSettings(getContext(), trackType); + } + + /** + * Selects the given audio or subtitle track for DVR playback. + * @param trackType Should be {@link TvTrackInfo#TYPE_SUBTITLE} + * or {@link TvTrackInfo#TYPE_AUDIO}. + * @param selectedTrack {@code null} to disable the audio or subtitle track according to + * trackType. + */ + void selectTrack(int trackType, TvTrackInfo selectedTrack) { + if (mDvrPlayer.isPlaybackPrepared()) { + mDvrPlayer.selectTrack(trackType, selectedTrack); + } + } + + /** + * Notifies the content of controls row or related recordings row is changed and the UI should + * be updated according to the change. + */ + void onMediaControllerUpdated() { + updateVerticalPosition(); + mRowsAdapter.notifyArrayItemRangeChanged(0, 2); + } + + private void selectBestMatchedTrack(int trackType) { + TvTrackInfo selectedTrack = getTrackSetting(trackType); + if (selectedTrack != null) { + TvTrackInfo bestMatchedTrack = TvTrackInfoUtils.getBestTrackInfo(getTracks(trackType), + selectedTrack.getId(), selectedTrack.getLanguage(), + trackType == TvTrackInfo.TYPE_AUDIO ? selectedTrack.getAudioChannelCount() : 0); + if (bestMatchedTrack != null && (trackType == TvTrackInfo.TYPE_AUDIO || Utils + .isEqualLanguage(bestMatchedTrack.getLanguage(), + selectedTrack.getLanguage()))) { + selectTrack(trackType, bestMatchedTrack); + return; + } + } + if (trackType == TvTrackInfo.TYPE_SUBTITLE) { + // Disables closed captioning if there's no matched language. + selectTrack(TvTrackInfo.TYPE_SUBTITLE, null); + } + } + + private void updateAspectRatio(float videoAspectRatio) { + if (videoAspectRatio <= 0) { + // We don't have video's width or height information, use window's aspect ratio. + videoAspectRatio = mWindowAspectRatio; + } + if (Math.abs(mAppliedAspectRatio - videoAspectRatio) < DISPLAY_ASPECT_RATIO_EPSILON) { + // No need to change + return; + } + if (Math.abs(mWindowAspectRatio - videoAspectRatio) < DISPLAY_ASPECT_RATIO_EPSILON) { + ((ViewGroup) mTvView.getParent()).setPadding(0, 0, 0, 0); + } else if (videoAspectRatio < mWindowAspectRatio) { + int newPadding = (mWindowWidth - Math.round(mWindowHeight * videoAspectRatio)) / 2; + ((ViewGroup) mTvView.getParent()).setPadding(newPadding, 0, newPadding, 0); + } else { + int newPadding = (mWindowHeight - Math.round(mWindowWidth / videoAspectRatio)) / 2; + ((ViewGroup) mTvView.getParent()).setPadding(0, newPadding, 0, newPadding); + } + mAppliedAspectRatio = videoAspectRatio; + } + + private void preparePlayback(Intent intent) { + mMediaSessionHelper.setupPlayback(mProgram, getSeekTimeFromIntent(intent)); + mPlaybackControlHelper.updateSecondaryRow(false, false); + getActivity().getMediaController().getTransportControls().prepare(); + updateRelatedRecordingsRow(); + } + + private void updateRelatedRecordingsRow() { + boolean wasEmpty = (mRelatedRecordingsRowAdapter.size() == 0); + mRelatedRecordingsRowAdapter.clear(); + long programId = mProgram.getId(); + String seriesId = mProgram.getSeriesId(); + SeriesRecording seriesRecording = mDvrDataManager.getSeriesRecording(seriesId); + if (seriesRecording != null) { + if (DEBUG) Log.d(TAG, "Update related recordings with:" + seriesId); + List relatedPrograms = + mDvrDataManager.getRecordedPrograms(seriesRecording.getId()); + for (RecordedProgram program : relatedPrograms) { + if (programId != program.getId()) { + mRelatedRecordingsRowAdapter.add(program); + } + } + } + if (mRelatedRecordingsRowAdapter.size() == 0) { + mRowsAdapter.remove(mRelatedRecordingsRow); + } else if (wasEmpty){ + mRowsAdapter.add(mRelatedRecordingsRow); + } + onMediaControllerUpdated(); + } + + private void updateVerticalPosition() { + int verticalPadding = 0; + verticalPadding += + mRelatedRecordingsRowAdapter.size() == 0 ? mPaddingWithoutRelatedRow : 0; + verticalPadding += + mPlaybackControlHelper.hasSecondaryRow() ? 0 : mPaddingWithoutSecondaryRow; + if (DEBUG) Log.d(TAG, "New controls padding: " + verticalPadding); + View view = getView(); + view.setPadding(view.getPaddingLeft(), verticalPadding, + view.getPaddingRight(), view.getPaddingBottom()); + } + + private void setUpRows() { + PlaybackControlsRowPresenter controlsRowPresenter = + mPlaybackControlHelper.createControlsRowAndPresenter(); + + ClassPresenterSelector selector = new ClassPresenterSelector(); + selector.addClassPresenter(PlaybackControlsRow.class, controlsRowPresenter); + selector.addClassPresenter(ListRow.class, new DvrListRowPresenter(getContext())); + + mRowsAdapter = new ArrayObjectAdapter(selector); + mRowsAdapter.add(mPlaybackControlHelper.getControlsRow()); + mRelatedRecordingsRow = getRelatedRecordingsRow(); + setAdapter(mRowsAdapter); + } + + private ListRow getRelatedRecordingsRow() { + mRelatedRecordingCardPresenter = new DvrPlaybackCardPresenter(getActivity(), this); + mRelatedRecordingsRowAdapter = new RelatedRecordingsAdapter(mRelatedRecordingCardPresenter); + HeaderItem header = new HeaderItem(0, + getActivity().getString(R.string.dvr_playback_related_recordings)); + return new ListRow(header, mRelatedRecordingsRowAdapter); + } + + private RecordedProgram getProgramFromIntent(Intent intent) { + long programId = intent.getLongExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_ID, -1); + return mDvrDataManager.getRecordedProgram(programId); + } + + private long getSeekTimeFromIntent(Intent intent) { + return intent.getLongExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_SEEK_TIME, + TvInputManager.TIME_SHIFT_INVALID_TIME); + } + + private class RelatedRecordingsAdapter extends SortedArrayAdapter { + RelatedRecordingsAdapter(DvrPlaybackCardPresenter presenter) { + super(new SinglePresenterSelector(presenter), BaseProgram.EPISODE_COMPARATOR); + } + + @Override + public long getId(BaseProgram item) { + return item.getId(); + } + } +} \ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/playback/DvrPlaybackSideFragment.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackSideFragment.java new file mode 100644 index 00000000..e49870f1 --- /dev/null +++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackSideFragment.java @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.dvr.ui.playback; + +import android.media.tv.TvTrackInfo; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v17.leanback.app.GuidedStepFragment; +import android.support.v17.leanback.widget.GuidedAction; +import android.text.TextUtils; +import android.transition.Transition; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.android.tv.R; +import com.android.tv.util.TvSettings; + +import java.util.List; +import java.util.Locale; + +/** + * Fragment for DVR playback closed-caption/multi-audio settings. + */ +public class DvrPlaybackSideFragment extends GuidedStepFragment { + /** + * The tag for passing track infos to side fragments. + */ + public static final String TRACK_INFOS = "dvr_key_track_infos"; + /** + * The tag for passing selected track's ID to side fragments. + */ + public static final String SELECTED_TRACK_ID = "dvr_key_selected_track_id"; + + private static final int ACTION_ID_NO_SUBTITLE = -1; + private static final int CHECK_SET_ID = 1; + + private List mTrackInfos; + private String mSelectedTrackId; + private TvTrackInfo mSelectedTrack; + private int mTrackType; + private DvrPlaybackOverlayFragment mOverlayFragment; + + @Override + public void onCreate(Bundle savedInstanceState) { + mTrackInfos = getArguments().getParcelableArrayList(TRACK_INFOS); + mTrackType = mTrackInfos.get(0).getType(); + mSelectedTrackId = getArguments().getString(SELECTED_TRACK_ID); + mOverlayFragment = ((DvrPlaybackOverlayFragment) getFragmentManager() + .findFragmentById(R.id.dvr_playback_controls_fragment)); + super.onCreate(savedInstanceState); + } + + @Override + public View onCreateBackgroundView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View backgroundView = super.onCreateBackgroundView(inflater, container, savedInstanceState); + backgroundView.setBackgroundColor(getResources() + .getColor(R.color.lb_playback_controls_background_light)); + return backgroundView; + } + + @Override + public void onCreateActions(@NonNull List actions, Bundle savedInstanceState) { + if (mTrackType == TvTrackInfo.TYPE_SUBTITLE) { + actions.add(new GuidedAction.Builder(getActivity()) + .id(ACTION_ID_NO_SUBTITLE) + .title(getString(R.string.closed_caption_option_item_off)) + .checkSetId(CHECK_SET_ID) + .checked(mSelectedTrackId == null) + .build()); + } + for (int i = 0; i < mTrackInfos.size(); i++) { + TvTrackInfo info = mTrackInfos.get(i); + boolean checked = TextUtils.equals(info.getId(), mSelectedTrackId); + GuidedAction action = new GuidedAction.Builder(getActivity()) + .id(i) + .title(getTrackLabel(info, i)) + .checkSetId(CHECK_SET_ID) + .checked(checked) + .build(); + actions.add(action); + if (checked) { + mSelectedTrack = info; + } + } + } + + @Override + public void onGuidedActionFocused(GuidedAction action) { + int actionId = (int) action.getId(); + mOverlayFragment.selectTrack(mTrackType, actionId < 0 ? null : mTrackInfos.get(actionId)); + } + + @Override + public void onGuidedActionClicked(GuidedAction action) { + int actionId = (int) action.getId(); + mSelectedTrack = actionId < 0 ? null : mTrackInfos.get(actionId); + TvSettings.setDvrPlaybackTrackSettings(getContext(), mTrackType, mSelectedTrack); + getFragmentManager().popBackStack(); + } + + @Override + public void onStart() { + super.onStart(); + // Workaround: when overlay fragment is faded out, any focus will lost due to overlay + // fragment's implementation. So we disable overlay fragment's fading here to prevent + // losing focus while users are interacting with the side fragment. + mOverlayFragment.setFadingEnabled(false); + } + + @Override + public void onStop() { + super.onStop(); + // We disable fading of overlay fragment to prevent side fragment from losing focus, + // therefore we should resume it here. + mOverlayFragment.setFadingEnabled(true); + mOverlayFragment.selectTrack(mTrackType, mSelectedTrack); + } + + private String getTrackLabel(TvTrackInfo track, int trackIndex) { + if (track.getLanguage() != null) { + return new Locale(track.getLanguage()).getDisplayName(); + } + return track.getType() == TvTrackInfo.TYPE_SUBTITLE ? + getString(R.string.closed_caption_unknown_language, trackIndex + 1) + : getString(R.string.multi_audio_unknown_language); + } + + @Override + protected void onProvideFragmentTransitions() { + super.onProvideFragmentTransitions(); + // Excludes the background scrim from transition to prevent the blinking caused by + // hiding the overlay fragment and sliding in the side fragment at the same time. + Transition t = getEnterTransition(); + if (t != null) { + t.excludeTarget(R.id.guidedstep_background, true); + } + } +} \ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/playback/DvrPlayer.java b/src/com/android/tv/dvr/ui/playback/DvrPlayer.java new file mode 100644 index 00000000..780bfb2f --- /dev/null +++ b/src/com/android/tv/dvr/ui/playback/DvrPlayer.java @@ -0,0 +1,583 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tv.dvr.ui.playback; + +import android.media.PlaybackParams; +import android.media.tv.TvContentRating; +import android.media.tv.TvInputManager; +import android.media.tv.TvTrackInfo; +import android.media.tv.TvView; +import android.media.session.PlaybackState; +import android.text.TextUtils; +import android.util.Log; + +import com.android.tv.dvr.data.RecordedProgram; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +class DvrPlayer { + private static final String TAG = "DvrPlayer"; + private static final boolean DEBUG = false; + + /** + * The max rewinding speed supported by DVR player. + */ + public static final int MAX_REWIND_SPEED = 256; + /** + * The max fast-forwarding speed supported by DVR player. + */ + public static final int MAX_FAST_FORWARD_SPEED = 256; + + private static final long SEEK_POSITION_MARGIN_MS = TimeUnit.SECONDS.toMillis(2); + private static final long REWIND_POSITION_MARGIN_MS = 32; // Workaround value. b/29994826 + + private RecordedProgram mProgram; + private long mInitialSeekPositionMs; + private final TvView mTvView; + private DvrPlayerCallback mCallback; + private OnAspectRatioChangedListener mOnAspectRatioChangedListener; + private OnContentBlockedListener mOnContentBlockedListener; + private OnTracksAvailabilityChangedListener mOnTracksAvailabilityChangedListener; + private OnTrackSelectedListener mOnAudioTrackSelectedListener; + private OnTrackSelectedListener mOnSubtitleTrackSelectedListener; + private String mSelectedAudioTrackId; + private String mSelectedSubtitleTrackId; + private float mAspectRatio = Float.NaN; + private int mPlaybackState = PlaybackState.STATE_NONE; + private long mTimeShiftCurrentPositionMs; + private boolean mPauseOnPrepared; + private boolean mHasClosedCaption; + private boolean mHasMultiAudio; + private final PlaybackParams mPlaybackParams = new PlaybackParams(); + private final DvrPlayerCallback mEmptyCallback = new DvrPlayerCallback(); + private long mStartPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME; + private boolean mTimeShiftPlayAvailable; + + public static class DvrPlayerCallback { + /** + * Called when the playback position is changed. The normal updating frequency is + * around 1 sec., which is restricted to the implementation of + * {@link android.media.tv.TvInputService}. + */ + public void onPlaybackPositionChanged(long positionMs) { } + /** + * Called when the playback state or the playback speed is changed. + */ + public void onPlaybackStateChanged(int playbackState, int playbackSpeed) { } + /** + * Called when the playback toward the end. + */ + public void onPlaybackEnded() { } + } + + public interface OnAspectRatioChangedListener { + /** + * Called when the Video's aspect ratio is changed. + * + * @param videoAspectRatio The aspect ratio of video. 0 stands for unknown ratios. + * Listeners should handle it carefully. + */ + void onAspectRatioChanged(float videoAspectRatio); + } + + public interface OnContentBlockedListener { + /** + * Called when the Video's aspect ratio is changed. + */ + void onContentBlocked(TvContentRating rating); + } + + public interface OnTracksAvailabilityChangedListener { + /** + * Called when the Video's subtitle or audio tracks are changed. + */ + void onTracksAvailabilityChanged(boolean hasClosedCaption, boolean hasMultiAudio); + } + + public interface OnTrackSelectedListener { + /** + * Called when certain subtitle or audio track is selected. + */ + void onTrackSelected(String selectedTrackId); + } + + public DvrPlayer(TvView tvView) { + mTvView = tvView; + mTvView.setCaptionEnabled(true); + mPlaybackParams.setSpeed(1.0f); + setTvViewCallbacks(); + setCallback(null); + } + + /** + * Prepares playback. + * + * @param doPlay indicates DVR player do or do not start playback after media is prepared. + */ + public void prepare(boolean doPlay) throws IllegalStateException { + if (DEBUG) Log.d(TAG, "prepare()"); + if (mProgram == null) { + throw new IllegalStateException("Recorded program not set"); + } else if (mPlaybackState != PlaybackState.STATE_NONE) { + throw new IllegalStateException("Playback is already prepared"); + } + mTvView.timeShiftPlay(mProgram.getInputId(), mProgram.getUri()); + mPlaybackState = PlaybackState.STATE_CONNECTING; + mPauseOnPrepared = !doPlay; + mCallback.onPlaybackStateChanged(mPlaybackState, 1); + } + + /** + * Resumes playback. + */ + public void play() throws IllegalStateException { + if (DEBUG) Log.d(TAG, "play()"); + if (!isPlaybackPrepared()) { + throw new IllegalStateException("Recorded program not set or video not ready yet"); + } + switch (mPlaybackState) { + case PlaybackState.STATE_FAST_FORWARDING: + case PlaybackState.STATE_REWINDING: + setPlaybackSpeed(1); + break; + default: + mTvView.timeShiftResume(); + } + mPlaybackState = PlaybackState.STATE_PLAYING; + mCallback.onPlaybackStateChanged(mPlaybackState, 1); + } + + /** + * Pauses playback. + */ + public void pause() throws IllegalStateException { + if (DEBUG) Log.d(TAG, "pause()"); + if (!isPlaybackPrepared()) { + throw new IllegalStateException("Recorded program not set or playback not started yet"); + } + switch (mPlaybackState) { + case PlaybackState.STATE_FAST_FORWARDING: + case PlaybackState.STATE_REWINDING: + setPlaybackSpeed(1); + // falls through + case PlaybackState.STATE_PLAYING: + mTvView.timeShiftPause(); + mPlaybackState = PlaybackState.STATE_PAUSED; + break; + default: + break; + } + mCallback.onPlaybackStateChanged(mPlaybackState, 1); + } + + /** + * Fast-forwards playback with the given speed. If the given speed is larger than + * {@value #MAX_FAST_FORWARD_SPEED}, uses {@value #MAX_FAST_FORWARD_SPEED}. + */ + public void fastForward(int speed) throws IllegalStateException { + if (DEBUG) Log.d(TAG, "fastForward()"); + if (!isPlaybackPrepared()) { + throw new IllegalStateException("Recorded program not set or playback not started yet"); + } + if (speed <= 0) { + throw new IllegalArgumentException("Speed cannot be negative or 0"); + } + if (mTimeShiftCurrentPositionMs >= mProgram.getDurationMillis() - SEEK_POSITION_MARGIN_MS) { + return; + } + speed = Math.min(speed, MAX_FAST_FORWARD_SPEED); + if (DEBUG) Log.d(TAG, "Let's play with speed: " + speed); + setPlaybackSpeed(speed); + mPlaybackState = PlaybackState.STATE_FAST_FORWARDING; + mCallback.onPlaybackStateChanged(mPlaybackState, speed); + } + + /** + * Rewinds playback with the given speed. If the given speed is larger than + * {@value #MAX_REWIND_SPEED}, uses {@value #MAX_REWIND_SPEED}. + */ + public void rewind(int speed) throws IllegalStateException { + if (DEBUG) Log.d(TAG, "rewind()"); + if (!isPlaybackPrepared()) { + throw new IllegalStateException("Recorded program not set or playback not started yet"); + } + if (speed <= 0) { + throw new IllegalArgumentException("Speed cannot be negative or 0"); + } + if (mTimeShiftCurrentPositionMs <= REWIND_POSITION_MARGIN_MS) { + return; + } + speed = Math.min(speed, MAX_REWIND_SPEED); + if (DEBUG) Log.d(TAG, "Let's play with speed: " + speed); + setPlaybackSpeed(-speed); + mPlaybackState = PlaybackState.STATE_REWINDING; + mCallback.onPlaybackStateChanged(mPlaybackState, speed); + } + + /** + * Seeks playback to the specified position. + */ + public void seekTo(long positionMs) throws IllegalStateException { + if (DEBUG) Log.d(TAG, "seekTo()"); + if (!isPlaybackPrepared()) { + throw new IllegalStateException("Recorded program not set or playback not started yet"); + } + if (mProgram == null || mPlaybackState == PlaybackState.STATE_NONE) { + return; + } + positionMs = getRealSeekPosition(positionMs, SEEK_POSITION_MARGIN_MS); + if (DEBUG) Log.d(TAG, "Now: " + getPlaybackPosition() + ", shift to: " + positionMs); + mTvView.timeShiftSeekTo(positionMs + mStartPositionMs); + if (mPlaybackState == PlaybackState.STATE_FAST_FORWARDING || + mPlaybackState == PlaybackState.STATE_REWINDING) { + mPlaybackState = PlaybackState.STATE_PLAYING; + mTvView.timeShiftResume(); + mCallback.onPlaybackStateChanged(mPlaybackState, 1); + } + } + + /** + * Resets playback. + */ + public void reset() { + if (DEBUG) Log.d(TAG, "reset()"); + mCallback.onPlaybackStateChanged(PlaybackState.STATE_NONE, 1); + mPlaybackState = PlaybackState.STATE_NONE; + mTvView.reset(); + mTimeShiftPlayAvailable = false; + mStartPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME; + mTimeShiftCurrentPositionMs = 0; + mPlaybackParams.setSpeed(1.0f); + mProgram = null; + mSelectedAudioTrackId = null; + mSelectedSubtitleTrackId = null; + } + + /** + * Sets callbacks for playback. + */ + public void setCallback(DvrPlayerCallback callback) { + if (callback != null) { + mCallback = callback; + } else { + mCallback = mEmptyCallback; + } + } + + /** + * Sets the listener to aspect ratio changing. + */ + public void setOnAspectRatioChangedListener(OnAspectRatioChangedListener listener) { + mOnAspectRatioChangedListener = listener; + } + + /** + * Sets the listener to content blocking. + */ + public void setOnContentBlockedListener(OnContentBlockedListener listener) { + mOnContentBlockedListener = listener; + } + + /** + * Sets the listener to tracks changing. + */ + public void setOnTracksAvailabilityChangedListener( + OnTracksAvailabilityChangedListener listener) { + mOnTracksAvailabilityChangedListener = listener; + } + + /** + * Sets the listener to tracks of the given type being selected. + * + * @param trackType should be either {@link TvTrackInfo#TYPE_AUDIO} + * or {@link TvTrackInfo#TYPE_SUBTITLE}. + */ + public void setOnTrackSelectedListener(int trackType, OnTrackSelectedListener listener) { + if (trackType == TvTrackInfo.TYPE_AUDIO) { + mOnAudioTrackSelectedListener = listener; + } else if (trackType == TvTrackInfo.TYPE_SUBTITLE) { + mOnSubtitleTrackSelectedListener = listener; + } + } + + /** + * Gets the listener to tracks of the given type being selected. + */ + public OnTrackSelectedListener getOnTrackSelectedListener(int trackType) { + if (trackType == TvTrackInfo.TYPE_AUDIO) { + return mOnAudioTrackSelectedListener; + } else if (trackType == TvTrackInfo.TYPE_SUBTITLE) { + return mOnSubtitleTrackSelectedListener; + } + return null; + } + + /** + * Sets recorded programs for playback. If the player is playing another program, stops it. + */ + public void setProgram(RecordedProgram program, long initialSeekPositionMs) { + if (mProgram != null && mProgram.equals(program)) { + return; + } + if (mPlaybackState != PlaybackState.STATE_NONE) { + reset(); + } + mInitialSeekPositionMs = initialSeekPositionMs; + mProgram = program; + } + + /** + * Returns the recorded program now playing. + */ + public RecordedProgram getProgram() { + return mProgram; + } + + /** + * Returns the currrent playback posistion in msecs. + */ + public long getPlaybackPosition() { + return mTimeShiftCurrentPositionMs; + } + + /** + * Returns the playback speed currently used. + */ + public int getPlaybackSpeed() { + return (int) mPlaybackParams.getSpeed(); + } + + /** + * Returns the playback state defined in {@link android.media.session.PlaybackState}. + */ + public int getPlaybackState() { + return mPlaybackState; + } + + /** + * Returns the subtitle tracks of the current playback. + */ + public ArrayList getSubtitleTracks() { + return new ArrayList<>(mTvView.getTracks(TvTrackInfo.TYPE_SUBTITLE)); + } + + /** + * Returns the audio tracks of the current playback. + */ + public ArrayList getAudioTracks() { + return new ArrayList<>(mTvView.getTracks(TvTrackInfo.TYPE_AUDIO)); + } + + /** + * Returns the ID of the selected track of the given type. + */ + public String getSelectedTrackId(int trackType) { + if (trackType == TvTrackInfo.TYPE_AUDIO) { + return mSelectedAudioTrackId; + } else if (trackType == TvTrackInfo.TYPE_SUBTITLE) { + return mSelectedSubtitleTrackId; + } + return null; + } + + /** + * Returns if playback of the recorded program is started. + */ + public boolean isPlaybackPrepared() { + return mPlaybackState != PlaybackState.STATE_NONE + && mPlaybackState != PlaybackState.STATE_CONNECTING; + } + + /** + * Selects the given track. + * + * @return ID of the selected track. + */ + String selectTrack(int trackType, TvTrackInfo selectedTrack) { + String oldSelectedTrackId = getSelectedTrackId(trackType); + String newSelectedTrackId = selectedTrack == null ? null : selectedTrack.getId(); + if (!TextUtils.equals(oldSelectedTrackId, newSelectedTrackId)) { + if (selectedTrack == null) { + mTvView.selectTrack(trackType, null); + return null; + } else { + List tracks = mTvView.getTracks(trackType); + if (tracks != null && tracks.contains(selectedTrack)) { + mTvView.selectTrack(trackType, newSelectedTrackId); + return newSelectedTrackId; + } else if (trackType == TvTrackInfo.TYPE_SUBTITLE && oldSelectedTrackId != null) { + // Track not found, disabled closed caption. + mTvView.selectTrack(trackType, null); + return null; + } + } + } + return oldSelectedTrackId; + } + + private void setSelectedTrackId(int trackType, String trackId) { + if (trackType == TvTrackInfo.TYPE_AUDIO) { + mSelectedAudioTrackId = trackId; + } else if (trackType == TvTrackInfo.TYPE_SUBTITLE) { + mSelectedSubtitleTrackId = trackId; + } + } + + private void setPlaybackSpeed(int speed) { + mPlaybackParams.setSpeed(speed); + mTvView.timeShiftSetPlaybackParams(mPlaybackParams); + } + + private long getRealSeekPosition(long seekPositionMs, long endMarginMs) { + return Math.max(0, Math.min(seekPositionMs, mProgram.getDurationMillis() - endMarginMs)); + } + + private void setTvViewCallbacks() { + mTvView.setTimeShiftPositionCallback(new TvView.TimeShiftPositionCallback() { + @Override + public void onTimeShiftStartPositionChanged(String inputId, long timeMs) { + if (DEBUG) Log.d(TAG, "onTimeShiftStartPositionChanged:" + timeMs); + mStartPositionMs = timeMs; + if (mTimeShiftPlayAvailable) { + resumeToWatchedPositionIfNeeded(); + } + } + + @Override + public void onTimeShiftCurrentPositionChanged(String inputId, long timeMs) { + if (DEBUG) Log.d(TAG, "onTimeShiftCurrentPositionChanged: " + timeMs); + if (!mTimeShiftPlayAvailable) { + // Workaround of b/31436263 + return; + } + // Workaround of b/32211561, TIF won't report start position when TIS report + // its start position as 0. In that case, we have to do the prework of playback + // on the first time we get current position, and the start position should be 0 + // at that time. + if (mStartPositionMs == TvInputManager.TIME_SHIFT_INVALID_TIME) { + mStartPositionMs = 0; + resumeToWatchedPositionIfNeeded(); + } + timeMs -= mStartPositionMs; + if (mPlaybackState == PlaybackState.STATE_REWINDING + && timeMs <= REWIND_POSITION_MARGIN_MS) { + play(); + } else { + mTimeShiftCurrentPositionMs = getRealSeekPosition(timeMs, 0); + mCallback.onPlaybackPositionChanged(mTimeShiftCurrentPositionMs); + if (timeMs >= mProgram.getDurationMillis()) { + pause(); + mCallback.onPlaybackEnded(); + } + } + } + }); + mTvView.setCallback(new TvView.TvInputCallback() { + @Override + public void onTimeShiftStatusChanged(String inputId, int status) { + if (DEBUG) Log.d(TAG, "onTimeShiftStatusChanged:" + status); + if (status == TvInputManager.TIME_SHIFT_STATUS_AVAILABLE + && mPlaybackState == PlaybackState.STATE_CONNECTING) { + mTimeShiftPlayAvailable = true; + if (mStartPositionMs != TvInputManager.TIME_SHIFT_INVALID_TIME) { + // onTimeShiftStatusChanged is sometimes called after + // onTimeShiftStartPositionChanged is called. In this case, + // resumeToWatchedPositionIfNeeded needs to be called here. + resumeToWatchedPositionIfNeeded(); + } + } + } + + @Override + public void onTracksChanged(String inputId, List tracks) { + boolean hasClosedCaption = + !mTvView.getTracks(TvTrackInfo.TYPE_SUBTITLE).isEmpty(); + boolean hasMultiAudio = mTvView.getTracks(TvTrackInfo.TYPE_AUDIO).size() > 1; + if ((hasClosedCaption != mHasClosedCaption || hasMultiAudio != mHasMultiAudio) + && mOnTracksAvailabilityChangedListener != null) { + mOnTracksAvailabilityChangedListener + .onTracksAvailabilityChanged(hasClosedCaption, hasMultiAudio); + } + mHasClosedCaption = hasClosedCaption; + mHasMultiAudio = hasMultiAudio; + } + + @Override + public void onTrackSelected(String inputId, int type, String trackId) { + if (type == TvTrackInfo.TYPE_AUDIO || type == TvTrackInfo.TYPE_SUBTITLE) { + setSelectedTrackId(type, trackId); + OnTrackSelectedListener listener = getOnTrackSelectedListener(type); + if (listener != null) { + listener.onTrackSelected(trackId); + } + } else if (type == TvTrackInfo.TYPE_VIDEO && trackId != null + && mOnAspectRatioChangedListener != null) { + List trackInfos = mTvView.getTracks(TvTrackInfo.TYPE_VIDEO); + if (trackInfos != null) { + for (TvTrackInfo trackInfo : trackInfos) { + if (trackInfo.getId().equals(trackId)) { + float videoAspectRatio; + int videoWidth = trackInfo.getVideoWidth(); + int videoHeight = trackInfo.getVideoHeight(); + if (videoWidth > 0 && videoHeight > 0) { + videoAspectRatio = trackInfo.getVideoPixelAspectRatio() + * trackInfo.getVideoWidth() / trackInfo.getVideoHeight(); + } else { + // Aspect ratio is unknown. Pass the message to listeners. + videoAspectRatio = 0; + } + if (DEBUG) Log.d(TAG, "Aspect Ratio: " + videoAspectRatio); + if (mAspectRatio != videoAspectRatio || videoAspectRatio == 0) { + mOnAspectRatioChangedListener + .onAspectRatioChanged(videoAspectRatio); + mAspectRatio = videoAspectRatio; + return; + } + } + } + } + } + } + + @Override + public void onContentBlocked(String inputId, TvContentRating rating) { + if (mOnContentBlockedListener != null) { + mOnContentBlockedListener.onContentBlocked(rating); + } + } + }); + } + + private void resumeToWatchedPositionIfNeeded() { + if (mInitialSeekPositionMs != TvInputManager.TIME_SHIFT_INVALID_TIME) { + mTvView.timeShiftSeekTo(getRealSeekPosition(mInitialSeekPositionMs, + SEEK_POSITION_MARGIN_MS) + mStartPositionMs); + mInitialSeekPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME; + } + if (mPauseOnPrepared) { + mTvView.timeShiftPause(); + mPlaybackState = PlaybackState.STATE_PAUSED; + mPauseOnPrepared = false; + } else { + mTvView.timeShiftResume(); + mPlaybackState = PlaybackState.STATE_PLAYING; + } + mCallback.onPlaybackStateChanged(mPlaybackState, 1); + } +} \ No newline at end of file diff --git a/src/com/android/tv/experiments/ExperimentFlag.java b/src/com/android/tv/experiments/ExperimentFlag.java index 8f60c2b5..c0cbd643 100644 --- a/src/com/android/tv/experiments/ExperimentFlag.java +++ b/src/com/android/tv/experiments/ExperimentFlag.java @@ -16,12 +16,19 @@ package com.android.tv.experiments; +import android.support.annotation.VisibleForTesting; /** * Experiments return values based on user, device and other criteria. */ public final class ExperimentFlag { - private final T mDefaultValue; + + private static boolean sAllowOverrides = false; + + @VisibleForTesting + public static void initForTest() { + sAllowOverrides = true; + } /** Returns a boolean experiment */ public static ExperimentFlag createFlag( @@ -30,6 +37,11 @@ public final class ExperimentFlag { defaultValue); } + private final T mDefaultValue; + + private T mOverrideValue = null; + private boolean mOverridden = false; + private ExperimentFlag( T defaultValue) { mDefaultValue = defaultValue; @@ -37,6 +49,22 @@ public final class ExperimentFlag { /** Returns value for this experiment */ public T get() { - return mDefaultValue; + return sAllowOverrides && mOverridden ? mOverrideValue : mDefaultValue; } + + @VisibleForTesting + public void override(T t) { + if (sAllowOverrides) { + mOverridden = true; + mOverrideValue = t; + } + } + + @VisibleForTesting + public void resetOverride() { + mOverridden = false; + } + + + } diff --git a/src/com/android/tv/experiments/Experiments.java b/src/com/android/tv/experiments/Experiments.java index f16c8d1e..e17fc300 100644 --- a/src/com/android/tv/experiments/Experiments.java +++ b/src/com/android/tv/experiments/Experiments.java @@ -23,11 +23,16 @@ import com.android.tv.common.BuildConfig; /** * Set of experiments visible in AOSP. * - *

- * This file is maintained by hand. + *

This file is maintained by hand. */ public final class Experiments { public static final ExperimentFlag CLOUD_EPG = createFlag( + true); + + /** + * Use network tuner if it is available and there is no other tuner types. + */ + public static final ExperimentFlag NETWORK_TUNER = createFlag( false); /** diff --git a/src/com/android/tv/guide/ProgramGuide.java b/src/com/android/tv/guide/ProgramGuide.java index 120b3dba..bec4e462 100644 --- a/src/com/android/tv/guide/ProgramGuide.java +++ b/src/com/android/tv/guide/ProgramGuide.java @@ -48,7 +48,7 @@ import com.android.tv.ChannelTuner; import com.android.tv.Features; import com.android.tv.MainActivity; import com.android.tv.R; -import com.android.tv.analytics.DurationTimer; +import com.android.tv.util.DurationTimer; import com.android.tv.analytics.Tracker; import com.android.tv.common.WeakHandler; import com.android.tv.data.ChannelDataManager; diff --git a/src/com/android/tv/guide/ProgramItemView.java b/src/com/android/tv/guide/ProgramItemView.java index 4c7a4404..d5fb418f 100644 --- a/src/com/android/tv/guide/ProgramItemView.java +++ b/src/com/android/tv/guide/ProgramItemView.java @@ -44,8 +44,8 @@ import com.android.tv.analytics.Tracker; import com.android.tv.common.feature.CommonFeatures; import com.android.tv.data.Channel; import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.DvrUiHelper; -import com.android.tv.dvr.ScheduledRecording; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.ui.DvrUiHelper; import com.android.tv.guide.ProgramManager.TableEntry; import com.android.tv.util.ToastUtils; import com.android.tv.util.Utils; @@ -106,18 +106,19 @@ public class ProgramItemView extends TextView { }, entry.getWidth() > ((ProgramItemView) view).mMaxWidthForRipple ? 0 : view.getResources() .getInteger(R.integer.program_guide_ripple_anim_duration)); - } else if (CommonFeatures.DVR.isEnabled(view.getContext())) { + } else if (entry.program != null && CommonFeatures.DVR.isEnabled(view.getContext())) { DvrManager dvrManager = singletons.getDvrManager(); if (entry.entryStartUtcMillis > System.currentTimeMillis() && dvrManager.isProgramRecordable(entry.program)) { if (entry.scheduledRecording == null) { - if (DvrUiHelper.checkStorageStatusAndShowErrorMessage(tvActivity, - channel.getInputId()) - && DvrUiHelper.handleCreateSchedule(tvActivity, entry.program)) { - String msg = view.getContext().getString( - R.string.dvr_msg_program_scheduled, entry.program.getTitle()); - ToastUtils.show(view.getContext(), msg, Toast.LENGTH_SHORT); - } + DvrUiHelper.checkStorageStatusAndShowErrorMessage(tvActivity, + channel.getInputId(), new Runnable() { + @Override + public void run() { + DvrUiHelper.requestRecordingFutureProgram(tvActivity, + entry.program, false); + } + }); } else { dvrManager.removeScheduledRecording(entry.scheduledRecording); String msg = view.getResources().getString( diff --git a/src/com/android/tv/guide/ProgramManager.java b/src/com/android/tv/guide/ProgramManager.java index e3d919df..e543fd05 100644 --- a/src/com/android/tv/guide/ProgramManager.java +++ b/src/com/android/tv/guide/ProgramManager.java @@ -29,7 +29,7 @@ import com.android.tv.data.ProgramDataManager; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrScheduleManager; import com.android.tv.dvr.DvrScheduleManager.OnConflictStateChangeListener; -import com.android.tv.dvr.ScheduledRecording; +import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.Utils; @@ -439,11 +439,24 @@ public class ProgramManager { mChannels = mChannelDataManager.getBrowsableChannelList(); mSelectedGenreId = GenreItems.ID_ALL_CHANNELS; mFilteredChannels = mChannels; + updateTableEntriesWithoutNotification(clearPreviousTableEntries); + // Channel update notification should be called after updating table entries, so that + // the listener can get the entries. notifyChannelsUpdated(); - updateTableEntries(clearPreviousTableEntries); + notifyTableEntriesUpdated(); + buildGenreFilters(); } private void updateTableEntries(boolean clear) { + updateTableEntriesWithoutNotification(clear); + notifyTableEntriesUpdated(); + buildGenreFilters(); + } + + /** + * Updates the table entries without notifying the change. + */ + private void updateTableEntriesWithoutNotification(boolean clear) { if (clear) { mChannelIdEntriesMap.clear(); } @@ -491,9 +504,6 @@ public class ProgramManager { } } } - - notifyTableEntriesUpdated(); - buildGenreFilters(); } private void notifyGenresUpdated() { diff --git a/src/com/android/tv/guide/ProgramTableAdapter.java b/src/com/android/tv/guide/ProgramTableAdapter.java index e4a67972..b9a0593d 100644 --- a/src/com/android/tv/guide/ProgramTableAdapter.java +++ b/src/com/android/tv/guide/ProgramTableAdapter.java @@ -45,19 +45,21 @@ import android.view.ViewGroup; import android.view.ViewParent; import android.view.ViewTreeObserver; import android.view.accessibility.AccessibilityManager; +import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; import com.android.tv.R; import com.android.tv.TvApplication; +import com.android.tv.common.TvCommonUtils; import com.android.tv.common.feature.CommonFeatures; import com.android.tv.data.Channel; import com.android.tv.data.Program; import com.android.tv.data.Program.CriticScore; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.ScheduledRecording; +import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.guide.ProgramManager.TableEntriesUpdatedListener; import com.android.tv.parental.ParentalControlSettings; import com.android.tv.ui.HardwareLayerAnimatorListenerAdapter; @@ -241,16 +243,6 @@ public class ProgramTableAdapter extends RecyclerView.Adapter { private static final long TIME_UNIT_MS = TimeUnit.MINUTES.toMillis(30); + + // Ex. 3:00 AM + private static final String TIME_PATTERN_SAME_DAY = "h:mm a"; + // Ex. Oct 21, 3:00 AM + private static final String TIME_PATTERN_DIFFERENT_DAY = "MMM d, h:mm a"; + private static int sRowHeaderOverlapping; // Nearest half hour at or before the start time. private long mStartUtcMs; + private final String mTimePatternSameDay; + private final String mTimePatternDifferentDay; public TimeListAdapter(Resources res) { if (sRowHeaderOverlapping == 0) { sRowHeaderOverlapping = Math.abs(res.getDimensionPixelOffset( R.dimen.program_guide_table_header_row_overlap)); } + Locale locale = res.getConfiguration().locale; + mTimePatternSameDay = DateFormat.getBestDateTimePattern(locale, TIME_PATTERN_SAME_DAY); + mTimePatternDifferentDay = + DateFormat.getBestDateTimePattern(locale, TIME_PATTERN_DIFFERENT_DAY); } public void update(long startTimeMs) { @@ -68,10 +83,14 @@ public class TimeListAdapter extends RecyclerView.Adapter { +public class ActionCardView extends RelativeLayout implements ItemListRowView.CardView { private static final String TAG = MenuView.TAG; private static final boolean DEBUG = MenuView.DEBUG; @@ -66,7 +66,7 @@ public class ActionCardView extends FrameLayout implements ItemListRowView.CardV } mIconView.setImageDrawable(action.getDrawable(getContext())); mLabelView.setText(action.getActionName(getContext())); - mStateView.setText(action.getActionDescription(getContext())); + mStateView.setText(action.getActionDescription()); if (action.isEnabled()) { setEnabled(true); setFocusable(true); diff --git a/src/com/android/tv/menu/AppLinkCardView.java b/src/com/android/tv/menu/AppLinkCardView.java index bfb5e3f1..d23d9a00 100644 --- a/src/com/android/tv/menu/AppLinkCardView.java +++ b/src/com/android/tv/menu/AppLinkCardView.java @@ -24,6 +24,7 @@ import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; +import android.os.AsyncTask; import android.support.annotation.Nullable; import android.support.v7.graphics.Palette; import android.text.TextUtils; @@ -55,7 +56,6 @@ public class AppLinkCardView extends BaseCardView { private final int mIconColorFilter; private ImageView mImageView; - private View mGradientView; private TextView mAppInfoView; private View mMetaViewHolder; private Channel mChannel; @@ -102,35 +102,115 @@ public class AppLinkCardView extends BaseCardView { int linkType = mChannel.getAppLinkType(getContext()); mIntent = mChannel.getAppLinkIntent(getContext()); + CharSequence appLabel = null; + mImageView.setForeground(null); switch (linkType) { case Channel.APP_LINK_TYPE_CHANNEL: setText(mChannel.getAppLinkText()); mAppInfoView.setVisibility(VISIBLE); - mGradientView.setVisibility(VISIBLE); mAppInfoView.setCompoundDrawablePadding(mIconPadding); - mAppInfoView.setCompoundDrawables(null, null, null, null); - mAppInfoView.setText(mPackageManager.getApplicationLabel(appInfo)); + mAppInfoView.setCompoundDrawablesRelative(null, null, null, null); + appLabel = mTvInputManagerHelper.getTvInputApplicationLabel(channel.getInputId()); + if (appLabel != null) { + mAppInfoView.setText(appLabel); + } else { + new AsyncTask() { + private final String mLoadTvInputId = mChannel.getInputId(); + + @Override + protected CharSequence doInBackground(Void... params) { + if (appInfo != null) { + return mPackageManager.getApplicationLabel(appInfo); + } + return null; + } + + @Override + protected void onPostExecute(CharSequence appLabel) { + mTvInputManagerHelper.setTvInputApplicationLabel( + mLoadTvInputId, appLabel); + if (mLoadTvInputId != mChannel.getInputId() || !isAttachedToWindow()) { + return; + } + mAppInfoView.setText(appLabel); + } + }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } if (!TextUtils.isEmpty(mChannel.getAppLinkIconUri())) { mChannel.loadBitmap(getContext(), Channel.LOAD_IMAGE_TYPE_APP_LINK_ICON, - mIconWidth, mIconHeight, createChannelLogoCallback(this, mChannel, - Channel.LOAD_IMAGE_TYPE_APP_LINK_ICON)); + mIconWidth, mIconHeight, + createChannelLogoCallback( + this, mChannel, Channel.LOAD_IMAGE_TYPE_APP_LINK_ICON)); } else if (appInfo.icon != 0) { - Drawable appIcon = mPackageManager.getApplicationIcon(appInfo); - BitmapUtils.setColorFilterToDrawable(mIconColorFilter, appIcon); - appIcon.setBounds(0, 0, mIconWidth, mIconHeight); - mAppInfoView.setCompoundDrawables(appIcon, null, null, null); + Drawable appIcon = + mTvInputManagerHelper.getTvInputApplicationIcon(mChannel.getInputId()); + if (appIcon != null) { + BitmapUtils.setColorFilterToDrawable(mIconColorFilter, appIcon); + appIcon.setBounds(0, 0, mIconWidth, mIconHeight); + mAppInfoView.setCompoundDrawablesRelative(appIcon, null, null, null); + } else { + new AsyncTask() { + private final String mLoadTvInputId = mChannel.getInputId(); + + @Override + protected Drawable doInBackground(Void... params) { + if (appInfo != null) { + return mPackageManager.getApplicationIcon(appInfo); + } + return null; + } + + @Override + protected void onPostExecute(Drawable appIcon) { + mTvInputManagerHelper.setTvInputApplicationIcon( + mLoadTvInputId, appIcon); + if (mLoadTvInputId != mChannel.getInputId() + || !isAttachedToWindow()) { + return; + } + BitmapUtils.setColorFilterToDrawable(mIconColorFilter, appIcon); + appIcon.setBounds(0, 0, mIconWidth, mIconHeight); + mAppInfoView.setCompoundDrawablesRelative(appIcon, null, null, null); + } + }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } } break; case Channel.APP_LINK_TYPE_APP: - setText(getContext().getString( - R.string.channels_item_app_link_app_launcher, - mPackageManager.getApplicationLabel(appInfo))); + appLabel = mTvInputManagerHelper.getTvInputApplicationLabel(mChannel.getInputId()); + if (appLabel != null) { + setText(getContext() + .getString(R.string.channels_item_app_link_app_launcher, appLabel)); + } else { + new AsyncTask() { + private final String mLoadTvInputId = mChannel.getInputId(); + + @Override + protected CharSequence doInBackground(Void... params) { + if (appInfo != null) { + return mPackageManager.getApplicationLabel(appInfo); + } + return null; + } + + @Override + protected void onPostExecute(CharSequence appLabel) { + mTvInputManagerHelper.setTvInputApplicationLabel( + mLoadTvInputId, appLabel); + if (mLoadTvInputId != mChannel.getInputId() || !isAttachedToWindow()) { + return; + } + setText(getContext() + .getString( + R.string.channels_item_app_link_app_launcher, + appLabel)); + } + }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } mAppInfoView.setVisibility(GONE); - mGradientView.setVisibility(GONE); break; default: mAppInfoView.setVisibility(GONE); - mGradientView.setVisibility(GONE); Log.d(TAG, "Should not be here."); } @@ -148,8 +228,6 @@ public class AppLinkCardView extends BaseCardView { } else { setCardImageWithBanner(appInfo); } - // Call super.onBind() at the end intentionally. In order to correctly handle extension of - // text view, text should be set before calling super.onBind. super.onBind(channel, selected); } @@ -182,13 +260,14 @@ public class AppLinkCardView extends BaseCardView { } } BitmapUtils.setColorFilterToDrawable(mIconColorFilter, drawable); - mAppInfoView.setCompoundDrawables(drawable, null, null, null); + mAppInfoView.setCompoundDrawablesRelative(drawable, null, null, null); } else if (type == Channel.LOAD_IMAGE_TYPE_APP_LINK_POSTER_ART) { if (bitmap == null) { setCardImageWithBanner( mTvInputManagerHelper.getTvInputAppInfo(mChannel.getInputId())); } else { mImageView.setImageBitmap(bitmap); + mImageView.setForeground(getContext().getDrawable(R.drawable.card_image_gradient)); if (mChannel.getAppLinkColor() == 0) { extractAndSetMetaViewBackgroundColor(bitmap); } @@ -200,7 +279,6 @@ public class AppLinkCardView extends BaseCardView { protected void onFinishInflate() { super.onFinishInflate(); mImageView = (ImageView) findViewById(R.id.image); - mGradientView = findViewById(R.id.image_gradient); mAppInfoView = (TextView) findViewById(R.id.app_info); mMetaViewHolder = findViewById(R.id.app_link_text_holder); } @@ -209,37 +287,85 @@ public class AppLinkCardView extends BaseCardView { // 1) Provided poster art image, 2) Activity banner, 3) Activity icon, 4) Application banner, // 5) Application icon, and 6) default image. private void setCardImageWithBanner(ApplicationInfo appInfo) { - Drawable banner = null; - if (mIntent != null) { - try { - banner = mPackageManager.getActivityBanner(mIntent); - if (banner == null) { - banner = mPackageManager.getActivityIcon(mIntent); + new AsyncTask() { + private String mLoadTvInputId = mChannel.getInputId(); + @Override + protected Drawable doInBackground(Void... params) { + Drawable banner = null; + if (mIntent != null) { + try { + banner = mPackageManager.getActivityBanner(mIntent); + if (banner == null) { + banner = mPackageManager.getActivityIcon(mIntent); + } + } catch (PackageManager.NameNotFoundException e) { + // do nothing. + } } - } catch (PackageManager.NameNotFoundException e) { - // do nothing. + return banner; } - } - if (banner == null && appInfo != null) { - if (appInfo.banner != 0) { - banner = mPackageManager.getApplicationBanner(appInfo); - } - if (banner == null && appInfo.icon != 0) { - banner = mPackageManager.getApplicationIcon(appInfo); + @Override + protected void onPostExecute(Drawable banner) { + if (mLoadTvInputId != mChannel.getInputId() || !isAttachedToWindow()) { + return; + } + if (banner != null) { + setCardImageWithBannerInternal(banner); + } else { + setCardImageWithApplicationInfoBanner(appInfo); + } } + }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + + private void setCardImageWithApplicationInfoBanner(ApplicationInfo appInfo) { + Drawable appBanner = + mTvInputManagerHelper.getTvInputApplicationBanner(mChannel.getInputId()); + if (appBanner != null) { + setCardImageWithBannerInternal(appBanner); + } else { + new AsyncTask() { + private final String mLoadTvInputId = mChannel.getInputId(); + @Override + protected Drawable doInBackground(Void... params) { + Drawable banner = null; + if (appInfo != null) { + if (appInfo.banner != 0) { + banner = mPackageManager.getApplicationBanner(appInfo); + } + if (banner == null && appInfo.icon != 0) { + banner = mPackageManager.getApplicationIcon(appInfo); + } + } + return banner; + } + + @Override + protected void onPostExecute(Drawable banner) { + mTvInputManagerHelper.setTvInputApplicationBanner( + mLoadTvInputId, banner); + if (mLoadTvInputId != mChannel.getInputId() || !isAttachedToWindow()) { + return; + } + setCardImageWithBannerInternal(banner); + } + }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } + } + private void setCardImageWithBannerInternal(Drawable banner) { if (banner == null) { mImageView.setImageResource(R.drawable.ic_recent_thumbnail_default); mImageView.setBackgroundResource(R.color.channel_card); } else { - Bitmap bitmap = - Bitmap.createBitmap(mCardImageWidth, mCardImageHeight, Bitmap.Config.ARGB_8888); + Bitmap bitmap = Bitmap.createBitmap( + mCardImageWidth, mCardImageHeight, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); banner.setBounds(0, 0, mCardImageWidth, mCardImageHeight); banner.draw(canvas); mImageView.setImageDrawable(banner); + mImageView.setForeground(getContext().getDrawable(R.drawable.card_image_gradient)); if (mChannel.getAppLinkColor() == 0) { extractAndSetMetaViewBackgroundColor(bitmap); } diff --git a/src/com/android/tv/menu/BaseCardView.java b/src/com/android/tv/menu/BaseCardView.java index c6a34a5d..fa74ce3e 100644 --- a/src/com/android/tv/menu/BaseCardView.java +++ b/src/com/android/tv/menu/BaseCardView.java @@ -57,6 +57,7 @@ public abstract class BaseCardView extends LinearLayout implements ItemListRo private TextView mTextViewFocused; private final int mCardImageWidth; private final float mCardHeight; + private boolean mSelected; public BaseCardView(Context context) { this(context, null); @@ -103,23 +104,9 @@ public abstract class BaseCardView extends LinearLayout implements ItemListRo /** * Called when the view is displayed. - * - * Before onBind is called, this view's text should be set to determine if it'll be extended - * or not in focus state. */ @Override public void onBind(T item, boolean selected) { - if (mTextView != null && mTextViewFocused != null) { - mTextViewFocused.measure( - MeasureSpec.makeMeasureSpec(mCardImageWidth, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); - mExtendViewOnFocus = mTextViewFocused.getLineCount() > 1; - if (mExtendViewOnFocus) { - setTextViewFocusedAlpha(selected ? 1f : 0f); - } else { - setTextViewFocusedAlpha(1f); - } - } setFocusAnimatedValue(selected ? SCALE_FACTOR_1F : SCALE_FACTOR_0F); } @@ -128,6 +115,7 @@ public abstract class BaseCardView extends LinearLayout implements ItemListRo @Override public void onSelected() { + mSelected = true; if (isAttachedToWindow() && getVisibility() == View.VISIBLE) { startFocusAnimation(SCALE_FACTOR_1F); } else { @@ -138,6 +126,7 @@ public abstract class BaseCardView extends LinearLayout implements ItemListRo @Override public void onDeselected() { + mSelected = false; if (isAttachedToWindow() && getVisibility() == View.VISIBLE) { startFocusAnimation(SCALE_FACTOR_0F); } else { @@ -156,6 +145,7 @@ public abstract class BaseCardView extends LinearLayout implements ItemListRo if (mTextView != null) { mTextView.setText(resId); } + onTextViewUpdated(); } /** @@ -168,6 +158,22 @@ public abstract class BaseCardView extends LinearLayout implements ItemListRo if (mTextView != null) { mTextView.setText(text); } + onTextViewUpdated(); + } + + private void onTextViewUpdated() { + if (mTextView != null && mTextViewFocused != null) { + mTextViewFocused.measure( + MeasureSpec.makeMeasureSpec(mCardImageWidth, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); + mExtendViewOnFocus = mTextViewFocused.getLineCount() > 1; + if (mExtendViewOnFocus) { + setTextViewFocusedAlpha(mSelected ? 1f : 0f); + } else { + setTextViewFocusedAlpha(1f); + } + } + setFocusAnimatedValue(mSelected ? SCALE_FACTOR_1F : SCALE_FACTOR_0F); } /** @@ -209,12 +215,18 @@ public abstract class BaseCardView extends LinearLayout implements ItemListRo setScaleX(scale); setScaleY(scale); setTranslationZ(mFocusTranslationZ * animatedValue); - if (mExtendViewOnFocus) { + if (mTextView != null && mTextViewFocused != null) { ViewGroup.LayoutParams params = mTextView.getLayoutParams(); - params.height = Math.round(mTextViewHeight - + (mExtendedTextViewHeight - mTextViewHeight) * animatedValue); - setTextViewLayoutParams(params); - setTextViewFocusedAlpha(animatedValue); + int height = mExtendViewOnFocus ? Math.round(mTextViewHeight + + (mExtendedTextViewHeight - mTextViewHeight) * animatedValue) + : (int) mTextViewHeight; + if (height != params.height) { + params.height = height; + setTextViewLayoutParams(params); + } + if (mExtendViewOnFocus) { + setTextViewFocusedAlpha(animatedValue); + } } } diff --git a/src/com/android/tv/menu/ChannelCardView.java b/src/com/android/tv/menu/ChannelCardView.java index 1c8015a6..4ee56892 100644 --- a/src/com/android/tv/menu/ChannelCardView.java +++ b/src/com/android/tv/menu/ChannelCardView.java @@ -45,7 +45,6 @@ public class ChannelCardView extends BaseCardView { private final int mCardImageHeight; private ImageView mImageView; - private View mGradientView; private TextView mChannelNumberNameView; private ProgressBar mProgressBar; private Channel mChannel; @@ -71,7 +70,6 @@ public class ChannelCardView extends BaseCardView { protected void onFinishInflate() { super.onFinishInflate(); mImageView = (ImageView) findViewById(R.id.image); - mGradientView = findViewById(R.id.image_gradient); mChannelNumberNameView = (TextView) findViewById(R.id.channel_number_and_name); mProgressBar = (ProgressBar) findViewById(R.id.progress); } @@ -88,7 +86,7 @@ public class ChannelCardView extends BaseCardView { mChannelNumberNameView.setVisibility(VISIBLE); mImageView.setImageResource(R.drawable.ic_recent_thumbnail_default); mImageView.setBackgroundResource(R.color.channel_card); - mGradientView.setVisibility(View.GONE); + mImageView.setForeground(null); mProgressBar.setVisibility(GONE); setTextViewEnabled(true); @@ -101,8 +99,6 @@ public class ChannelCardView extends BaseCardView { } updateProgramInformation(); - // Call super.onBind() at the end intentionally. In order to correctly handle extension of - // text view, text should be set before calling super.onBind. super.onBind(channel, selected); } @@ -123,7 +119,7 @@ public class ChannelCardView extends BaseCardView { private void updatePosterArt(Bitmap posterArt) { mImageView.setImageBitmap(posterArt); - mGradientView.setVisibility(View.VISIBLE); + mImageView.setForeground(getContext().getDrawable(R.drawable.card_image_gradient)); } private void updateProgramInformation() { diff --git a/src/com/android/tv/menu/ChannelsRow.java b/src/com/android/tv/menu/ChannelsRow.java index dedf0993..490d73de 100644 --- a/src/com/android/tv/menu/ChannelsRow.java +++ b/src/com/android/tv/menu/ChannelsRow.java @@ -26,8 +26,14 @@ import com.android.tv.recommendation.Recommender; public class ChannelsRow extends ItemListRow { public static final String ID = ChannelsRow.class.getName(); - private static final int MIN_COUNT_FOR_RECENT_CHANNELS = 5; - private static final int MAX_COUNT_FOR_RECENT_CHANNELS = 10; + /** + * Minimum count for recent channels. + */ + public static final int MIN_COUNT_FOR_RECENT_CHANNELS = 5; + /** + * Maximum count for recent channels. + */ + public static final int MAX_COUNT_FOR_RECENT_CHANNELS = 10; private Recommender mTvRecommendation; private ChannelsRowAdapter mChannelsAdapter; diff --git a/src/com/android/tv/menu/ChannelsRowAdapter.java b/src/com/android/tv/menu/ChannelsRowAdapter.java index c8e1bd05..4ba6a93a 100644 --- a/src/com/android/tv/menu/ChannelsRowAdapter.java +++ b/src/com/android/tv/menu/ChannelsRowAdapter.java @@ -31,7 +31,6 @@ import com.android.tv.dvr.DvrDataManager; import com.android.tv.recommendation.Recommender; import com.android.tv.util.SetupUtils; import com.android.tv.util.TvInputManagerHelper; -import com.android.tv.util.Utils; import java.util.ArrayList; import java.util.List; @@ -130,8 +129,6 @@ public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter @Override public void onBindViewHolder(MyViewHolder viewHolder, int position) { - super.onBindViewHolder(viewHolder, position); - int viewType = getItemViewType(position); if (viewType == R.layout.menu_card_guide) { viewHolder.itemView.setOnClickListener(mGuideOnClickListener); @@ -147,6 +144,7 @@ public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter viewHolder.itemView.setTag(getItemList().get(position)); viewHolder.itemView.setOnClickListener(mChannelOnClickListener); } + super.onBindViewHolder(viewHolder, position); } @Override diff --git a/src/com/android/tv/menu/ItemListRowView.java b/src/com/android/tv/menu/ItemListRowView.java index 4919c595..01257628 100644 --- a/src/com/android/tv/menu/ItemListRowView.java +++ b/src/com/android/tv/menu/ItemListRowView.java @@ -28,6 +28,7 @@ import android.view.ViewGroup; import com.android.tv.MainActivity; import com.android.tv.R; +import com.android.tv.util.ViewCache; import java.util.Collections; import java.util.List; @@ -194,9 +195,20 @@ public class ItemListRowView extends MenuRowView implements OnChildSelectedListe return mItemList.size(); } + /** + * Returns the position of the item. + */ + protected int getItemPosition(T item) { + return mItemList.indexOf(item); + } + @Override public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - View view = mLayoutInflater.inflate(getLayoutResId(viewType), parent, false); + int resId = getLayoutResId(viewType); + View view = ViewCache.getInstance().getView(resId); + if (view == null) { + view = mLayoutInflater.inflate(resId, parent, false); + } return new MyViewHolder(view); } diff --git a/src/com/android/tv/menu/Menu.java b/src/com/android/tv/menu/Menu.java index 1160a5b5..25e629c1 100644 --- a/src/com/android/tv/menu/Menu.java +++ b/src/com/android/tv/menu/Menu.java @@ -27,23 +27,30 @@ import android.support.annotation.IntDef; import android.support.annotation.NonNull; import android.support.annotation.VisibleForTesting; import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; import com.android.tv.ChannelTuner; import com.android.tv.R; import com.android.tv.TvApplication; -import com.android.tv.analytics.DurationTimer; +import com.android.tv.TvOptionsManager; import com.android.tv.analytics.Tracker; import com.android.tv.common.TvCommonUtils; import com.android.tv.common.WeakHandler; import com.android.tv.menu.MenuRowFactory.PartnerRow; -import com.android.tv.menu.MenuRowFactory.PipOptionsRow; import com.android.tv.menu.MenuRowFactory.TvOptionsRow; import com.android.tv.ui.TunableTvView; +import com.android.tv.util.DurationTimer; +import com.android.tv.util.ViewCache; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; /** * A class which controls the menu. @@ -81,10 +88,21 @@ public class Menu { sRowIdListForReason.add(PlayControlsRow.ID); // REASON_PLAY_CONTROLS_JUMP_TO_NEXT } + private static final Map PRELOAD_VIEW_IDS = new HashMap<>(); + static { + PRELOAD_VIEW_IDS.put(R.layout.menu_card_guide, 1); + PRELOAD_VIEW_IDS.put(R.layout.menu_card_setup, 1); + PRELOAD_VIEW_IDS.put(R.layout.menu_card_dvr, 1); + PRELOAD_VIEW_IDS.put(R.layout.menu_card_app_link, 1); + PRELOAD_VIEW_IDS.put(R.layout.menu_card_channel, ChannelsRow.MAX_COUNT_FOR_RECENT_CHANNELS); + PRELOAD_VIEW_IDS.put(R.layout.menu_card_action, 7); + } + private static final String SCREEN_NAME = "Menu"; private static final int MSG_HIDE_MENU = 1000; + private final Context mContext; private final IMenuView mMenuView; private final Tracker mTracker; private final DurationTimer mVisibleTimer = new DurationTimer(); @@ -103,15 +121,16 @@ public class Menu { @VisibleForTesting Menu(Context context, IMenuView menuView, MenuRowFactory menuRowFactory, OnMenuVisibilityChangeListener onMenuVisibilityChangeListener) { - this(context, null, menuView, menuRowFactory, onMenuVisibilityChangeListener); + this(context, null, null, menuView, menuRowFactory, onMenuVisibilityChangeListener); } - public Menu(Context context, TunableTvView tvView, IMenuView menuView, - MenuRowFactory menuRowFactory, + public Menu(Context context, TunableTvView tvView, TvOptionsManager optionsManager, + IMenuView menuView, MenuRowFactory menuRowFactory, OnMenuVisibilityChangeListener onMenuVisibilityChangeListener) { + mContext = context; mMenuView = menuView; mTracker = TvApplication.getSingletons(context).getTracker(); - mMenuUpdater = new MenuUpdater(context, tvView, this); + mMenuUpdater = new MenuUpdater(this, tvView, optionsManager); Resources res = context.getResources(); mShowDurationMillis = res.getInteger(R.integer.menu_show_duration); mOnMenuVisibilityChangeListener = onMenuVisibilityChangeListener; @@ -130,7 +149,6 @@ public class Menu { addMenuRow(menuRowFactory.createMenuRow(this, ChannelsRow.class)); addMenuRow(menuRowFactory.createMenuRow(this, PartnerRow.class)); addMenuRow(menuRowFactory.createMenuRow(this, TvOptionsRow.class)); - addMenuRow(menuRowFactory.createMenuRow(this, PipOptionsRow.class)); mMenuView.setMenuRows(mMenuRows); } @@ -159,6 +177,23 @@ public class Menu { mHandler.removeCallbacksAndMessages(null); } + /** + * Preloads the item view used for the menu. + */ + public void preloadItemViews() { + LayoutInflater inflater = + (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + // Use a fake parent to make the layoutParams set correctly. + ViewGroup fakeParent = new LinearLayout(mContext); + for (int id : PRELOAD_VIEW_IDS.keySet()) { + int count = PRELOAD_VIEW_IDS.get(id); + for (int i = 0; i < count; i++) { + View view = inflater.inflate(id, fakeParent, false); + ViewCache.getInstance().putView(id, view); + } + } + } + /** * Shows the main menu. * diff --git a/src/com/android/tv/menu/MenuAction.java b/src/com/android/tv/menu/MenuAction.java index 0d59552a..b4356059 100644 --- a/src/com/android/tv/menu/MenuAction.java +++ b/src/com/android/tv/menu/MenuAction.java @@ -20,9 +20,9 @@ import android.content.Context; import android.graphics.drawable.Drawable; import android.text.TextUtils; -import com.android.tv.MainActivity; import com.android.tv.R; import com.android.tv.TvOptionsManager; +import com.android.tv.TvOptionsManager.OptionType; /** * A class to define possible actions from main menu. @@ -36,12 +36,9 @@ public class MenuAction { public static final MenuAction SELECT_DISPLAY_MODE_ACTION = new MenuAction(R.string.options_item_display_mode, TvOptionsManager.OPTION_DISPLAY_MODE, R.drawable.ic_tvoption_aspect); - public static final MenuAction PIP_IN_APP_ACTION = - new MenuAction(R.string.options_item_pip, TvOptionsManager.OPTION_IN_APP_PIP, - R.drawable.ic_tvoption_pip); public static final MenuAction SYSTEMWIDE_PIP_ACTION = new MenuAction(R.string.options_item_pip, TvOptionsManager.OPTION_SYSTEMWIDE_PIP, - R.drawable.ic_pip_option_layout2); + R.drawable.ic_tvoption_pip); public static final MenuAction SELECT_AUDIO_LANGUAGE_ACTION = new MenuAction(R.string.options_item_multi_audio, TvOptionsManager.OPTION_MULTI_AUDIO, R.drawable.ic_tvoption_multi_track); @@ -51,34 +48,36 @@ public class MenuAction { public static final MenuAction DEV_ACTION = new MenuAction(R.string.options_item_developer, TvOptionsManager.OPTION_DEVELOPER, R.drawable.ic_developer_mode_tv_white_48dp); - // TODO: Change the icon. public static final MenuAction SETTINGS_ACTION = new MenuAction(R.string.options_item_settings, TvOptionsManager.OPTION_SETTINGS, R.drawable.ic_settings); - // Actions in the PIP option row. - public static final MenuAction PIP_SELECT_INPUT_ACTION = - new MenuAction(R.string.pip_options_item_source, TvOptionsManager.OPTION_PIP_INPUT, - R.drawable.ic_pip_option_input); - public static final MenuAction PIP_SWAP_ACTION = - new MenuAction(R.string.pip_options_item_swap, TvOptionsManager.OPTION_PIP_SWAP, - R.drawable.ic_pip_option_swap); - public static final MenuAction PIP_SOUND_ACTION = - new MenuAction(R.string.pip_options_item_sound, TvOptionsManager.OPTION_PIP_SOUND, - R.drawable.ic_pip_option_swap_audio); - public static final MenuAction PIP_LAYOUT_ACTION = - new MenuAction(R.string.pip_options_item_layout, TvOptionsManager.OPTION_PIP_LAYOUT, - R.drawable.ic_pip_option_layout1); - public static final MenuAction PIP_SIZE_ACTION = - new MenuAction(R.string.pip_options_item_size, TvOptionsManager.OPTION_PIP_SIZE, - R.drawable.ic_pip_option_size); private final String mActionName; private final int mActionNameResId; - private final int mType; + @OptionType private final int mType; + private String mActionDescription; private Drawable mDrawable; private int mDrawableResId; private boolean mEnabled = true; + /** + * Sets the action description. Returns {@code trye} if the description is changed. + */ + public static boolean setActionDescription(MenuAction action, String actionDescription) { + String oldDescription = action.mActionDescription; + action.mActionDescription = actionDescription; + return !TextUtils.equals(action.mActionDescription, oldDescription); + } + + /** + * Enables or disables the action. Returns {@code true} if the value is changed. + */ + public static boolean setEnabled(MenuAction action, boolean enabled) { + boolean changed = action.mEnabled != enabled; + action.mEnabled = enabled; + return changed; + } + public MenuAction(int actionNameResId, int type, int drawableResId) { mActionName = null; mActionNameResId = actionNameResId; @@ -102,11 +101,11 @@ public class MenuAction { return context.getString(mActionNameResId); } - public String getActionDescription(Context context) { - return ((MainActivity) context).getTvOptionsManager().getOptionString(mType); + public String getActionDescription() { + return mActionDescription; } - public int getType() { + @OptionType public int getType() { return mType; } @@ -120,28 +119,10 @@ public class MenuAction { return mDrawable; } - /** - * Sets drawable resource id. - * - * @return {@code true} if drawable is changed. - */ - public boolean setDrawableResId(int resId) { - if (mDrawableResId == resId) { - return false; - } - mDrawable = null; - mDrawableResId = resId; - return true; - } - public boolean isEnabled() { return mEnabled; } - public void setEnabled(boolean enabled) { - mEnabled = enabled; - } - public int getActionNameResId() { return mActionNameResId; } diff --git a/src/com/android/tv/menu/MenuLayoutManager.java b/src/com/android/tv/menu/MenuLayoutManager.java index 6c767247..a16ac197 100644 --- a/src/com/android/tv/menu/MenuLayoutManager.java +++ b/src/com/android/tv/menu/MenuLayoutManager.java @@ -384,10 +384,15 @@ public class MenuLayoutManager { mSelectedPosition = position; if (DEBUG) dumpChildren("startRowAnimation()"); - MenuRowView currentView = mMenuRowViews.get(position); // Show the children of the next row. - currentView.getTitleView().setVisibility(View.VISIBLE); - currentView.getContentsView().setVisibility(View.VISIBLE); + final MenuRowView currentView = mMenuRowViews.get(position); + TextView currentTitleView = currentView.getTitleView(); + View currentContentsView = currentView.getContentsView(); + currentTitleView.setVisibility(View.VISIBLE); + currentContentsView.setVisibility(View.VISIBLE); + if (currentView instanceof PlayControlsRowView) { + ((PlayControlsRowView) currentView).onPreselected(); + } // Request focus after the new contents view shows up. mMenuView.requestFocus(); if (mTempTitleViewForOld == null) { @@ -407,7 +412,7 @@ public class MenuLayoutManager { // Old row. MenuRow oldRow = mMenuRows.get(oldPosition); - MenuRowView oldView = mMenuRowViews.get(oldPosition); + final MenuRowView oldView = mMenuRowViews.get(oldPosition); View oldContentsView = oldView.getContentsView(); // Old contents view. animators.add(createAlphaAnimator(oldContentsView, 1.0f, 0.0f, 1.0f, mLinearOutSlowIn) @@ -468,8 +473,6 @@ public class MenuLayoutManager { } // Current row. Rect currentLayoutRect = new Rect(layouts.get(position)); - TextView currentTitleView = currentView.getTitleView(); - View currentContentsView = currentView.getContentsView(); currentContentsView.setAlpha(0.0f); if (scrollDown) { // Current title view. @@ -572,9 +575,8 @@ public class MenuLayoutManager { for (ViewPropertyValueHolder holder : propertyValuesAfterAnimation) { holder.property.set(holder.view, holder.value); } - oldTitleView.setVisibility(View.VISIBLE); - mMenuRowViews.get(oldPosition).onDeselected(); - mMenuRowViews.get(position).onSelected(true); + oldView.onDeselected(); + currentView.onSelected(true); mTempTitleViewForOld.setVisibility(View.GONE); mTempTitleViewForCurrent.setVisibility(View.GONE); layout(mMenuView.getLeft(), mMenuView.getTop(), mMenuView.getRight(), diff --git a/src/com/android/tv/menu/MenuRowFactory.java b/src/com/android/tv/menu/MenuRowFactory.java index c67a0e04..2d5453fe 100644 --- a/src/com/android/tv/menu/MenuRowFactory.java +++ b/src/com/android/tv/menu/MenuRowFactory.java @@ -67,8 +67,6 @@ public class MenuRowFactory { } else if (TvOptionsRow.class.equals(key)) { return new TvOptionsRow(mMainActivity, menu, mTvCustomizationManager .getCustomActions(TvCustomizationManager.ID_OPTIONS_ROW)); - } else if (PipOptionsRow.class.equals(key)) { - return new PipOptionsRow(mMainActivity, menu); } return null; } @@ -77,6 +75,9 @@ public class MenuRowFactory { * A menu row which represents the TV options row. */ public static class TvOptionsRow extends ItemListRow { + /** The ID of the row. */ + public static final String ID = TvOptionsRow.class.getName(); + private TvOptionsRow(Context context, Menu menu, List customActions) { super(context, menu, R.string.menu_title_options, R.dimen.action_card_height, new TvOptionsRowAdapter(context, customActions)); @@ -90,25 +91,6 @@ public class MenuRowFactory { } } - /** - * A menu row which represents the PIP options row. - */ - public static class PipOptionsRow extends ItemListRow { - private final MainActivity mMainActivity; - - private PipOptionsRow(Context context, Menu menu) { - super(context, menu, R.string.menu_title_pip_options, R.dimen.action_card_height, - new PipOptionsRowAdapter(context)); - mMainActivity = (MainActivity) context; - } - - @Override - public boolean isVisible() { - // TODO: Remove the dependency on MainActivity. - return super.isVisible() && mMainActivity.isPipEnabled(); - } - } - /** * A menu row which represents the partner row. */ diff --git a/src/com/android/tv/menu/MenuUpdater.java b/src/com/android/tv/menu/MenuUpdater.java index 075b299e..7ad38e74 100644 --- a/src/com/android/tv/menu/MenuUpdater.java +++ b/src/com/android/tv/menu/MenuUpdater.java @@ -16,11 +16,14 @@ package com.android.tv.menu; -import android.content.Context; import android.support.annotation.Nullable; import com.android.tv.ChannelTuner; +import com.android.tv.TvOptionsManager; +import com.android.tv.TvOptionsManager.OptionChangedListener; +import com.android.tv.TvOptionsManager.OptionType; import com.android.tv.data.Channel; +import com.android.tv.menu.MenuRowFactory.TvOptionsRow; import com.android.tv.ui.TunableTvView; import com.android.tv.ui.TunableTvView.OnScreenBlockingChangedListener; @@ -30,10 +33,10 @@ import com.android.tv.ui.TunableTvView.OnScreenBlockingChangedListener; *

As the menu is updated when it shows up, this class handles only the dynamic updates. */ public class MenuUpdater { - // Can be null for testing. - @Nullable - private final TunableTvView mTvView; private final Menu mMenu; + // Can be null for testing. + @Nullable private final TunableTvView mTvView; + @Nullable private final TvOptionsManager mOptionsManager; private ChannelTuner mChannelTuner; private final ChannelTuner.Listener mChannelTunerListener = new ChannelTuner.Listener() { @@ -42,7 +45,7 @@ public class MenuUpdater { @Override public void onBrowsableChannelListChanged() { - mMenu.update(); + mMenu.update(ChannelsRow.ID); } @Override @@ -53,10 +56,17 @@ public class MenuUpdater { mMenu.update(ChannelsRow.ID); } }; + private final OptionChangedListener mOptionChangeListener = new OptionChangedListener() { + @Override + public void onOptionChanged(@OptionType int optionType, String newString) { + mMenu.update(TvOptionsRow.ID); + } + }; - public MenuUpdater(Context context, TunableTvView tvView, Menu menu) { - mTvView = tvView; + public MenuUpdater(Menu menu, TunableTvView tvView, TvOptionsManager optionsManager) { mMenu = menu; + mTvView = tvView; + mOptionsManager = optionsManager; if (mTvView != null) { mTvView.setOnScreenBlockedListener(new OnScreenBlockingChangedListener() { @Override @@ -65,11 +75,18 @@ public class MenuUpdater { } }); } + if (mOptionsManager != null) { + mOptionsManager.setOptionChangedListener(TvOptionsManager.OPTION_CLOSED_CAPTIONS, + mOptionChangeListener); + mOptionsManager.setOptionChangedListener(TvOptionsManager.OPTION_DISPLAY_MODE, + mOptionChangeListener); + mOptionsManager.setOptionChangedListener(TvOptionsManager.OPTION_MULTI_AUDIO, + mOptionChangeListener); + } } /** - * Sets the instance of {@link ChannelTuner}. Call this method when the channel tuner is ready - * or not available any more. + * Sets the instance of {@link ChannelTuner}. Call this method when the channel tuner is ready. */ public void setChannelTuner(ChannelTuner channelTuner) { if (mChannelTuner != null) { @@ -79,7 +96,6 @@ public class MenuUpdater { if (mChannelTuner != null) { mChannelTuner.addListener(mChannelTunerListener); } - mMenu.update(); } /** @@ -92,5 +108,10 @@ public class MenuUpdater { if (mTvView != null) { mTvView.setOnScreenBlockedListener(null); } + if (mOptionsManager != null) { + mOptionsManager.setOptionChangedListener(TvOptionsManager.OPTION_CLOSED_CAPTIONS, null); + mOptionsManager.setOptionChangedListener(TvOptionsManager.OPTION_DISPLAY_MODE, null); + mOptionsManager.setOptionChangedListener(TvOptionsManager.OPTION_MULTI_AUDIO, null); + } } } diff --git a/src/com/android/tv/menu/OptionsRowAdapter.java b/src/com/android/tv/menu/OptionsRowAdapter.java index 93bd0a4d..dd6194a1 100644 --- a/src/com/android/tv/menu/OptionsRowAdapter.java +++ b/src/com/android/tv/menu/OptionsRowAdapter.java @@ -21,8 +21,6 @@ import android.view.View; import com.android.tv.R; import com.android.tv.TvApplication; -import com.android.tv.TvOptionsManager; -import com.android.tv.TvOptionsManager.OptionChangedListener; import com.android.tv.analytics.Tracker; import java.util.List; @@ -66,12 +64,9 @@ public abstract class OptionsRowAdapter extends ItemListRowView.ItemListAdapter< public void update() { if (mActionList == null) { mActionList = createActions(); - updateActions(); setItemList(mActionList); } else { - if (updateActions()) { - setItemList(mActionList); - } + updateActions(); } } @@ -81,7 +76,7 @@ public abstract class OptionsRowAdapter extends ItemListRowView.ItemListAdapter< } protected abstract List createActions(); - protected abstract boolean updateActions(); + protected abstract void updateActions(); protected abstract void executeAction(int type); /** @@ -93,37 +88,6 @@ public abstract class OptionsRowAdapter extends ItemListRowView.ItemListAdapter< return mActionList.get(position); } - /** - * Sets the action at the given position. - * Note that action at the position may differ from returned by {@link #createActions}. - * See {@link CustomizableOptionsRowAdapter} - */ - protected void setAction(int position, MenuAction action) { - mActionList.set(position, action); - } - - /** - * Adds an action to the given position. - * Note that action at the position may differ from returned by {@link #createActions}. - * See {@link CustomizableOptionsRowAdapter} - */ - protected void addAction(int position, MenuAction action) { - mActionList.add(position, action); - } - - /** - * Removes an action at the given position. - * Note that action at the position may differ from returned by {@link #createActions}. - * See {@link CustomizableOptionsRowAdapter} - */ - protected void removeAction(int position) { - mActionList.remove(position); - } - - protected int getActionSize() { - return mActionList.size(); - } - @Override public void onBindViewHolder(MyViewHolder viewHolder, int position) { super.onBindViewHolder(viewHolder, position); @@ -139,14 +103,4 @@ public abstract class OptionsRowAdapter extends ItemListRowView.ItemListAdapter< // be preserved. return mActionList.get(position).getType(); } - - protected void setOptionChangedListener(final MenuAction action) { - TvOptionsManager om = getMainActivity().getTvOptionsManager(); - om.setOptionChangedListener(action.getType(), new OptionChangedListener() { - @Override - public void onOptionChanged(String newOption) { - setItemList(mActionList); - } - }); - } } diff --git a/src/com/android/tv/menu/PartnerOptionsRowAdapter.java b/src/com/android/tv/menu/PartnerOptionsRowAdapter.java index f3e09f80..c8249a4c 100644 --- a/src/com/android/tv/menu/PartnerOptionsRowAdapter.java +++ b/src/com/android/tv/menu/PartnerOptionsRowAdapter.java @@ -38,8 +38,7 @@ public class PartnerOptionsRowAdapter extends CustomizableOptionsRowAdapter { } @Override - protected boolean updateActions() { + protected void updateActions() { // TODO: Support adding description for custom actions. - return false; } } diff --git a/src/com/android/tv/menu/PipOptionsRowAdapter.java b/src/com/android/tv/menu/PipOptionsRowAdapter.java deleted file mode 100644 index 87203e9d..00000000 --- a/src/com/android/tv/menu/PipOptionsRowAdapter.java +++ /dev/null @@ -1,137 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.menu; - -import android.content.Context; -import android.text.TextUtils; - -import com.android.tv.MainActivity; -import com.android.tv.R; -import com.android.tv.TvOptionsManager; -import com.android.tv.ui.TvViewUiManager; -import com.android.tv.ui.sidepanel.PipInputSelectorFragment; -import com.android.tv.util.PipInputManager.PipInput; -import com.android.tv.util.TvSettings; - -import java.util.ArrayList; -import java.util.List; - -/* - * An adapter of PIP options. - */ -public class PipOptionsRowAdapter extends OptionsRowAdapter { - private static final int[] DRAWABLE_ID_FOR_LAYOUT = { - R.drawable.ic_pip_option_layout1, - R.drawable.ic_pip_option_layout2, - R.drawable.ic_pip_option_layout3, - R.drawable.ic_pip_option_layout4, - R.drawable.ic_pip_option_layout5 }; - - private final TvOptionsManager mTvOptionsManager; - private final TvViewUiManager mTvViewUiManager; - - public PipOptionsRowAdapter(Context context) { - super(context); - mTvOptionsManager = getMainActivity().getTvOptionsManager(); - mTvViewUiManager = getMainActivity().getTvViewUiManager(); - } - - @Override - protected List createActions() { - List actionList = new ArrayList<>(); - actionList.add(MenuAction.PIP_SELECT_INPUT_ACTION); - actionList.add(MenuAction.PIP_SWAP_ACTION); - actionList.add(MenuAction.PIP_SOUND_ACTION); - actionList.add(MenuAction.PIP_LAYOUT_ACTION); - actionList.add(MenuAction.PIP_SIZE_ACTION); - for (MenuAction action : actionList) { - setOptionChangedListener(action); - } - return actionList; - } - - @Override - public boolean updateActions() { - boolean changed = false; - if (updateSelectInputAction()) { - changed = true; - } - if (updateLayoutAction()) { - changed = true; - } - if (updateSizeAction()) { - changed = true; - } - return changed; - } - - private boolean updateSelectInputAction() { - String oldInputLabel = mTvOptionsManager.getOptionString(TvOptionsManager.OPTION_PIP_INPUT); - - MainActivity tvActivity = getMainActivity(); - PipInput newInput = tvActivity.getPipInputManager().getPipInput(tvActivity.getPipChannel()); - String newInputLabel = newInput == null ? null : newInput.getLabel(); - - if (!TextUtils.equals(oldInputLabel, newInputLabel)) { - mTvOptionsManager.onPipInputChanged(newInputLabel); - return true; - } - return false; - } - - private boolean updateLayoutAction() { - return MenuAction.PIP_LAYOUT_ACTION.setDrawableResId( - DRAWABLE_ID_FOR_LAYOUT[mTvViewUiManager.getPipLayout()]); - } - - private boolean updateSizeAction() { - boolean oldEnabled = MenuAction.PIP_SIZE_ACTION.isEnabled(); - boolean newEnabled = mTvViewUiManager.getPipLayout() != TvSettings.PIP_LAYOUT_SIDE_BY_SIDE; - if (oldEnabled != newEnabled) { - MenuAction.PIP_SIZE_ACTION.setEnabled(newEnabled); - return true; - } - return false; - } - - @Override - protected void executeAction(int type) { - switch (type) { - case TvOptionsManager.OPTION_PIP_INPUT: - getMainActivity().getOverlayManager().getSideFragmentManager().show( - new PipInputSelectorFragment()); - break; - case TvOptionsManager.OPTION_PIP_SWAP: - getMainActivity().swapPip(); - break; - case TvOptionsManager.OPTION_PIP_SOUND: - getMainActivity().togglePipSoundMode(); - break; - case TvOptionsManager.OPTION_PIP_LAYOUT: - int oldLayout = mTvViewUiManager.getPipLayout(); - int newLayout = (oldLayout + 1) % (TvSettings.PIP_LAYOUT_LAST + 1); - mTvViewUiManager.setPipLayout(newLayout, true); - MenuAction.PIP_LAYOUT_ACTION.setDrawableResId(DRAWABLE_ID_FOR_LAYOUT[newLayout]); - break; - case TvOptionsManager.OPTION_PIP_SIZE: - int oldSize = mTvViewUiManager.getPipSize(); - int newSize = (oldSize + 1) % (TvSettings.PIP_SIZE_LAST + 1); - mTvViewUiManager.setPipSize(newSize, true); - break; - } - } -} diff --git a/src/com/android/tv/menu/PlayControlsButton.java b/src/com/android/tv/menu/PlayControlsButton.java index aff39db3..77715f28 100644 --- a/src/com/android/tv/menu/PlayControlsButton.java +++ b/src/com/android/tv/menu/PlayControlsButton.java @@ -39,6 +39,9 @@ public class PlayControlsButton extends FrameLayout { private final int mIconColor; private int mIconFocusedColor; + private int mImageResourceId; + private int mTintColor; + public PlayControlsButton(Context context) { this(context, null); } @@ -67,10 +70,21 @@ public class PlayControlsButton extends FrameLayout { * Sets the resource ID of the image to be displayed in the center of this control. */ public void setImageResId(int imageResId) { - mIcon.setImageResource(imageResId); - // Since on foucus changing, icons' color should be switched with animation, + int newTintColor = hasFocus() ? mIconFocusedColor : mIconColor; + if (mImageResourceId != imageResId) { + mImageResourceId = imageResId; + mIcon.setImageResource(imageResId); + updateTint(newTintColor); + } else if (newTintColor != mTintColor) { + updateTint(newTintColor); + } + } + + private void updateTint(int tintColor) { + mTintColor = tintColor; + // Since on focus changing, icons' color should be switched with animation, // as a result, selectors cannot be used to switch colors in this case. - mIcon.getDrawable().setTint(hasFocus() ? mIconFocusedColor : mIconColor); + mIcon.getDrawable().setTint(tintColor); } /** @@ -117,7 +131,9 @@ public class PlayControlsButton extends FrameLayout { } else { mIcon.setVisibility(View.GONE); mLabel.setVisibility(View.VISIBLE); - mLabel.setText(label); + if (!TextUtils.equals(mLabel.getText(), label)) { + mLabel.setText(label); + } } } diff --git a/src/com/android/tv/menu/PlayControlsRowView.java b/src/com/android/tv/menu/PlayControlsRowView.java index a620d4dd..4d766788 100644 --- a/src/com/android/tv/menu/PlayControlsRowView.java +++ b/src/com/android/tv/menu/PlayControlsRowView.java @@ -18,10 +18,10 @@ package com.android.tv.menu; import android.content.Context; import android.content.res.Resources; +import android.text.TextUtils; import android.text.format.DateFormat; import android.util.AttributeSet; import android.view.View; -import android.view.ViewGroup; import android.widget.TextView; import android.widget.Toast; @@ -34,17 +34,16 @@ import com.android.tv.common.SoftPreconditions; import com.android.tv.common.feature.CommonFeatures; import com.android.tv.data.Channel; import com.android.tv.data.Program; +import com.android.tv.dialog.HalfSizedDialogFragment; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrDataManager.OnDvrScheduleLoadFinishedListener; import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.DvrUiHelper; -import com.android.tv.dvr.ScheduledRecording; +import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.dvr.ui.DvrStopRecordingFragment; -import com.android.tv.dvr.ui.HalfSizedDialogFragment; +import com.android.tv.dvr.ui.DvrUiHelper; import com.android.tv.menu.Menu.MenuShowReason; import com.android.tv.ui.TunableTvView; -import com.android.tv.util.Utils; public class PlayControlsRowView extends MenuRowView { private static final int NORMAL_WIDTH_MAX_BUTTON_COUNT = 5; @@ -53,14 +52,10 @@ public class PlayControlsRowView extends MenuRowView { private final int mTimeTextLeftMargin; private final int mTimelineWidth; // Views - private View mBackgroundView; + private TextView mBackgroundView; private View mTimeIndicator; private TextView mTimeText; - private View mProgressEmptyBefore; - private View mProgressWatched; - private View mProgressBuffered; - private View mProgressEmptyAfter; - private View mControlBar; + private PlaybackProgressBar mProgress; private PlayControlsButton mJumpPreviousButton; private PlayControlsButton mRewindButton; private PlayControlsButton mPlayPauseButton; @@ -69,7 +64,6 @@ public class PlayControlsRowView extends MenuRowView { private PlayControlsButton mRecordButton; private TextView mProgramStartTimeText; private TextView mProgramEndTimeText; - private View mUnavailableMessageText; private TunableTvView mTvView; private TimeShiftManager mTimeShiftManager; private final DvrDataManager mDvrDataManager; @@ -83,6 +77,8 @@ public class PlayControlsRowView extends MenuRowView { private final int mNormalButtonMargin; private final int mCompactButtonMargin; + private final String mUnavailableMessage; + private final ScheduledRecordingListener mScheduledRecordingListener = new ScheduledRecordingListener() { @Override @@ -138,6 +134,7 @@ public class PlayControlsRowView extends MenuRowView { mDvrManager = null; } mMainActivity = (MainActivity) context; + mUnavailableMessage = res.getString(R.string.play_controls_unavailable); } @Override @@ -171,14 +168,10 @@ public class PlayControlsRowView extends MenuRowView { super.onFinishInflate(); // Clip the ViewGroup(body) to the rounded rectangle of outline. findViewById(R.id.body).setClipToOutline(true); - mBackgroundView = findViewById(R.id.background); + mBackgroundView = (TextView) findViewById(R.id.background); mTimeIndicator = findViewById(R.id.time_indicator); mTimeText = (TextView) findViewById(R.id.time_text); - mProgressEmptyBefore = findViewById(R.id.timeline_bg_start); - mProgressWatched = findViewById(R.id.watched); - mProgressBuffered = findViewById(R.id.buffered); - mProgressEmptyAfter = findViewById(R.id.timeline_bg_end); - mControlBar = findViewById(R.id.play_control_bar); + mProgress = (PlaybackProgressBar) findViewById(R.id.progress); mJumpPreviousButton = (PlayControlsButton) findViewById(R.id.jump_previous); mRewindButton = (PlayControlsButton) findViewById(R.id.rewind); mPlayPauseButton = (PlayControlsButton) findViewById(R.id.play_pause); @@ -187,7 +180,6 @@ public class PlayControlsRowView extends MenuRowView { mRecordButton = (PlayControlsButton) findViewById(R.id.record); mProgramStartTimeText = (TextView) findViewById(R.id.program_start_time); mProgramEndTimeText = (TextView) findViewById(R.id.program_end_time); - mUnavailableMessageText = findViewById(R.id.unavailable_text); initializeButton(mJumpPreviousButton, R.drawable.lb_ic_skip_previous, R.string.play_controls_description_skip_previous, null, new Runnable() { @@ -195,7 +187,7 @@ public class PlayControlsRowView extends MenuRowView { public void run() { if (mTimeShiftManager.isAvailable()) { mTimeShiftManager.jumpToPrevious(); - updateControls(); + updateControls(true); } } }); @@ -235,7 +227,7 @@ public class PlayControlsRowView extends MenuRowView { public void run() { if (mTimeShiftManager.isAvailable()) { mTimeShiftManager.jumpToNext(); - updateControls(); + updateControls(true); } } }); @@ -265,18 +257,17 @@ public class PlayControlsRowView extends MenuRowView { if (!(mDvrManager != null && mDvrManager.isChannelRecordable(currentChannel))) { Toast.makeText(mMainActivity, R.string.dvr_msg_cannot_record_channel, Toast.LENGTH_SHORT).show(); - } else if (DvrUiHelper.checkStorageStatusAndShowErrorMessage(mMainActivity, - currentChannel.getInputId())) { + } else { Program program = TvApplication.getSingletons(mMainActivity).getProgramDataManager() .getCurrentProgram(currentChannel.getId()); - if (program == null) { - DvrUiHelper.showChannelRecordDurationOptions(mMainActivity, currentChannel); - } else if (DvrUiHelper.handleCreateSchedule(mMainActivity, program)) { - String msg = mMainActivity.getString(R.string.dvr_msg_current_program_scheduled, - program.getTitle(), - Utils.toTimeString(program.getEndTimeUtcMillis(), false)); - Toast.makeText(mMainActivity, msg, Toast.LENGTH_SHORT).show(); - } + DvrUiHelper.checkStorageStatusAndShowErrorMessage(mMainActivity, + currentChannel.getInputId(), new Runnable() { + @Override + public void run() { + DvrUiHelper.requestRecordingCurrentProgram(mMainActivity, + currentChannel, program, true); + } + }); } } else if (currentChannel != null) { DvrUiHelper.showStopRecordingDialog(mMainActivity, currentChannel.getId(), @@ -318,39 +309,37 @@ public class PlayControlsRowView extends MenuRowView { @Override public void onAvailabilityChanged() { updateMenuVisibility(); - if (isShown()) { - PlayControlsRowView.this.updateAll(); - } + PlayControlsRowView.this.updateAll(false); } @Override public void onPlayStatusChanged(int status) { updateMenuVisibility(); - if (mTimeShiftManager.isAvailable() && isShown()) { - updateControls(); + if (mTimeShiftManager.isAvailable()) { + updateControls(false); } } @Override public void onRecordTimeRangeChanged() { - if (mTimeShiftManager.isAvailable() && isShown()) { - updateControls(); + if (mTimeShiftManager.isAvailable()) { + updateControls(false); } } @Override public void onCurrentPositionChanged() { - if (mTimeShiftManager.isAvailable() && isShown()) { + if (mTimeShiftManager.isAvailable()) { initializeTimeline(); - updateControls(); + updateControls(false); } } @Override public void onProgramInfoChanged() { - if (mTimeShiftManager.isAvailable() && isShown()) { + if (mTimeShiftManager.isAvailable()) { initializeTimeline(); - updateControls(); + updateControls(false); } } @@ -372,7 +361,8 @@ public class PlayControlsRowView extends MenuRowView { } } }); - updateAll(); + // force update to initialize everything + updateAll(true); } private void initializeTimeline() { @@ -380,6 +370,8 @@ public class PlayControlsRowView extends MenuRowView { mTimeShiftManager.getCurrentPositionMs()); mProgramStartTimeMs = program.getStartTimeUtcMillis(); mProgramEndTimeMs = program.getEndTimeUtcMillis(); + mProgress.setMax(mProgramEndTimeMs - mProgramStartTimeMs); + updateRecTimeText(); SoftPreconditions.checkArgument(mProgramStartTimeMs <= mProgramEndTimeMs); } @@ -389,10 +381,13 @@ public class PlayControlsRowView extends MenuRowView { getMenu().setKeepVisible(keepMenuVisible); } + public void onPreselected() { + updateControls(true); + } + @Override public void onSelected(boolean showTitle) { super.onSelected(showTitle); - updateControls(); postHideRippleAnimation(); } @@ -474,28 +469,32 @@ public class PlayControlsRowView extends MenuRowView { * Updates the view contents. It is called from the PlayControlsRow. */ public void update() { - updateAll(); + updateAll(false); } - private void updateAll() { + private void updateAll(boolean forceUpdate) { if (mTimeShiftManager.isAvailable() && !mTvView.isScreenBlocked()) { setEnabled(true); initializeTimeline(); mBackgroundView.setEnabled(true); + setTextIfNeeded(mBackgroundView, null); } else { setEnabled(false); mBackgroundView.setEnabled(false); + setTextIfNeeded(mBackgroundView, mUnavailableMessage); } - updateControls(); + // force the controls be updated no matter it's visible or not. + updateControls(forceUpdate); } - private void updateControls() { - updateTime(); - updateProgress(); - updateRecTimeText(); - updateButtons(); - updateRecordButton(); - updateButtonMargin(); + private void updateControls(boolean forceUpdate) { + if (forceUpdate || getContentsView().isShown()) { + updateTime(); + updateProgress(); + updateButtons(); + updateRecordButton(); + updateButtonMargin(); + } } private void updateTime() { @@ -504,70 +503,39 @@ public class PlayControlsRowView extends MenuRowView { mTimeIndicator.setVisibility(View.VISIBLE); } else { mTimeText.setVisibility(View.INVISIBLE); - mTimeIndicator.setVisibility(View.INVISIBLE); + mTimeIndicator.setVisibility(View.GONE); return; } long currentPositionMs = mTimeShiftManager.getCurrentPositionMs(); - ViewGroup.MarginLayoutParams params = - (ViewGroup.MarginLayoutParams) mTimeText.getLayoutParams(); int currentTimePositionPixel = convertDurationToPixel(currentPositionMs - mProgramStartTimeMs); - params.leftMargin = currentTimePositionPixel + mTimeTextLeftMargin; - mTimeText.setLayoutParams(params); - mTimeText.setText(getTimeString(currentPositionMs)); - params = (ViewGroup.MarginLayoutParams) mTimeIndicator.getLayoutParams(); - params.leftMargin = currentTimePositionPixel + mTimeIndicatorLeftMargin; - mTimeIndicator.setLayoutParams(params); + mTimeText.setTranslationX(currentTimePositionPixel + mTimeTextLeftMargin); + setTextIfNeeded(mTimeText, getTimeString(currentPositionMs)); + mTimeIndicator.setTranslationX(currentTimePositionPixel + mTimeIndicatorLeftMargin); } private void updateProgress() { if (isEnabled()) { - mProgressWatched.setVisibility(View.VISIBLE); - mProgressBuffered.setVisibility(View.VISIBLE); - mProgressEmptyAfter.setVisibility(View.VISIBLE); - } else { - mProgressWatched.setVisibility(View.INVISIBLE); - mProgressBuffered.setVisibility(View.INVISIBLE); - mProgressEmptyAfter.setVisibility(View.INVISIBLE); - if (mProgramStartTimeMs < mProgramEndTimeMs) { - layoutProgress(mProgressEmptyBefore, mProgramStartTimeMs, mProgramEndTimeMs); - } else { - // Not initialized yet. - layoutProgress(mProgressEmptyBefore, mTimelineWidth); - } - return; - } - - long progressStartTimeMs = Math.min(mProgramEndTimeMs, + long progressStartTimeMs = Math.min(mProgramEndTimeMs, Math.max(mProgramStartTimeMs, mTimeShiftManager.getRecordStartTimeMs())); - long currentPlayingTimeMs = Math.min(mProgramEndTimeMs, + long currentPlayingTimeMs = Math.min(mProgramEndTimeMs, Math.max(mProgramStartTimeMs, mTimeShiftManager.getCurrentPositionMs())); - long progressEndTimeMs = Math.min(mProgramEndTimeMs, + long progressEndTimeMs = Math.min(mProgramEndTimeMs, Math.max(mProgramStartTimeMs, mTimeShiftManager.getRecordEndTimeMs())); - - layoutProgress(mProgressEmptyBefore, mProgramStartTimeMs, progressStartTimeMs); - layoutProgress(mProgressWatched, progressStartTimeMs, currentPlayingTimeMs); - layoutProgress(mProgressBuffered, currentPlayingTimeMs, progressEndTimeMs); - } - - private void layoutProgress(View progress, long progressStartTimeMs, long progressEndTimeMs) { - layoutProgress(progress, Math.max(0, - convertDurationToPixel(progressEndTimeMs - progressStartTimeMs)) + 1); - } - - private void layoutProgress(View progress, int width) { - ViewGroup.MarginLayoutParams params = - (ViewGroup.MarginLayoutParams) progress.getLayoutParams(); - params.width = width; - progress.setLayoutParams(params); + mProgress.setProgressRange(progressStartTimeMs - mProgramStartTimeMs, + progressEndTimeMs - mProgramStartTimeMs); + mProgress.setProgress(currentPlayingTimeMs - mProgramStartTimeMs); + } else { + mProgress.setProgressRange(0, 0); + } } private void updateRecTimeText() { if (isEnabled()) { mProgramStartTimeText.setVisibility(View.VISIBLE); - mProgramStartTimeText.setText(getTimeString(mProgramStartTimeMs)); + setTextIfNeeded(mProgramStartTimeText, getTimeString(mProgramStartTimeMs)); mProgramEndTimeText.setVisibility(View.VISIBLE); - mProgramEndTimeText.setText(getTimeString(mProgramEndTimeMs)); + setTextIfNeeded(mProgramEndTimeText, getTimeString(mProgramEndTimeMs)); } else { mProgramStartTimeText.setVisibility(View.GONE); mProgramEndTimeText.setVisibility(View.GONE); @@ -576,11 +544,17 @@ public class PlayControlsRowView extends MenuRowView { private void updateButtons() { if (isEnabled()) { - mControlBar.setVisibility(View.VISIBLE); - mUnavailableMessageText.setVisibility(View.GONE); + mPlayPauseButton.setVisibility(View.VISIBLE); + mJumpPreviousButton.setVisibility(View.VISIBLE); + mJumpNextButton.setVisibility(View.VISIBLE); + mRewindButton.setVisibility(View.VISIBLE); + mFastForwardButton.setVisibility(View.VISIBLE); } else { - mControlBar.setVisibility(View.INVISIBLE); - mUnavailableMessageText.setVisibility(View.VISIBLE); + mPlayPauseButton.setVisibility(View.GONE); + mJumpPreviousButton.setVisibility(View.GONE); + mJumpNextButton.setVisibility(View.GONE); + mRewindButton.setVisibility(View.GONE); + mFastForwardButton.setVisibility(View.GONE); return; } @@ -622,6 +596,12 @@ public class PlayControlsRowView extends MenuRowView { } private void updateRecordButton() { + if (isEnabled()) { + mRecordButton.setVisibility(VISIBLE); + } else { + mRecordButton.setVisibility(GONE); + return; + } if (!(mDvrManager != null && mDvrManager.isChannelRecordable(mMainActivity.getCurrentChannel()))) { mRecordButton.setVisibility(View.GONE); @@ -682,4 +662,10 @@ public class PlayControlsRowView extends MenuRowView { mDvrDataManager.removeScheduledRecordingListener(mScheduledRecordingListener); } } + + private void setTextIfNeeded(TextView textView, String text) { + if (!TextUtils.equals(textView.getText(), text)) { + textView.setText(text); + } + } } diff --git a/src/com/android/tv/menu/PlaybackProgressBar.java b/src/com/android/tv/menu/PlaybackProgressBar.java new file mode 100644 index 00000000..e8061bc6 --- /dev/null +++ b/src/com/android/tv/menu/PlaybackProgressBar.java @@ -0,0 +1,168 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.menu; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.LayerDrawable; +import android.util.AttributeSet; +import android.view.View; + +import com.android.tv.R; + +/** + * A progress bar control which has two progresses which start in the middle of the control. + */ +public class PlaybackProgressBar extends View { + private final LayerDrawable mProgressDrawable; + private final Drawable mPrimaryDrawable; + private final Drawable mSecondaryDrawable; + private long mMax = 100; + private long mProgressStart = 0; + private long mProgressEnd = 0; + private long mProgress = 0; + + public PlaybackProgressBar(Context context) { + this(context, null); + } + + public PlaybackProgressBar(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public PlaybackProgressBar(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public PlaybackProgressBar(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + TypedArray a = context.obtainStyledAttributes( + attrs, R.styleable.PlaybackProgressBar, defStyleAttr, defStyleRes); + mProgressDrawable = + (LayerDrawable) a.getDrawable(R.styleable.PlaybackProgressBar_progressDrawable); + mPrimaryDrawable = mProgressDrawable.findDrawableByLayerId(android.R.id.progress); + mSecondaryDrawable = + mProgressDrawable.findDrawableByLayerId(android.R.id.secondaryProgress); + a.recycle(); + refreshProgress(); + } + + @Override + protected void onDraw(Canvas canvas) { + final int saveCount = canvas.save(); + canvas.translate(getPaddingLeft(), getPaddingTop()); + mProgressDrawable.draw(canvas); + canvas.restoreToCount(saveCount); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + refreshProgress(); + } + + public void setMax(long max) { + if (max < 0) { + max = 0; + } + if (max != mMax) { + mMax = max; + if (mProgressStart > max) { + mProgressStart = max; + } + if (mProgressEnd > max) { + mProgressEnd = max; + } + if (mProgress > max) { + mProgress = max; + } + refreshProgress(); + } + } + + /** + * Sets the start and end position of the progress. + */ + public void setProgressRange(long start, long end) { + start = constrain(start, 0, mMax); + end = constrain(end, start, mMax); + mProgress = constrain(mProgress, start, end); + if (start != mProgressStart || end != mProgressEnd) { + mProgressStart = start; + mProgressEnd = end; + setProgressLevels(); + } + } + + /** + * Sets the progress position. + */ + public void setProgress(long progress) { + progress = constrain(progress, mProgressStart, mProgressEnd); + if (progress != mProgress) { + mProgress = progress; + setProgressLevels(); + } + } + + private long constrain(long value, long min, long max) { + return Math.min(Math.max(value, min), max); + } + + private void refreshProgress() { + int width = getWidth() - getPaddingStart() - getPaddingEnd(); + int height = getHeight() - getPaddingTop() - getPaddingBottom(); + mProgressDrawable.setBounds(0, 0, width, height); + setProgressLevels(); + } + + private void setProgressLevels() { + boolean progressUpdated = setProgressBound(mPrimaryDrawable, mProgressStart, mProgress); + progressUpdated |= setProgressBound(mSecondaryDrawable, mProgress, mProgressEnd); + if (progressUpdated) { + postInvalidate(); + } + } + + private boolean setProgressBound(Drawable drawable, long start, long end) { + Rect oldBounds = drawable.getBounds(); + if (mMax == 0) { + if (!isEqualRect(oldBounds, 0, 0, 0, 0)) { + drawable.setBounds(0, 0, 0, 0); + return true; + } + return false; + } + int width = mProgressDrawable.getBounds().width(); + int height = mProgressDrawable.getBounds().height(); + int left = (int) (width * start / mMax); + int right = (int) (width * end / mMax); + if (!isEqualRect(oldBounds, left, 0, right, height)) { + drawable.setBounds(left, 0, right, height); + return true; + } + return false; + } + + private boolean isEqualRect(Rect rect, int left, int top, int right, int bottom) { + return rect.left == left && rect.top == top && rect.right == right && rect.bottom == bottom; + } +} diff --git a/src/com/android/tv/menu/TvOptionsRowAdapter.java b/src/com/android/tv/menu/TvOptionsRowAdapter.java index fb062246..220fcd3a 100644 --- a/src/com/android/tv/menu/TvOptionsRowAdapter.java +++ b/src/com/android/tv/menu/TvOptionsRowAdapter.java @@ -21,7 +21,6 @@ import android.media.tv.TvTrackInfo; import android.support.annotation.VisibleForTesting; import com.android.tv.Features; -import com.android.tv.R; import com.android.tv.TvOptionsManager; import com.android.tv.customization.CustomAction; import com.android.tv.data.DisplayMode; @@ -30,7 +29,6 @@ import com.android.tv.ui.sidepanel.ClosedCaptionFragment; import com.android.tv.ui.sidepanel.DeveloperOptionFragment; import com.android.tv.ui.sidepanel.DisplayModeFragment; import com.android.tv.ui.sidepanel.MultiAudioFragment; -import com.android.tv.util.PipInputManager; import java.util.ArrayList; import java.util.List; @@ -39,12 +37,6 @@ import java.util.List; * An adapter of options. */ public class TvOptionsRowAdapter extends CustomizableOptionsRowAdapter { - private static final boolean ENABLE_IN_APP_PIP = false; - - private int mPositionPipAction; - // If mInAppPipAction is false, system-wide PIP is used. - private boolean mInAppPipAction = true; - public TvOptionsRowAdapter(Context context, List customActions) { super(context, customActions); } @@ -53,123 +45,62 @@ public class TvOptionsRowAdapter extends CustomizableOptionsRowAdapter { protected List createBaseActions() { List actionList = new ArrayList<>(); actionList.add(MenuAction.SELECT_CLOSED_CAPTION_ACTION); - setOptionChangedListener(MenuAction.SELECT_CLOSED_CAPTION_ACTION); actionList.add(MenuAction.SELECT_DISPLAY_MODE_ACTION); - setOptionChangedListener(MenuAction.SELECT_DISPLAY_MODE_ACTION); - actionList.add(MenuAction.PIP_IN_APP_ACTION); - setOptionChangedListener(MenuAction.PIP_IN_APP_ACTION); - mPositionPipAction = actionList.size() - 1; + if (Features.PICTURE_IN_PICTURE.isEnabled(getMainActivity())) { + actionList.add(MenuAction.SYSTEMWIDE_PIP_ACTION); + } actionList.add(MenuAction.SELECT_AUDIO_LANGUAGE_ACTION); - setOptionChangedListener(MenuAction.SELECT_AUDIO_LANGUAGE_ACTION); actionList.add(MenuAction.MORE_CHANNELS_ACTION); if (DeveloperOptionFragment.shouldShow()) { actionList.add(MenuAction.DEV_ACTION); } actionList.add(MenuAction.SETTINGS_ACTION); - if (getCustomActions() != null) { - // Adjust Pip action position which will be changed by applying custom actions. - for (CustomAction customAction : getCustomActions()) { - if (customAction.isFront()) { - mPositionPipAction++; - } - } - } - + updateClosedCaptionAction(); + updateMultiAudioAction(); + updateDisplayModeAction(); return actionList; } @Override - protected boolean updateActions() { - boolean changed = false; - if (updatePipAction()) { - changed = true; + protected void updateActions() { + if (updateClosedCaptionAction()) { + notifyItemChanged(getItemPosition(MenuAction.SELECT_CLOSED_CAPTION_ACTION)); } if (updateMultiAudioAction()) { - changed = true; + notifyItemChanged(getItemPosition(MenuAction.SELECT_AUDIO_LANGUAGE_ACTION)); } if (updateDisplayModeAction()) { - changed = true; + notifyItemChanged(getItemPosition(MenuAction.SELECT_DISPLAY_MODE_ACTION)); } - return changed; } - private boolean updatePipAction() { - // There are four states. - // Case 1. The device doesn't even have any input for PIP. (e.g. OTT box without HDMI input) - // => Remove the icon. - // Case 2. The device has one or more inputs for PIP but none of them are currently - // available. - // => Show the icon but disable it. - // Case 3. The device has one or more available PIP inputs and now it's tuned off. - // => Show the icon with "Off". - // Case 4. The device has one or more available PIP inputs but it's already turned on. - // => Show the icon with "On". - - boolean changed = false; - - // Case 1 - PipInputManager pipInputManager = getMainActivity().getPipInputManager(); - if (ENABLE_IN_APP_PIP && pipInputManager.getPipInputSize(false) > 1) { - if (!mInAppPipAction) { - removeAction(mPositionPipAction); - addAction(mPositionPipAction, MenuAction.PIP_IN_APP_ACTION); - mInAppPipAction = true; - changed = true; - } - } else { - if (mInAppPipAction) { - removeAction(mPositionPipAction); - mInAppPipAction = false; - if (Features.PICTURE_IN_PICTURE.isEnabled(getMainActivity())) { - addAction(mPositionPipAction, MenuAction.SYSTEMWIDE_PIP_ACTION); - } - return true; - } - return false; - } - - // Case 2 - boolean isPipEnabled = getMainActivity().isPipEnabled(); - boolean oldEnabled = MenuAction.PIP_IN_APP_ACTION.isEnabled(); - boolean newEnabled = pipInputManager.getPipInputSize(true) > 0; - if (oldEnabled != newEnabled) { - // Should not disable the item if the PIP is already turned on so that the user can - // force exit it. - if (newEnabled || !isPipEnabled) { - MenuAction.PIP_IN_APP_ACTION.setEnabled(newEnabled); - changed = true; - } - } - - // Case 3 & 4 - we just need to update the icon. - MenuAction.PIP_IN_APP_ACTION.setDrawableResId( - isPipEnabled ? R.drawable.ic_tvoption_pip : R.drawable.ic_tvoption_pip_off); - return changed; + @VisibleForTesting + private boolean updateClosedCaptionAction() { + return updateActionDescription(MenuAction.SELECT_CLOSED_CAPTION_ACTION); } @VisibleForTesting boolean updateMultiAudioAction() { List audioTracks = getMainActivity().getTracks(TvTrackInfo.TYPE_AUDIO); - boolean oldEnabled = MenuAction.SELECT_AUDIO_LANGUAGE_ACTION.isEnabled(); - boolean newEnabled = audioTracks != null && audioTracks.size() > 1; - if (oldEnabled != newEnabled) { - MenuAction.SELECT_AUDIO_LANGUAGE_ACTION.setEnabled(newEnabled); - return true; - } - return false; + boolean enabled = audioTracks != null && audioTracks.size() > 1; + // Use "|" operator for non-short-circuit evaluation. + return MenuAction.setEnabled(MenuAction.SELECT_AUDIO_LANGUAGE_ACTION, enabled) + | updateActionDescription(MenuAction.SELECT_AUDIO_LANGUAGE_ACTION); } private boolean updateDisplayModeAction() { TvViewUiManager uiManager = getMainActivity().getTvViewUiManager(); - boolean oldEnabled = MenuAction.SELECT_DISPLAY_MODE_ACTION.isEnabled(); - boolean newEnabled = uiManager.isDisplayModeAvailable(DisplayMode.MODE_FULL) + boolean enabled = uiManager.isDisplayModeAvailable(DisplayMode.MODE_FULL) || uiManager.isDisplayModeAvailable(DisplayMode.MODE_ZOOM); - if (oldEnabled != newEnabled) { - MenuAction.SELECT_DISPLAY_MODE_ACTION.setEnabled(newEnabled); - return true; - } - return false; + // Use "|" operator for non-short-circuit evaluation. + return MenuAction.setEnabled(MenuAction.SELECT_DISPLAY_MODE_ACTION, enabled) + | updateActionDescription(MenuAction.SELECT_DISPLAY_MODE_ACTION); + } + + private boolean updateActionDescription(MenuAction action) { + return MenuAction.setActionDescription(action, + getMainActivity().getTvOptionsManager().getOptionString(action.getType())); } @Override @@ -183,9 +114,6 @@ public class TvOptionsRowAdapter extends CustomizableOptionsRowAdapter { getMainActivity().getOverlayManager().getSideFragmentManager() .show(new DisplayModeFragment()); break; - case TvOptionsManager.OPTION_IN_APP_PIP: - getMainActivity().togglePipView(); - break; case TvOptionsManager.OPTION_SYSTEMWIDE_PIP: getMainActivity().enterPictureInPictureMode(); break; @@ -205,4 +133,4 @@ public class TvOptionsRowAdapter extends CustomizableOptionsRowAdapter { break; } } -} +} \ No newline at end of file diff --git a/src/com/android/tv/onboarding/SetupSourcesFragment.java b/src/com/android/tv/onboarding/SetupSourcesFragment.java index 7607822c..f56daec5 100644 --- a/src/com/android/tv/onboarding/SetupSourcesFragment.java +++ b/src/com/android/tv/onboarding/SetupSourcesFragment.java @@ -38,6 +38,7 @@ import com.android.tv.common.ui.setup.SetupGuidedStepFragment; import com.android.tv.common.ui.setup.SetupMultiPaneFragment; import com.android.tv.data.ChannelDataManager; import com.android.tv.data.TvInputNewComparator; +import com.android.tv.tuner.TunerInputController; import com.android.tv.ui.GuidedActionsStylistWithDivider; import com.android.tv.util.SetupUtils; import com.android.tv.util.TvInputManagerHelper; @@ -204,6 +205,7 @@ public class SetupSourcesFragment extends SetupMultiPaneFragment { mChannelDataManager.addListener(mChannelDataManagerListener); super.onCreate(savedInstanceState); mParentFragment = (SetupSourcesFragment) getParentFragment(); + TunerInputController.executeNetworkTunerDiscoveryAsyncTask(getContext()); } @Override diff --git a/src/com/android/tv/receiver/BootCompletedReceiver.java b/src/com/android/tv/receiver/BootCompletedReceiver.java index 8d6c5a14..03d7873f 100644 --- a/src/com/android/tv/receiver/BootCompletedReceiver.java +++ b/src/com/android/tv/receiver/BootCompletedReceiver.java @@ -27,7 +27,7 @@ import com.android.tv.Features; import com.android.tv.TvActivity; import com.android.tv.TvApplication; import com.android.tv.common.feature.CommonFeatures; -import com.android.tv.dvr.DvrRecordingService; +import com.android.tv.dvr.recorder.DvrRecordingService; import com.android.tv.recommendation.NotificationService; import com.android.tv.util.OnboardingUtils; import com.android.tv.util.SetupUtils; diff --git a/src/com/android/tv/receiver/GlobalKeyReceiver.java b/src/com/android/tv/receiver/GlobalKeyReceiver.java index 8cd4fdf1..2d9ee10e 100644 --- a/src/com/android/tv/receiver/GlobalKeyReceiver.java +++ b/src/com/android/tv/receiver/GlobalKeyReceiver.java @@ -20,6 +20,8 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.media.tv.TvContract; +import android.os.AsyncTask; +import android.provider.Settings; import android.util.Log; import android.view.KeyEvent; @@ -31,27 +33,57 @@ import com.android.tv.TvApplication; public class GlobalKeyReceiver extends BroadcastReceiver { private static final boolean DEBUG = false; private static final String TAG = "GlobalKeyReceiver"; + private static final String ACTION_GLOBAL_BUTTON = "android.intent.action.GLOBAL_BUTTON"; + // Settings.Secure.USER_SETUP_COMPLETE is hidden. + private static final String SETTINGS_USER_SETUP_COMPLETE = "user_setup_complete"; + + private static boolean sUserSetupComplete; @Override public void onReceive(Context context, Intent intent) { TvApplication.setCurrentRunningProcess(context, true); + Context appContext = context.getApplicationContext(); + if (DEBUG) Log.d(TAG, "onReceive: " + intent); + if (sUserSetupComplete) { + handleIntent(appContext, intent); + } else { + new AsyncTask() { + @Override + protected Boolean doInBackground(Void... params) { + return Settings.Secure.getInt(appContext.getContentResolver(), + SETTINGS_USER_SETUP_COMPLETE, 0) != 0; + } + + @Override + protected void onPostExecute(Boolean setupComplete) { + if (DEBUG) Log.d(TAG, "Is setup complete: " + setupComplete); + sUserSetupComplete = setupComplete; + if (sUserSetupComplete) { + handleIntent(appContext, intent); + } + } + }.execute(); + } + } + + private void handleIntent(Context appContext, Intent intent) { if (ACTION_GLOBAL_BUTTON.equals(intent.getAction())) { KeyEvent event = intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT); - if (DEBUG) Log.d(TAG, "onReceive: " + event); + if (DEBUG) Log.d(TAG, "handleIntent: " + event); int keyCode = event.getKeyCode(); int action = event.getAction(); if (action == KeyEvent.ACTION_UP) { switch (keyCode) { case KeyEvent.KEYCODE_GUIDE: - context.startActivity( + appContext.startActivity( new Intent(Intent.ACTION_VIEW, TvContract.Programs.CONTENT_URI)); break; case KeyEvent.KEYCODE_TV: - ((TvApplication) context.getApplicationContext()).handleTvKey(); + ((TvApplication) appContext).handleTvKey(); break; case KeyEvent.KEYCODE_TV_INPUT: - ((TvApplication) context.getApplicationContext()).handleTvInputKey(); + ((TvApplication) appContext).handleTvInputKey(); break; default: // Do nothing diff --git a/src/com/android/tv/receiver/PackageIntentsReceiver.java b/src/com/android/tv/receiver/PackageIntentsReceiver.java index 26d000e7..2d3f8705 100644 --- a/src/com/android/tv/receiver/PackageIntentsReceiver.java +++ b/src/com/android/tv/receiver/PackageIntentsReceiver.java @@ -19,8 +19,10 @@ package com.android.tv.receiver; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; +import android.net.Uri; import com.android.tv.TvApplication; +import com.android.tv.util.Partner; /** * A class for handling the broadcast intents from PackageManager. @@ -31,5 +33,9 @@ public class PackageIntentsReceiver extends BroadcastReceiver { public void onReceive(Context context, Intent intent) { TvApplication.setCurrentRunningProcess(context, true); ((TvApplication) context.getApplicationContext()).handleInputCountChanged(); + + Uri uri = intent.getData(); + final String packageName = (uri != null ? uri.getSchemeSpecificPart() : null); + Partner.reset(context, packageName); } } diff --git a/src/com/android/tv/recommendation/NotificationService.java b/src/com/android/tv/recommendation/NotificationService.java index 30ec73e3..a472f559 100644 --- a/src/com/android/tv/recommendation/NotificationService.java +++ b/src/com/android/tv/recommendation/NotificationService.java @@ -426,6 +426,7 @@ public class NotificationService extends Service implements Recommender.Listener : 100 - (int) (programLeftTimsMs * 100 / programDurationMs); Intent intent = new Intent(Intent.ACTION_VIEW, channel.getUri()); intent.putExtra(TUNE_PARAMS_RECOMMENDATION_TYPE, mRecommendationType); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); final PendingIntent notificationIntent = PendingIntent.getActivity(this, 0, intent, 0); // This callback will run on the main thread. diff --git a/src/com/android/tv/search/DataManagerSearch.java b/src/com/android/tv/search/DataManagerSearch.java index 5f89a21a..d90908f1 100644 --- a/src/com/android/tv/search/DataManagerSearch.java +++ b/src/com/android/tv/search/DataManagerSearch.java @@ -265,9 +265,7 @@ public class DataManagerSearch implements SearchInterface { } private String buildIntentData(long channelId) { - return TvContract.buildChannelUri(channelId).buildUpon() - .appendQueryParameter(Utils.PARAM_SOURCE, SOURCE_TV_SEARCH) - .build().toString(); + return TvContract.buildChannelUri(channelId).toString(); } private boolean isRatingBlocked(TvContentRating[] ratings) { diff --git a/src/com/android/tv/search/SearchInterface.java b/src/com/android/tv/search/SearchInterface.java index caa45812..c9a63128 100644 --- a/src/com/android/tv/search/SearchInterface.java +++ b/src/com/android/tv/search/SearchInterface.java @@ -24,8 +24,6 @@ import java.util.List; * Interface for channel and program search. */ public interface SearchInterface { - String SOURCE_TV_SEARCH = "TvSearch"; - int ACTION_TYPE_AMBIGUOUS = 1; int ACTION_TYPE_SWITCH_CHANNEL = 2; int ACTION_TYPE_SWITCH_INPUT = 3; diff --git a/src/com/android/tv/search/TvProviderSearch.java b/src/com/android/tv/search/TvProviderSearch.java index 2ceec19a..ea144786 100644 --- a/src/com/android/tv/search/TvProviderSearch.java +++ b/src/com/android/tv/search/TvProviderSearch.java @@ -38,6 +38,8 @@ import com.android.tv.search.LocalSearchProvider.SearchResult; import com.android.tv.util.PermissionUtils; import com.android.tv.util.Utils; +import junit.framework.Assert; + import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -189,6 +191,10 @@ public class TvProviderSearch implements SearchInterface { @WorkerThread private List searchChannels(String query, String[] columnForExactMatching, String[] columnForPartialMatching, Set channelsFound, int limit) { + Assert.assertTrue( + (columnForExactMatching != null && columnForExactMatching.length > 0) || + (columnForPartialMatching != null && columnForPartialMatching.length > 0)); + String[] projection = { Channels._ID, Channels.COLUMN_DISPLAY_NUMBER, @@ -308,6 +314,10 @@ public class TvProviderSearch implements SearchInterface { String[] columnForPartialMatching, Set channelsFound, int limit) { if (DEBUG) Log.d(TAG, "Searching programs: '" + query + "'"); long time = SystemClock.elapsedRealtime(); + Assert.assertTrue( + (columnForExactMatching != null && columnForExactMatching.length > 0) || + (columnForPartialMatching != null && columnForPartialMatching.length > 0)); + String[] projection = { Programs.COLUMN_CHANNEL_ID, Programs.COLUMN_TITLE, @@ -402,9 +412,7 @@ public class TvProviderSearch implements SearchInterface { } private String buildIntentData(long channelId) { - return TvContract.buildChannelUri(channelId).buildUpon() - .appendQueryParameter(Utils.PARAM_SOURCE, SOURCE_TV_SEARCH) - .build().toString(); + return TvContract.buildChannelUri(channelId).toString(); } private boolean isRatingBlocked(String ratings) { diff --git a/src/com/android/tv/tuner/TunerHal.java b/src/com/android/tv/tuner/TunerHal.java index de19766e..64394ea3 100644 --- a/src/com/android/tv/tuner/TunerHal.java +++ b/src/com/android/tv/tuner/TunerHal.java @@ -20,6 +20,9 @@ import android.content.Context; import android.support.annotation.IntDef; import android.support.annotation.StringDef; import android.util.Log; +import android.util.Pair; + +import com.android.tv.Features; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -48,6 +51,7 @@ public abstract class TunerHal implements AutoCloseable { public static final int TUNER_TYPE_BUILT_IN = 1; public static final int TUNER_TYPE_USB = 2; + public static final int TUNER_TYPE_NETWORK = 3; protected static final int PID_PAT = 0; protected static final int PID_ATSC_SI_BASE = 0x1ffb; @@ -69,31 +73,33 @@ public abstract class TunerHal implements AutoCloseable { */ public synchronized static TunerHal createInstance(Context context) { TunerHal tunerHal = null; - if (getTunerType(context) == TUNER_TYPE_BUILT_IN) { + if (useBuiltInTuner(context)) { } - if (tunerHal == null) { + if (tunerHal == null && UsbTunerHal.getNumberOfDevices(context) > 0) { tunerHal = new UsbTunerHal(context); } - if (tunerHal.openFirstAvailable()) { - return tunerHal; - } - return null; + return tunerHal != null && tunerHal.openFirstAvailable() ? tunerHal : null; } /** * Gets the number of tuner devices currently present. */ - public static int getTunerCount(Context context) { - if (getTunerType(context) == TUNER_TYPE_BUILT_IN) { + public static Pair getTunerTypeAndCount(Context context) { + if (useBuiltInTuner(context)) { } - return UsbTunerHal.getNumberOfDevices(context); + int usbTunerCount = UsbTunerHal.getNumberOfDevices(context); + if (usbTunerCount > 0) { + return new Pair<>(TUNER_TYPE_USB, usbTunerCount); + } + return new Pair<>(null, 0); } /** - * Gets the type of tuner devices currently used. + * Returns if tuner input service would use built-in tuners instead of USB tuners or network + * tuners. */ - public static int getTunerType(Context context) { - return TUNER_TYPE_USB; + static boolean useBuiltInTuner(Context context) { + return false; } protected TunerHal(Context context) { @@ -106,6 +112,14 @@ public abstract class TunerHal implements AutoCloseable { return mIsStreaming; } + /** + * Returns {@code true} if this tuner HAL can be reused to save tuning time between channels + * of the same frequency. + */ + public boolean isReusable() { + return true; + } + @Override protected void finalize() throws Throwable { super.finalize(); @@ -131,9 +145,12 @@ public abstract class TunerHal implements AutoCloseable { * * @param frequency a frequency of the channel to tune to * @param modulation a modulation method of the channel to tune to + * @param channelNumber channel number when channel number is already known. Some tuner HAL + * may use channelNumber instead of frequency for tune. * @return {@code true} if the operation was successful, {@code false} otherwise */ - public synchronized boolean tune(int frequency, @ModulationType String modulation) { + public synchronized boolean tune(int frequency, @ModulationType String modulation, + String channelNumber) { if (!isDeviceOpen()) { Log.e(TAG, "There's no available device"); return false; diff --git a/src/com/android/tv/tuner/TunerInputController.java b/src/com/android/tv/tuner/TunerInputController.java index d89b6a0c..65bbbdd0 100644 --- a/src/com/android/tv/tuner/TunerInputController.java +++ b/src/com/android/tv/tuner/TunerInputController.java @@ -16,30 +16,40 @@ package com.android.tv.tuner; +import android.app.AlarmManager; +import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.hardware.usb.UsbDevice; import android.hardware.usb.UsbManager; -import android.media.tv.TvInputInfo; -import android.media.tv.TvInputManager; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.os.AsyncTask; import android.os.Handler; import android.os.Looper; import android.os.Message; -import android.support.v4.os.BuildCompat; +import android.os.SystemClock; +import android.preference.PreferenceManager; +import android.text.TextUtils; import android.util.Log; import android.widget.Toast; import com.android.tv.Features; +import com.android.tv.R; import com.android.tv.TvApplication; -import com.android.tv.tuner.R; import com.android.tv.tuner.setup.TunerSetupActivity; import com.android.tv.tuner.tvinput.TunerTvInputService; +import com.android.tv.tuner.util.SystemPropertiesProxy; import com.android.tv.tuner.util.TunerInputInfoUtils; +import java.text.ParseException; +import java.text.SimpleDateFormat; import java.util.Map; +import java.util.concurrent.TimeUnit; /** * Controls the package visibility of {@link TunerTvInputService}. @@ -51,10 +61,39 @@ import java.util.Map; public class TunerInputController extends BroadcastReceiver { private static final boolean DEBUG = true; private static final String TAG = "TunerInputController"; + private static final String PREFERENCE_IS_NETWORK_TUNER_ATTACHED = "network_tuner"; + private static final String SECURITY_PATCH_LEVEL_KEY = "ro.build.version.security_patch"; + private static final String SECURITY_PATCH_LEVEL_FORMAT = "yyyy-MM-dd"; + + /** + * Action of {@link Intent} to check network connection repeatedly when it is necessary. + */ + public static final String CHECKING_NETWORK_CONNECTION = + "com.android.tv.action.CHECKING_NETWORK_CONNECTION"; + + /** + * Action of {@link Intent} when network tuner is attached. + */ + public static final String NETWORK_TUNER_ATTACHED = + "com.android.tv.action.NETWORK_TUNER_ATTACHED"; + + /** + * Action of {@link Intent} when network tuner is detached. + */ + public static final String NETWORK_TUNER_DETACHED = + "com.android.tv.action.NETWORK_TUNER_DETACHED"; + + private static final String EXTRA_CHECKING_DURATION = + "com.android.tv.action.extra.CHECKING_DURATION"; + + private static final long INITIAL_CHECKING_DURATION_MS = TimeUnit.SECONDS.toMillis(10); + private static final long MAXIMUM_CHECKING_DURATION_MS = TimeUnit.MINUTES.toMillis(10); private static final TunerDevice[] TUNER_DEVICES = { - new TunerDevice(0x2040, 0xb123), // WinTV-HVR-955Q - new TunerDevice(0x07ca, 0x0837) // AverTV Volar Hybrid Q + new TunerDevice(0x2040, 0xb123, null), // WinTV-HVR-955Q + new TunerDevice(0x07ca, 0x0837, null), // AverTV Volar Hybrid Q + // WinTV-dualHD (bulk) will be supported after 2017 April security patch. + new TunerDevice(0x2040, 0x826d, "2017-04-01"), // WinTV-dualHD (bulk) }; private static final int MSG_ENABLE_INPUT_SERVICE = 1000; @@ -70,7 +109,9 @@ public class TunerInputController extends BroadcastReceiver { if (mDvbDeviceAccessor == null) { mDvbDeviceAccessor = new DvbDeviceAccessor(context); } - enableTunerTvInputService(context, mDvbDeviceAccessor.isDvbDeviceAvailable()); + boolean enabled = mDvbDeviceAccessor.isDvbDeviceAvailable(); + enableTunerTvInputService( + context, enabled, false, enabled ? TunerHal.TUNER_TYPE_USB : null); break; } } @@ -84,14 +125,35 @@ public class TunerInputController extends BroadcastReceiver { private final int vendorId; private final int productId; - private TunerDevice(int vendorId, int productId) { + // security patch level from which the specific tuner type is supported. + private final String minSecurityLevel; + + private TunerDevice(int vendorId, int productId, String minSecurityLevel) { this.vendorId = vendorId; this.productId = productId; + this.minSecurityLevel = minSecurityLevel; } private boolean equals(UsbDevice device) { return device.getVendorId() == vendorId && device.getProductId() == productId; } + + private boolean isSupported(String currentSecurityLevel) { + if (minSecurityLevel == null) { + return true; + } + + long supportSecurityLevelTimeStamp = 0; + long currentSecurityLevelTimestamp = 0; + try { + SimpleDateFormat format = new SimpleDateFormat(SECURITY_PATCH_LEVEL_FORMAT); + supportSecurityLevelTimeStamp = format.parse(minSecurityLevel).getTime(); + currentSecurityLevelTimestamp = format.parse(currentSecurityLevel).getTime(); + } catch (ParseException e) { + } + return supportSecurityLevelTimeStamp != 0 + && supportSecurityLevelTimeStamp <= currentSecurityLevelTimestamp; + } } @Override @@ -99,17 +161,20 @@ public class TunerInputController extends BroadcastReceiver { if (DEBUG) Log.d(TAG, "Broadcast intent received:" + intent); TvApplication.setCurrentRunningProcess(context, true); if (!Features.TUNER.isEnabled(context)) { - enableTunerTvInputService(context, false); + enableTunerTvInputService(context, false, false, null); return; } + SharedPreferences sharedPreferences = + PreferenceManager.getDefaultSharedPreferences(context); switch (intent.getAction()) { case Intent.ACTION_BOOT_COMPLETED: + executeNetworkTunerDiscoveryAsyncTask(context, INITIAL_CHECKING_DURATION_MS); case TvApplication.ACTION_APPLICATION_FIRST_LAUNCHED: case UsbManager.ACTION_USB_DEVICE_ATTACHED: case UsbManager.ACTION_USB_DEVICE_DETACHED: - if (TunerInputInfoUtils.isBuiltInTuner(context)) { - enableTunerTvInputService(context, true); + if (TunerHal.useBuiltInTuner(context)) { + enableTunerTvInputService(context, true, false, TunerHal.TUNER_TYPE_BUILT_IN); break; } // Falls back to the below to check USB tuner devices. @@ -123,7 +188,41 @@ public class TunerInputController extends BroadcastReceiver { mHandler.obtainMessage(MSG_ENABLE_INPUT_SERVICE, context), DVB_DRIVER_CHECK_DELAY_MS); } else { - enableTunerTvInputService(context, false); + if (sharedPreferences.getBoolean(PREFERENCE_IS_NETWORK_TUNER_ATTACHED, false)) { + // Since network tuner is attached, do not disable TunerTvInput, + // just updates the TvInputInfo. + TunerInputInfoUtils.updateTunerInputInfo(context); + break; + } + enableTunerTvInputService(context, false, false, TextUtils + .equals(intent.getAction(), UsbManager.ACTION_USB_DEVICE_DETACHED) ? + TunerHal.TUNER_TYPE_USB : null); + } + break; + case CHECKING_NETWORK_CONNECTION: + long repeatedDurationMs = intent.getLongExtra(EXTRA_CHECKING_DURATION, + INITIAL_CHECKING_DURATION_MS); + executeNetworkTunerDiscoveryAsyncTask(context, + Math.min(repeatedDurationMs * 2, MAXIMUM_CHECKING_DURATION_MS)); + break; + case NETWORK_TUNER_ATTACHED: + // Network tuner detection is initiated by UI. So the app should not + // be killed. + sharedPreferences.edit() + .putBoolean(PREFERENCE_IS_NETWORK_TUNER_ATTACHED, true).apply(); + enableTunerTvInputService(context, true, true, TunerHal.TUNER_TYPE_NETWORK); + break; + case NETWORK_TUNER_DETACHED: + sharedPreferences.edit() + .putBoolean(PREFERENCE_IS_NETWORK_TUNER_ATTACHED, false).apply(); + if(!isUsbTunerConnected(context) && !TunerHal.useBuiltInTuner(context)) { + // Network tuner detection is initiated by UI. So the app should not + // be killed. + enableTunerTvInputService(context, false, true, TunerHal.TUNER_TYPE_NETWORK); + } else { + // Since USB tuner is attached, do not disable TunerTvInput, + // just updates the TvInputInfo. + TunerInputInfoUtils.updateTunerInputInfo(context); } break; } @@ -138,12 +237,15 @@ public class TunerInputController extends BroadcastReceiver { private boolean isUsbTunerConnected(Context context) { UsbManager manager = (UsbManager) context.getSystemService(Context.USB_SERVICE); Map deviceList = manager.getDeviceList(); + String currentSecurityLevel = + SystemPropertiesProxy.getString(SECURITY_PATCH_LEVEL_KEY, null); + for (UsbDevice device : deviceList.values()) { if (DEBUG) { Log.d(TAG, "Device: " + device); } for (TunerDevice tuner : TUNER_DEVICES) { - if (tuner.equals(device)) { + if (tuner.equals(device) && tuner.isSupported(currentSecurityLevel)) { Log.i(TAG, "Tuner found"); return true; } @@ -158,7 +260,8 @@ public class TunerInputController extends BroadcastReceiver { * @param context {@link Context} instance * @param enabled {@code true} to enable the service; otherwise {@code false} */ - private void enableTunerTvInputService(Context context, boolean enabled) { + private void enableTunerTvInputService(Context context, boolean enabled, + boolean forceDontKillApp, Integer tunerType) { if (DEBUG) Log.d(TAG, "enableTunerTvInputService: " + enabled); PackageManager pm = context.getPackageManager(); ComponentName componentName = new ComponentName(context, TunerTvInputService.class); @@ -170,7 +273,8 @@ public class TunerInputController extends BroadcastReceiver { // Since PackageManager.DONT_KILL_APP delays the operation by 10 seconds // (PackageManagerService.BROADCAST_DELAY), we'd better avoid using it. It is used only // when the LiveChannels app is active since we don't want to kill the running app. - int flags = TvApplication.getSingletons(context).getMainActivityWrapper().isCreated() + int flags = forceDontKillApp + || TvApplication.getSingletons(context).getMainActivityWrapper().isCreated() ? PackageManager.DONT_KILL_APP : 0; int newState = enabled ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED : PackageManager.COMPONENT_ENABLED_STATE_DISABLED; @@ -179,14 +283,67 @@ public class TunerInputController extends BroadcastReceiver { TunerSetupActivity.onTvInputEnabled(context, enabled); // Enable/disable the USB tuner TV input. pm.setComponentEnabledSetting(componentName, newState, flags); - if (!enabled) { - Toast.makeText( - context, R.string.msg_usb_device_detached, Toast.LENGTH_SHORT).show(); + if (!enabled && tunerType != null) { + if (tunerType == TunerHal.TUNER_TYPE_USB) { + Toast.makeText(context, R.string.msg_usb_tuner_disconnected, + Toast.LENGTH_SHORT).show(); + } else if (tunerType == TunerHal.TUNER_TYPE_NETWORK) { + Toast.makeText(context, R.string.msg_network_tuner_disconnected, + Toast.LENGTH_SHORT).show(); + } } if (DEBUG) Log.d(TAG, "Status updated:" + enabled); } else if (enabled) { - // When # of USB tuners is changed or the device just boots. + // When # of tuners is changed or the tuner input service is switching from/to using + // network tuners or the device just boots. TunerInputInfoUtils.updateTunerInputInfo(context); } } + + /** + * Discovers a network tuner. If the network connection is down, it won't repeatedly checking. + */ + public static void executeNetworkTunerDiscoveryAsyncTask(final Context context) { + executeNetworkTunerDiscoveryAsyncTask(context, 0); + } + + /** + * Discovers a network tuner. + * @param context {@link Context} + * @param repeatedDurationMs the time length to wait to repeatedly check network status to start + * finding network tuner when the network connection is not available. + * {@code 0} to disable repeatedly checking. + */ + private static void executeNetworkTunerDiscoveryAsyncTask(final Context context, + final long repeatedDurationMs) { + if (!Features.NETWORK_TUNER.isEnabled(context)) { + return; + } + new AsyncTask() { + @Override + protected Void doInBackground(Void... params) { + if (isNetworkConnected(context)) { + // Implement and execute network tuner discovery AsyncTask here. + } else if (repeatedDurationMs > 0) { + AlarmManager alarmManager = + (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + Intent networkCheckingIntent = new Intent(context, TunerInputController.class); + networkCheckingIntent.setAction(CHECKING_NETWORK_CONNECTION); + networkCheckingIntent.putExtra(EXTRA_CHECKING_DURATION, repeatedDurationMs); + PendingIntent alarmIntent = PendingIntent.getBroadcast( + context, 0, networkCheckingIntent, PendingIntent.FLAG_UPDATE_CURRENT); + alarmManager.set(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + + repeatedDurationMs, alarmIntent); + } + return null; + } + }.execute(); + } + + private static boolean isNetworkConnected(Context context) { + ConnectivityManager cm = (ConnectivityManager) + context.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo networkInfo = cm.getActiveNetworkInfo(); + return networkInfo != null && networkInfo.isConnected(); + } } diff --git a/src/com/android/tv/tuner/TunerPreferences.java b/src/com/android/tv/tuner/TunerPreferences.java index 1547e3ae..a387be74 100644 --- a/src/com/android/tv/tuner/TunerPreferences.java +++ b/src/com/android/tv/tuner/TunerPreferences.java @@ -39,6 +39,7 @@ public class TunerPreferences { private static final String PREFS_KEY_CHANNEL_DATA_VERSION = "channel_data_version"; private static final String PREFS_KEY_SCANNED_CHANNEL_COUNT = "scanned_channel_count"; + private static final String PREFS_KEY_LAST_POSTAL_CODE = "last_postal_code"; private static final String PREFS_KEY_SCAN_DONE = "scan_done"; private static final String PREFS_KEY_LAUNCH_SETUP = "launch_setup"; private static final String PREFS_KEY_STORE_TS_STREAM = "store_ts_stream"; @@ -86,8 +87,7 @@ public class TunerPreferences { /** * Releases the resources. */ - @MainThread - public static void release(Context context) { + public static synchronized void release(Context context) { if (useContentProvider(context) && sContentObserver != null) { context.getContentResolver().unregisterContentObserver(sContentObserver); } @@ -99,7 +99,8 @@ public class TunerPreferences { * This preferences is used across processes, so the preferences should be loaded again when the * databases changes. */ - public static synchronized void loadPreferences(Context context) { + @MainThread + public static void loadPreferences(Context context) { if (sLoadPreferencesTask != null && sLoadPreferencesTask.getStatus() != AsyncTask.Status.FINISHED) { sLoadPreferencesTask.cancel(true); @@ -113,8 +114,7 @@ public class TunerPreferences { return TisConfiguration.isPackagedWithLiveChannels(context); } - @MainThread - public static int getChannelDataVersion(Context context) { + public static synchronized int getChannelDataVersion(Context context) { SoftPreconditions.checkState(sInitialized); if (useContentProvider(context)) { return sPreferenceValues.getInt(PREFS_KEY_CHANNEL_DATA_VERSION, @@ -126,8 +126,7 @@ public class TunerPreferences { } } - @MainThread - public static void setChannelDataVersion(Context context, int version) { + public static synchronized void setChannelDataVersion(Context context, int version) { if (useContentProvider(context)) { setPreference(context, PREFS_KEY_CHANNEL_DATA_VERSION, version); } else { @@ -137,8 +136,7 @@ public class TunerPreferences { } } - @MainThread - public static int getScannedChannelCount(Context context) { + public static synchronized int getScannedChannelCount(Context context) { SoftPreconditions.checkState(sInitialized); if (useContentProvider(context)) { return sPreferenceValues.getInt(PREFS_KEY_SCANNED_CHANNEL_COUNT); @@ -148,8 +146,7 @@ public class TunerPreferences { } } - @MainThread - public static void setScannedChannelCount(Context context, int channelCount) { + public static synchronized void setScannedChannelCount(Context context, int channelCount) { if (useContentProvider(context)) { setPreference(context, PREFS_KEY_SCANNED_CHANNEL_COUNT, channelCount); } else { @@ -159,8 +156,25 @@ public class TunerPreferences { } } - @MainThread - public static boolean isScanDone(Context context) { + public static synchronized String getLastPostalCode(Context context) { + SoftPreconditions.checkState(sInitialized); + if (useContentProvider(context)) { + return sPreferenceValues.getString(PREFS_KEY_LAST_POSTAL_CODE); + } else { + return getSharedPreferences(context).getString(PREFS_KEY_LAST_POSTAL_CODE, null); + } + } + + public static synchronized void setLastPostalCode(Context context, String postalCode) { + if (useContentProvider(context)) { + setPreference(context, PREFS_KEY_LAST_POSTAL_CODE, postalCode); + } else { + getSharedPreferences(context).edit() + .putString(PREFS_KEY_LAST_POSTAL_CODE, postalCode).apply(); + } + } + + public static synchronized boolean isScanDone(Context context) { SoftPreconditions.checkState(sInitialized); if (useContentProvider(context)) { return sPreferenceValues.getBoolean(PREFS_KEY_SCAN_DONE); @@ -170,8 +184,7 @@ public class TunerPreferences { } } - @MainThread - public static void setScanDone(Context context) { + public static synchronized void setScanDone(Context context) { if (useContentProvider(context)) { setPreference(context, PREFS_KEY_SCAN_DONE, true); } else { @@ -181,8 +194,7 @@ public class TunerPreferences { } } - @MainThread - public static boolean shouldShowSetupActivity(Context context) { + public static synchronized boolean shouldShowSetupActivity(Context context) { SoftPreconditions.checkState(sInitialized); if (useContentProvider(context)) { return sPreferenceValues.getBoolean(PREFS_KEY_LAUNCH_SETUP); @@ -192,8 +204,7 @@ public class TunerPreferences { } } - @MainThread - public static void setShouldShowSetupActivity(Context context, boolean need) { + public static synchronized void setShouldShowSetupActivity(Context context, boolean need) { if (useContentProvider(context)) { setPreference(context, PREFS_KEY_LAUNCH_SETUP, need); } else { @@ -203,8 +214,7 @@ public class TunerPreferences { } } - @MainThread - public static boolean getStoreTsStream(Context context) { + public static synchronized boolean getStoreTsStream(Context context) { SoftPreconditions.checkState(sInitialized); if (useContentProvider(context)) { return sPreferenceValues.getBoolean(PREFS_KEY_STORE_TS_STREAM, false); @@ -214,8 +224,7 @@ public class TunerPreferences { } } - @MainThread - public static void setStoreTsStream(Context context, boolean shouldStore) { + public static synchronized void setStoreTsStream(Context context, boolean shouldStore) { if (useContentProvider(context)) { setPreference(context, PREFS_KEY_STORE_TS_STREAM, shouldStore); } else { @@ -229,8 +238,23 @@ public class TunerPreferences { return context.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE); } - @MainThread - private static void setPreference(final Context context, final String key, final String value) { + private static synchronized void setPreference(Context context, String key, String value) { + sPreferenceValues.putString(key, value); + savePreference(context, key, value); + } + + private static synchronized void setPreference(Context context, String key, int value) { + sPreferenceValues.putInt(key, value); + savePreference(context, key, Integer.toString(value)); + } + + private static synchronized void setPreference(Context context, String key, boolean value) { + sPreferenceValues.putBoolean(key, value); + savePreference(context, key, Boolean.toString(value)); + } + + private static void savePreference(final Context context, final String key, + final String value) { new AsyncTask() { @Override protected Void doInBackground(Void... params) { @@ -249,18 +273,6 @@ public class TunerPreferences { }.execute(); } - @MainThread - private static void setPreference(Context context, String key, int value) { - sPreferenceValues.putInt(key, value); - setPreference(context, key, Integer.toString(value)); - } - - @MainThread - private static void setPreference(Context context, String key, boolean value) { - sPreferenceValues.putBoolean(key, value); - setPreference(context, key, Boolean.toString(value)); - } - private static class LoadPreferencesTask extends AsyncTask { private final Context mContext; private LoadPreferencesTask(Context context) { @@ -292,6 +304,9 @@ public class TunerPreferences { case PREFS_KEY_STORE_TS_STREAM: bundle.putBoolean(key, Boolean.parseBoolean(value)); break; + case PREFS_KEY_LAST_POSTAL_CODE: + bundle.putString(key, value); + break; } } } @@ -303,8 +318,10 @@ public class TunerPreferences { } @Override - protected void onPostExecute(Bundle bundle) { - sPreferenceValues.putAll(bundle); + protected synchronized void onPostExecute(Bundle bundle) { + if (bundle != null) { + sPreferenceValues.putAll(bundle); + } } } -} +} \ No newline at end of file diff --git a/src/com/android/tv/tuner/UsbTunerHal.java b/src/com/android/tv/tuner/UsbTunerHal.java index 22e35ea1..b1608ede 100644 --- a/src/com/android/tv/tuner/UsbTunerHal.java +++ b/src/com/android/tv/tuner/UsbTunerHal.java @@ -169,6 +169,10 @@ public class UsbTunerHal extends TunerHal { * Gets the number of USB tuner devices currently present. */ public static int getNumberOfDevices(Context context) { - return (new DvbDeviceAccessor(context)).getNumOfDvbDevices(); + try { + return (new DvbDeviceAccessor(context)).getNumOfDvbDevices(); + } catch (Exception e) { + return 0; + } } } diff --git a/src/com/android/tv/tuner/cc/Cea708Parser.java b/src/com/android/tv/tuner/cc/Cea708Parser.java index 92ab0620..c43fe512 100644 --- a/src/com/android/tv/tuner/cc/Cea708Parser.java +++ b/src/com/android/tv/tuner/cc/Cea708Parser.java @@ -140,6 +140,7 @@ public class Cea708Parser { private int mCommand = 0; private int mListenServiceNumber = 0; private boolean mDtvCcPacking = false; + private boolean mFirstServiceNumberDiscovered; // Assign a dummy listener in order to avoid null checks. private OnCea708ParserListener mListener = new OnCea708ParserListener() { @@ -332,12 +333,14 @@ public class Cea708Parser { mDiscoveredNumBytes.put( serviceNumber, blockSize + mDiscoveredNumBytes.get(serviceNumber, 0)); } - if (mLastDiscoveryLaunchedMs + DISCOVERY_PERIOD_MS < SystemClock.elapsedRealtime()) { + if (mLastDiscoveryLaunchedMs + DISCOVERY_PERIOD_MS < SystemClock.elapsedRealtime() + || !mFirstServiceNumberDiscovered) { for (int i = 0; i < mDiscoveredNumBytes.size(); ++i) { int discoveredNumBytes = mDiscoveredNumBytes.valueAt(i); if (discoveredNumBytes >= DISCOVERY_NUM_BYTES_THRESHOLD) { int discoveredServiceNumber = mDiscoveredNumBytes.keyAt(i); mListener.discoverServiceNumber(discoveredServiceNumber); + mFirstServiceNumberDiscovered = true; } } mDiscoveredNumBytes.clear(); diff --git a/src/com/android/tv/tuner/data/PsipData.java b/src/com/android/tv/tuner/data/PsipData.java index e3cdb3a9..ac7fdedb 100644 --- a/src/com/android/tv/tuner/data/PsipData.java +++ b/src/com/android/tv/tuner/data/PsipData.java @@ -24,7 +24,7 @@ import com.android.tv.tuner.data.Track.AtscAudioTrack; import com.android.tv.tuner.data.Track.AtscCaptionTrack; import com.android.tv.tuner.ts.SectionParser; import com.android.tv.tuner.util.ConvertUtils; -import com.android.tv.tuner.util.StringUtils; +import com.android.tv.util.StringUtils; import java.util.ArrayList; import java.util.List; diff --git a/src/com/android/tv/tuner/data/TunerChannel.java b/src/com/android/tv/tuner/data/TunerChannel.java index 22cf2aa6..41f66e7d 100644 --- a/src/com/android/tv/tuner/data/TunerChannel.java +++ b/src/com/android/tv/tuner/data/TunerChannel.java @@ -19,12 +19,11 @@ package com.android.tv.tuner.data; import android.support.annotation.NonNull; import android.util.Log; -import com.android.tv.tuner.data.Channel; import com.android.tv.tuner.data.Channel.TunerChannelProto; import com.android.tv.tuner.data.Track.AtscAudioTrack; import com.android.tv.tuner.data.Track.AtscCaptionTrack; import com.android.tv.tuner.util.Ints; -import com.android.tv.tuner.util.StringUtils; +import com.android.tv.util.StringUtils; import com.google.protobuf.nano.MessageNano; import java.io.IOException; @@ -40,6 +39,11 @@ import java.util.Objects; public class TunerChannel implements Comparable, PsipData.TvTracksInterface { private static final String TAG = "TunerChannel"; + /** + * Channel number separator between major number and minor number. + */ + public static final char CHANNEL_NUMBER_SEPARATOR = '-'; + // See ATSC Code Points Registry. private static final String[] ATSC_SERVICE_TYPE_NAMES = new String[] { "ATSC Reserved", @@ -63,6 +67,7 @@ public class TunerChannel implements Comparable, PsipData.TvTracks // According to ISO13818-1, Mpeg2 StreamType has a range from 0x00 to 0xff. public static final int INVALID_STREAMTYPE = -1; + // @GuardedBy(this) Writing operations and toByteArray will be guarded. b/34197766 private final TunerChannelProto mProto; private TunerChannel(PsipData.VctItem channel, int programNumber, @@ -145,6 +150,44 @@ public class TunerChannel implements Comparable, PsipData.TvTracks return new TunerChannel(channel, 0, pmtItems, Channel.TYPE_FILE); } + /** + * Create a TunerChannel object suitable for network tuners + * @param major Channel number major + * @param minor Channel number minor + * @param programNumber Program number + * @param shortName Short name + * @param recordingProhibited Recording prohibition info + * @param videoFormat Video format. Should be {@code null} or one of the followings: + * {@link android.media.tv.TvContract.Channels#VIDEO_FORMAT_240P}, + * {@link android.media.tv.TvContract.Channels#VIDEO_FORMAT_360P}, + * {@link android.media.tv.TvContract.Channels#VIDEO_FORMAT_480I}, + * {@link android.media.tv.TvContract.Channels#VIDEO_FORMAT_480P}, + * {@link android.media.tv.TvContract.Channels#VIDEO_FORMAT_576I}, + * {@link android.media.tv.TvContract.Channels#VIDEO_FORMAT_576P}, + * {@link android.media.tv.TvContract.Channels#VIDEO_FORMAT_720P}, + * {@link android.media.tv.TvContract.Channels#VIDEO_FORMAT_1080I}, + * {@link android.media.tv.TvContract.Channels#VIDEO_FORMAT_1080P}, + * {@link android.media.tv.TvContract.Channels#VIDEO_FORMAT_2160P}, + * {@link android.media.tv.TvContract.Channels#VIDEO_FORMAT_4320P} + * @return a TunerChannel object + */ + public static TunerChannel forNetwork(int major, int minor, int programNumber, + String shortName, boolean recordingProhibited, String videoFormat) { + TunerChannel tunerChannel = new TunerChannel(programNumber, Collections.EMPTY_LIST); + tunerChannel.setVirtualMajor(major); + tunerChannel.setVirtualMinor(minor); + tunerChannel.setShortName(shortName); + // Set audio and video pids in order to work around the audio-only channel check. + tunerChannel.setAudioPids(new ArrayList<>(Arrays.asList(0))); + tunerChannel.selectAudioTrack(0); + tunerChannel.setVideoPid(0); + tunerChannel.setRecordingProhibited(recordingProhibited); + if (videoFormat != null) { + tunerChannel.setVideoFormat(videoFormat); + } + return tunerChannel; + } + public String getName() { return (!mProto.shortName.isEmpty()) ? mProto.shortName : mProto.longName; } @@ -193,7 +236,7 @@ public class TunerChannel implements Comparable, PsipData.TvTracks return mProto.videoPid; } - public void setVideoPid(int videoPid) { + synchronized public void setVideoPid(int videoPid) { mProto.videoPid = videoPid; } @@ -219,7 +262,7 @@ public class TunerChannel implements Comparable, PsipData.TvTracks return Ints.asList(mProto.audioPids); } - public void setAudioPids(List audioPids) { + synchronized public void setAudioPids(List audioPids) { mProto.audioPids = Ints.toArray(audioPids); } @@ -227,7 +270,7 @@ public class TunerChannel implements Comparable, PsipData.TvTracks return Ints.asList(mProto.audioStreamTypes); } - public void setAudioStreamTypes(List audioStreamTypes) { + synchronized public void setAudioStreamTypes(List audioStreamTypes) { mProto.audioStreamTypes = Ints.toArray(audioStreamTypes); } @@ -239,32 +282,32 @@ public class TunerChannel implements Comparable, PsipData.TvTracks return mProto.type; } - public void setFilepath(String filepath) { - mProto.filepath = filepath; + synchronized public void setFilepath(String filepath) { + mProto.filepath = filepath == null ? "" : filepath; } public String getFilepath() { return mProto.filepath; } - public void setVirtualMajor(int virtualMajor) { + synchronized public void setVirtualMajor(int virtualMajor) { mProto.virtualMajor = virtualMajor; } - public void setVirtualMinor(int virtualMinor) { + synchronized public void setVirtualMinor(int virtualMinor) { mProto.virtualMinor = virtualMinor; } - public void setShortName(String shortName) { - mProto.shortName = shortName; + synchronized public void setShortName(String shortName) { + mProto.shortName = shortName == null ? "" : shortName; } - public void setFrequency(int frequency) { + synchronized public void setFrequency(int frequency) { mProto.frequency = frequency; } - public void setModulation(String modulation) { - mProto.modulation = modulation; + synchronized public void setModulation(String modulation) { + mProto.modulation = modulation == null ? "" : modulation; } public boolean hasVideo() { @@ -279,13 +322,18 @@ public class TunerChannel implements Comparable, PsipData.TvTracks return mProto.channelId; } - public void setChannelId(long channelId) { + synchronized public void setChannelId(long channelId) { mProto.channelId = channelId; } public String getDisplayNumber() { - if (mProto.virtualMajor != 0 && mProto.virtualMinor != 0) { - return String.format("%d-%d", mProto.virtualMajor, mProto.virtualMinor); + return getDisplayNumber(true); + } + + public String getDisplayNumber(boolean ignoreZeroMinorNumber) { + if (mProto.virtualMajor != 0 && (mProto.virtualMinor != 0 || !ignoreZeroMinorNumber)) { + return String.format("%d%c%d", mProto.virtualMajor, CHANNEL_NUMBER_SEPARATOR, + mProto.virtualMinor); } else if (mProto.virtualMajor != 0) { return Integer.toString(mProto.virtualMajor); } else { @@ -298,7 +346,7 @@ public class TunerChannel implements Comparable, PsipData.TvTracks } @Override - public void setHasCaptionTrack() { + synchronized public void setHasCaptionTrack() { mProto.hasCaptionTrack = true; } @@ -312,7 +360,7 @@ public class TunerChannel implements Comparable, PsipData.TvTracks return Collections.unmodifiableList(Arrays.asList(mProto.audioTracks)); } - public void setAudioTracks(List audioTracks) { + synchronized public void setAudioTracks(List audioTracks) { mProto.audioTracks = audioTracks.toArray(new AtscAudioTrack[audioTracks.size()]); } @@ -321,11 +369,11 @@ public class TunerChannel implements Comparable, PsipData.TvTracks return Collections.unmodifiableList(Arrays.asList(mProto.captionTracks)); } - public void setCaptionTracks(List captionTracks) { + synchronized public void setCaptionTracks(List captionTracks) { mProto.captionTracks = captionTracks.toArray(new AtscCaptionTrack[captionTracks.size()]); } - public void selectAudioTrack(int index) { + synchronized public void selectAudioTrack(int index) { if (0 <= index && index < mProto.audioPids.length) { mProto.audioTrackIndex = index; } else { @@ -333,6 +381,22 @@ public class TunerChannel implements Comparable, PsipData.TvTracks } } + synchronized public void setRecordingProhibited(boolean recordingProhibited) { + mProto.recordingProhibited = recordingProhibited; + } + + public boolean isRecordingProhibited() { + return mProto.recordingProhibited; + } + + synchronized public void setVideoFormat(String videoFormat) { + mProto.videoFormat = videoFormat == null ? "" : videoFormat; + } + + public String getVideoFormat() { + return mProto.videoFormat; + } + @Override public String toString() { switch (mProto.type) { @@ -359,7 +423,10 @@ public class TunerChannel implements Comparable, PsipData.TvTracks if (ret != 0) { return ret; } - + ret = StringUtils.compare(getName(), channel.getName()); + if (ret != 0) { + return ret; + } // For FileTsStreamer, file paths should be compared. return StringUtils.compare(getFilepath(), channel.getFilepath()); } @@ -374,12 +441,19 @@ public class TunerChannel implements Comparable, PsipData.TvTracks @Override public int hashCode() { - return Objects.hash(getFrequency(), getProgramNumber(), getFilepath()); + return Objects.hash(getFrequency(), getProgramNumber(), getName(), getFilepath()); } // Serialization - public byte[] toByteArray() { - return MessageNano.toByteArray(mProto); + synchronized public byte[] toByteArray() { + try { + return MessageNano.toByteArray(mProto); + } catch (Exception e) { + // Retry toByteArray. b/34197766 + Log.w(TAG, "TunerChannel or its variables are modified in multiple thread without lock", + e); + return MessageNano.toByteArray(mProto); + } } public static TunerChannel parseFrom(byte[] data) { diff --git a/src/com/android/tv/tuner/exoplayer/ExoPlayerSampleExtractor.java b/src/com/android/tv/tuner/exoplayer/ExoPlayerSampleExtractor.java index c105e222..89641530 100644 --- a/src/com/android/tv/tuner/exoplayer/ExoPlayerSampleExtractor.java +++ b/src/com/android/tv/tuner/exoplayer/ExoPlayerSampleExtractor.java @@ -23,17 +23,29 @@ import android.os.HandlerThread; import android.os.Looper; import android.os.Message; import android.os.SystemClock; +import android.util.Pair; -import com.google.android.exoplayer.C; import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormatHolder; import com.google.android.exoplayer.SampleHolder; -import com.google.android.exoplayer.SampleSource; -import com.google.android.exoplayer.extractor.ExtractorSampleSource; -import com.google.android.exoplayer.extractor.ExtractorSampleSource.EventListener; -import com.google.android.exoplayer.upstream.Allocator; import com.google.android.exoplayer.upstream.DataSource; -import com.google.android.exoplayer.upstream.DefaultAllocator; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.FormatHolder; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; +import com.google.android.exoplayer2.source.ExtractorMediaSource; +import com.google.android.exoplayer2.source.ExtractorMediaSource.EventListener; +import com.google.android.exoplayer2.source.MediaPeriod; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.SampleStream; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.trackselection.FixedTrackSelection; +import com.google.android.exoplayer2.trackselection.TrackSelection; +import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.upstream.DefaultAllocator; +import com.android.tv.tuner.exoplayer.ac3.Ac3DefaultTrackRenderer; import com.android.tv.tuner.exoplayer.buffer.BufferManager; import com.android.tv.tuner.exoplayer.buffer.RecordingSampleBuffer; import com.android.tv.tuner.exoplayer.buffer.SimpleSampleBuffer; @@ -42,10 +54,11 @@ import com.android.tv.tuner.tvinput.PlaybackBufferListener; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; +import java.util.LinkedList; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicLong; /** * A class that extracts samples from a live broadcast stream while storing the sample on the disk. @@ -54,11 +67,7 @@ import java.util.concurrent.atomic.AtomicLong; public class ExoPlayerSampleExtractor implements SampleExtractor { private static final String TAG = "ExoPlayerSampleExtracto"; - // Buffer segment size for memory allocator. Copied from demo implementation of ExoPlayer. - private static final int BUFFER_SEGMENT_SIZE_IN_BYTES = 64 * 1024; - // Buffer segment count for sample source. Copied from demo implementation of ExoPlayer. - private static final int BUFFER_SEGMENT_COUNT = 256; - + private static final int INVALID_TRACK_INDEX = -1; private final HandlerThread mSourceReaderThread; private final long mId; @@ -70,36 +79,69 @@ public class ExoPlayerSampleExtractor implements SampleExtractor { private AtomicBoolean mOnCompletionCalled = new AtomicBoolean(); private IOException mExceptionOnPrepare; private List mTrackFormats; + private int mVideoTrackIndex = INVALID_TRACK_INDEX; + private boolean mVideoTrackMet; + private long mBaseSamplePts = Long.MIN_VALUE; private HashMap mLastExtractedPositionUsMap = new HashMap<>(); + private final List> mPendingSamples = new LinkedList<>(); private OnCompletionListener mOnCompletionListener; private Handler mOnCompletionListenerHandler; private IOException mError; - public ExoPlayerSampleExtractor(Uri uri, DataSource source, BufferManager bufferManager, + public ExoPlayerSampleExtractor(Uri uri, final DataSource source, BufferManager bufferManager, PlaybackBufferListener bufferListener, boolean isRecording) { // It'll be used as a timeshift file chunk name's prefix. mId = System.currentTimeMillis(); - Allocator allocator = new DefaultAllocator(BUFFER_SEGMENT_SIZE_IN_BYTES); EventListener eventListener = new EventListener() { - @Override - public void onLoadError(int sourceId, IOException e) { - mError = e; + public void onLoadError(IOException error) { + mError = error; } }; mSourceReaderThread = new HandlerThread("SourceReaderThread"); - mSourceReaderWorker = new SourceReaderWorker(new ExtractorSampleSource(uri, source, - allocator, BUFFER_SEGMENT_COUNT * BUFFER_SEGMENT_SIZE_IN_BYTES, + mSourceReaderWorker = new SourceReaderWorker(new ExtractorMediaSource(uri, + new com.google.android.exoplayer2.upstream.DataSource.Factory() { + @Override + public com.google.android.exoplayer2.upstream.DataSource createDataSource() { + // Returns an adapter implementation for ExoPlayer V2 DataSource interface. + return new com.google.android.exoplayer2.upstream.DataSource() { + @Override + public long open(DataSpec dataSpec) throws IOException { + return source.open( + new com.google.android.exoplayer.upstream.DataSpec( + dataSpec.uri, dataSpec.postBody, + dataSpec.absoluteStreamPosition, dataSpec.position, + dataSpec.length, dataSpec.key, dataSpec.flags)); + } + + @Override + public int read(byte[] buffer, int offset, int readLength) + throws IOException { + return source.read(buffer, offset, readLength); + } + + @Override + public Uri getUri() { + return null; + } + + @Override + public void close() throws IOException { + source.close(); + } + }; + } + }, + new DefaultExtractorsFactory(), // Do not create a handler if we not on a looper. e.g. test. - Looper.myLooper() != null ? new Handler() : null, - eventListener, 0)); + Looper.myLooper() != null ? new Handler() : null, eventListener)); if (isRecording) { mSampleBuffer = new RecordingSampleBuffer(bufferManager, bufferListener, false, RecordingSampleBuffer.BUFFER_REASON_RECORDING); } else { - if (bufferManager == null || bufferManager.isDisabled()) { + if (bufferManager == null) { mSampleBuffer = new SimpleSampleBuffer(bufferListener); } else { mSampleBuffer = new RecordingSampleBuffer(bufferManager, bufferListener, true, @@ -114,43 +156,141 @@ public class ExoPlayerSampleExtractor implements SampleExtractor { mOnCompletionListenerHandler = handler; } - private class SourceReaderWorker implements Handler.Callback { + private class SourceReaderWorker implements Handler.Callback, MediaPeriod.Callback { public static final int MSG_PREPARE = 1; public static final int MSG_FETCH_SAMPLES = 2; public static final int MSG_RELEASE = 3; private static final int RETRY_INTERVAL_MS = 50; - private final SampleSource mSampleSource; - private SampleSource.SampleSourceReader mSampleSourceReader; + private final MediaSource mSampleSource; + private MediaPeriod mMediaPeriod; + private SampleStream[] mStreams; private boolean[] mTrackMetEos; private boolean mMetEos = false; private long mCurrentPosition; + private DecoderInputBuffer mDecoderInputBuffer; + private SampleHolder mSampleHolder; + private boolean mPrepareRequested; - public SourceReaderWorker(SampleSource sampleSource) { + public SourceReaderWorker(MediaSource sampleSource) { mSampleSource = sampleSource; + mSampleSource.prepareSource(null, false, new MediaSource.Listener() { + @Override + public void onSourceInfoRefreshed(Timeline timeline, Object manifest) { + // Dynamic stream change is not supported yet. b/28169263 + // For now, this will cause EOS and playback reset. + } + }); + mDecoderInputBuffer = new DecoderInputBuffer( + DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL); + mSampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL); + } + + MediaFormat convertFormat(Format format) { + if (format.sampleMimeType.startsWith("audio/")) { + return MediaFormat.createAudioFormat(format.id, format.sampleMimeType, + format.bitrate, format.maxInputSize, + com.google.android.exoplayer.C.UNKNOWN_TIME_US, format.channelCount, + format.sampleRate, format.initializationData, format.language, + format.pcmEncoding); + } else if (format.sampleMimeType.startsWith("video/")) { + return MediaFormat.createVideoFormat( + format.id, format.sampleMimeType, format.bitrate, format.maxInputSize, + com.google.android.exoplayer.C.UNKNOWN_TIME_US, format.width, format.height, + format.initializationData, format.rotationDegrees, + format.pixelWidthHeightRatio, format.projectionData, format.stereoMode); + } else if (format.sampleMimeType.endsWith("/cea-608") + || format.sampleMimeType.startsWith("text/")) { + return MediaFormat.createTextFormat( + format.id, format.sampleMimeType, format.bitrate, + com.google.android.exoplayer.C.UNKNOWN_TIME_US, format.language); + } else { + return MediaFormat.createFormatForMimeType( + format.id, format.sampleMimeType, format.bitrate, + com.google.android.exoplayer.C.UNKNOWN_TIME_US); + } + } + + @Override + public void onPrepared(MediaPeriod mediaPeriod) { + if (mMediaPeriod == null) { + // This instance is already released while the extractor is preparing. + return; + } + TrackSelection.Factory selectionFactory = new FixedTrackSelection.Factory(); + TrackGroupArray trackGroupArray = mMediaPeriod.getTrackGroups(); + TrackSelection[] selections = new TrackSelection[trackGroupArray.length]; + for (int i = 0; i < selections.length; ++i) { + selections[i] = selectionFactory.createTrackSelection(trackGroupArray.get(i), 0); + } + boolean retain[] = new boolean[trackGroupArray.length]; + boolean reset[] = new boolean[trackGroupArray.length]; + mStreams = new SampleStream[trackGroupArray.length]; + mMediaPeriod.selectTracks(selections, retain, mStreams, reset, 0); + if (mTrackFormats == null) { + int trackCount = trackGroupArray.length; + mTrackMetEos = new boolean[trackCount]; + List trackFormats = new ArrayList<>(); + int videoTrackCount = 0; + for (int i = 0; i < trackCount; i++) { + Format format = trackGroupArray.get(i).getFormat(0); + if (format.sampleMimeType.startsWith("video/")) { + videoTrackCount++; + mVideoTrackIndex = i; + } + trackFormats.add(convertFormat(format)); + } + if (videoTrackCount > 1) { + // Disable dropping samples when there are multiple video tracks. + mVideoTrackIndex = INVALID_TRACK_INDEX; + } + mTrackFormats = trackFormats; + List ids = new ArrayList<>(); + for (int i = 0; i < mTrackFormats.size(); i++) { + ids.add(String.format(Locale.ENGLISH, "%s_%x", Long.toHexString(mId), i)); + } + try { + mSampleBuffer.init(ids, mTrackFormats); + } catch (IOException e) { + // In this case, we will not schedule any further operation. + // mExceptionOnPrepare will be notified to ExoPlayer, and ExoPlayer will + // call release() eventually. + mExceptionOnPrepare = e; + return; + } + mSourceReaderHandler.sendEmptyMessage(MSG_FETCH_SAMPLES); + mPrepared = true; + } + } + + @Override + public void onContinueLoadingRequested(MediaPeriod source) { + source.continueLoading(mCurrentPosition); } @Override public boolean handleMessage(Message message) { switch (message.what) { case MSG_PREPARE: - mPrepared = prepare(); - if (!mPrepared && mExceptionOnPrepare == null) { - mSourceReaderHandler - .sendEmptyMessageDelayed(MSG_PREPARE, RETRY_INTERVAL_MS); - } else{ - mSourceReaderHandler.sendEmptyMessage(MSG_FETCH_SAMPLES); + if (!mPrepareRequested) { + mPrepareRequested = true; + mMediaPeriod = mSampleSource.createPeriod(0, + new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE), 0); + mMediaPeriod.prepare(this); + try { + mMediaPeriod.maybeThrowPrepareError(); + } catch (IOException e) { + mError = e; + } } return true; case MSG_FETCH_SAMPLES: boolean didSomething = false; - SampleHolder sample = new SampleHolder( - SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL); ConditionVariable conditionVariable = new ConditionVariable(); - int trackCount = mSampleSourceReader.getTrackCount(); + int trackCount = mStreams.length; for (int i = 0; i < trackCount; ++i) { - if (!mTrackMetEos[i] && SampleSource.NOTHING_READ - != fetchSample(i, sample, conditionVariable)) { + if (!mTrackMetEos[i] && C.RESULT_NOTHING_READ + != fetchSample(i, mSampleHolder, conditionVariable)) { if (mMetEos) { // If mMetEos was on during fetchSample() due to an error, // fetching from other tracks is not necessary. @@ -159,6 +299,7 @@ public class ExoPlayerSampleExtractor implements SampleExtractor { didSomething = true; } } + mMediaPeriod.continueLoading(mCurrentPosition); if (!mMetEos) { if (didSomething) { mSourceReaderHandler.sendEmptyMessage(MSG_FETCH_SAMPLES); @@ -171,17 +312,10 @@ public class ExoPlayerSampleExtractor implements SampleExtractor { } return true; case MSG_RELEASE: - if (mSampleSourceReader != null) { - if (mPrepared) { - // ExtractorSampleSource expects all the tracks should be disabled - // before releasing. - int count = mSampleSourceReader.getTrackCount(); - for (int i = 0; i < count; ++i) { - mSampleSourceReader.disable(i); - } - } - mSampleSourceReader.release(); - mSampleSourceReader = null; + if (mMediaPeriod != null) { + mSampleSource.releasePeriod(mMediaPeriod); + mSampleSource.releaseSource(); + mMediaPeriod = null; } cleanUp(); mSourceReaderHandler.removeCallbacksAndMessages(null); @@ -190,91 +324,109 @@ public class ExoPlayerSampleExtractor implements SampleExtractor { return false; } - private boolean prepare() { - if (mSampleSourceReader == null) { - mSampleSourceReader = mSampleSource.register(); - } - if(!mSampleSourceReader.prepare(0)) { - return false; - } - if (mTrackFormats == null) { - int trackCount = mSampleSourceReader.getTrackCount(); - mTrackMetEos = new boolean[trackCount]; - List trackFormats = new ArrayList<>(); - for (int i = 0; i < trackCount; i++) { - trackFormats.add(mSampleSourceReader.getFormat(i)); - mSampleSourceReader.enable(i, 0); - - } - mTrackFormats = trackFormats; - List ids = new ArrayList<>(); - for (int i = 0; i < mTrackFormats.size(); i++) { - ids.add(String.format(Locale.ENGLISH, "%s_%x", Long.toHexString(mId), i)); - } - try { - mSampleBuffer.init(ids, mTrackFormats); - } catch (IOException e) { - // In this case, we will not schedule any further operation. - // mExceptionOnPrepare will be notified to ExoPlayer, and ExoPlayer will - // call release() eventually. - mExceptionOnPrepare = e; - return false; - } - } - return true; - } - private int fetchSample(int track, SampleHolder sample, ConditionVariable conditionVariable) { - mSampleSourceReader.continueBuffering(track, mCurrentPosition); - - MediaFormatHolder formatHolder = new MediaFormatHolder(); - sample.clearData(); - int ret = mSampleSourceReader.readData(track, mCurrentPosition, formatHolder, sample); - if (ret == SampleSource.SAMPLE_READ) { - if (mCurrentPosition < sample.timeUs) { - mCurrentPosition = sample.timeUs; + FormatHolder dummyFormatHolder = new FormatHolder(); + mDecoderInputBuffer.clear(); + int ret = mStreams[track].readData(dummyFormatHolder, mDecoderInputBuffer); + if (ret == C.RESULT_BUFFER_READ + // Double-check if the extractor provided the data to prevent NPE. b/33758354 + && mDecoderInputBuffer.data != null) { + if (mCurrentPosition < mDecoderInputBuffer.timeUs) { + mCurrentPosition = mDecoderInputBuffer.timeUs; } try { Long lastExtractedPositionUs = mLastExtractedPositionUsMap.get(track); if (lastExtractedPositionUs == null) { - mLastExtractedPositionUsMap.put(track, sample.timeUs); + mLastExtractedPositionUsMap.put(track, mDecoderInputBuffer.timeUs); } else { mLastExtractedPositionUsMap.put(track, - Math.max(lastExtractedPositionUs, sample.timeUs)); + Math.max(lastExtractedPositionUs, mDecoderInputBuffer.timeUs)); } - queueSample(track, sample, conditionVariable); + queueSample(track, conditionVariable); } catch (IOException e) { mLastExtractedPositionUsMap.clear(); mMetEos = true; mSampleBuffer.setEos(); } - } else if (ret == SampleSource.END_OF_STREAM) { + } else if (ret == C.RESULT_END_OF_INPUT) { mTrackMetEos[track] = true; for (int i = 0; i < mTrackMetEos.length; ++i) { if (!mTrackMetEos[i]) { break; } - if (i == mTrackMetEos.length -1) { + if (i == mTrackMetEos.length - 1) { mMetEos = true; mSampleBuffer.setEos(); } } } - // TODO: Handle SampleSource.FORMAT_READ for dynamic resolution change. b/28169263 + // TODO: Handle C.RESULT_FORMAT_READ for dynamic resolution change. b/28169263 return ret; } - } - - private void queueSample(int index, SampleHolder sample, ConditionVariable conditionVariable) - throws IOException { - long writeStartTimeNs = SystemClock.elapsedRealtimeNanos(); - mSampleBuffer.writeSample(index, sample, conditionVariable); - // Checks whether the storage has enough bandwidth for recording samples. - if (mSampleBuffer.isWriteSpeedSlow(sample.size, - SystemClock.elapsedRealtimeNanos() - writeStartTimeNs)) { - mSampleBuffer.handleWriteSpeedSlow(); + private void queueSample(int index, ConditionVariable conditionVariable) + throws IOException { + if (mVideoTrackIndex != INVALID_TRACK_INDEX) { + if (!mVideoTrackMet) { + if (index != mVideoTrackIndex) { + SampleHolder sample = + new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL); + mSampleHolder.flags = + (mDecoderInputBuffer.isKeyFrame() + ? com.google.android.exoplayer.C.SAMPLE_FLAG_SYNC : 0) + | (mDecoderInputBuffer.isDecodeOnly() + ? com.google.android.exoplayer.C.SAMPLE_FLAG_DECODE_ONLY + : 0); + sample.timeUs = mDecoderInputBuffer.timeUs; + sample.size = mDecoderInputBuffer.data.position(); + sample.ensureSpaceForWrite(sample.size); + mDecoderInputBuffer.flip(); + sample.data.position(0); + sample.data.put(mDecoderInputBuffer.data); + sample.data.flip(); + mPendingSamples.add(new Pair<>(index, sample)); + return; + } + mVideoTrackMet = true; + mBaseSamplePts = + mDecoderInputBuffer.timeUs + - Ac3DefaultTrackRenderer.INITIAL_AUDIO_BUFFERING_TIME_US; + for (Pair pair : mPendingSamples) { + if (pair.second.timeUs >= mBaseSamplePts) { + mSampleBuffer.writeSample(pair.first, pair.second, conditionVariable); + } + } + mPendingSamples.clear(); + } else { + if (mDecoderInputBuffer.timeUs < mBaseSamplePts + && mVideoTrackIndex != index) { + return; + } + } + } + // Copy the decoder input to the sample holder. + mSampleHolder.clearData(); + mSampleHolder.flags = + (mDecoderInputBuffer.isKeyFrame() + ? com.google.android.exoplayer.C.SAMPLE_FLAG_SYNC : 0) + | (mDecoderInputBuffer.isDecodeOnly() + ? com.google.android.exoplayer.C.SAMPLE_FLAG_DECODE_ONLY : 0); + mSampleHolder.timeUs = mDecoderInputBuffer.timeUs; + mSampleHolder.size = mDecoderInputBuffer.data.position(); + mSampleHolder.ensureSpaceForWrite(mSampleHolder.size); + mDecoderInputBuffer.flip(); + mSampleHolder.data.position(0); + mSampleHolder.data.put(mDecoderInputBuffer.data); + mSampleHolder.data.flip(); + long writeStartTimeNs = SystemClock.elapsedRealtimeNanos(); + mSampleBuffer.writeSample(index, mSampleHolder, conditionVariable); + + // Checks whether the storage has enough bandwidth for recording samples. + if (mSampleBuffer.isWriteSpeedSlow(mSampleHolder.size, + SystemClock.elapsedRealtimeNanos() - writeStartTimeNs)) { + mSampleBuffer.handleWriteSpeedSlow(); + } } } @@ -328,7 +480,7 @@ public class ExoPlayerSampleExtractor implements SampleExtractor { } @Override - public boolean continueBuffering(long positionUs) { + public boolean continueBuffering(long positionUs) { return mSampleBuffer.continueBuffering(positionUs); } @@ -386,12 +538,14 @@ public class ExoPlayerSampleExtractor implements SampleExtractor { } private long getLastExtractedPositionUs() { - long lastExtractedPositionUs = Long.MAX_VALUE; - for (long value : mLastExtractedPositionUsMap.values()) { - lastExtractedPositionUs = Math.min(lastExtractedPositionUs, value); + long lastExtractedPositionUs = Long.MIN_VALUE; + for (Map.Entry entry : mLastExtractedPositionUsMap.entrySet()) { + if (mVideoTrackIndex != entry.getKey()) { + lastExtractedPositionUs = Math.max(lastExtractedPositionUs, entry.getValue()); + } } - if (lastExtractedPositionUs == Long.MAX_VALUE) { - lastExtractedPositionUs = C.UNKNOWN_TIME_US; + if (lastExtractedPositionUs == Long.MIN_VALUE) { + lastExtractedPositionUs = com.google.android.exoplayer.C.UNKNOWN_TIME_US; } return lastExtractedPositionUs; } diff --git a/src/com/android/tv/tuner/exoplayer/FileSampleExtractor.java b/src/com/android/tv/tuner/exoplayer/FileSampleExtractor.java index ec7b4b16..b7e42a7c 100644 --- a/src/com/android/tv/tuner/exoplayer/FileSampleExtractor.java +++ b/src/com/android/tv/tuner/exoplayer/FileSampleExtractor.java @@ -25,7 +25,6 @@ import com.android.tv.tuner.exoplayer.buffer.RecordingSampleBuffer; import com.android.tv.tuner.tvinput.PlaybackBufferListener; import android.os.Handler; -import android.util.Pair; import java.io.IOException; import java.util.ArrayList; @@ -61,18 +60,17 @@ public class FileSampleExtractor implements SampleExtractor{ @Override public boolean prepare() throws IOException { - ArrayList> trackInfos = - mBufferManager.readTrackInfoFiles(); - if (trackInfos == null || trackInfos.isEmpty()) { + List trackFormatList = mBufferManager.readTrackInfoFiles(); + if (trackFormatList == null || trackFormatList.isEmpty()) { throw new IOException("Cannot find meta files for the recording."); } - mTrackCount = trackInfos.size(); + mTrackCount = trackFormatList.size(); List ids = new ArrayList<>(); mTrackFormats.clear(); for (int i = 0; i < mTrackCount; ++i) { - Pair pair = trackInfos.get(i); - ids.add(pair.first); - mTrackFormats.add(MediaFormatUtil.createMediaFormat(pair.second)); + BufferManager.TrackFormat trackFormat = trackFormatList.get(i); + ids.add(trackFormat.trackId); + mTrackFormats.add(MediaFormatUtil.createMediaFormat(trackFormat.format)); } mSampleBuffer = new RecordingSampleBuffer(mBufferManager, mBufferListener, true, RecordingSampleBuffer.BUFFER_REASON_RECORDED_PLAYBACK); diff --git a/src/com/android/tv/tuner/exoplayer/MpegTsPlayer.java b/src/com/android/tv/tuner/exoplayer/MpegTsPlayer.java index 381b22e9..ba0edf20 100644 --- a/src/com/android/tv/tuner/exoplayer/MpegTsPlayer.java +++ b/src/com/android/tv/tuner/exoplayer/MpegTsPlayer.java @@ -39,8 +39,8 @@ import com.android.tv.common.SoftPreconditions; import com.android.tv.tuner.data.Cea708Data; import com.android.tv.tuner.data.Cea708Data.CaptionEvent; import com.android.tv.tuner.data.TunerChannel; -import com.android.tv.tuner.exoplayer.ac3.Ac3PassthroughTrackRenderer; -import com.android.tv.tuner.exoplayer.ac3.Ac3TrackRenderer; +import com.android.tv.tuner.exoplayer.ac3.Ac3DefaultTrackRenderer; +import com.android.tv.tuner.exoplayer.ac3.Ac3MediaCodecTrackRenderer; import com.android.tv.tuner.source.TsDataSource; import com.android.tv.tuner.source.TsDataSourceManager; import com.android.tv.tuner.tvinput.EventDetector; @@ -48,11 +48,12 @@ import com.android.tv.tuner.tvinput.EventDetector; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -/** - * MPEG-2 TS stream player implementation using ExoPlayer. - */ -public class MpegTsPlayer implements ExoPlayer.Listener, MediaCodecVideoTrackRenderer.EventListener, - Ac3PassthroughTrackRenderer.EventListener, Ac3TrackRenderer.Ac3EventListener { +/** MPEG-2 TS stream player implementation using ExoPlayer. */ +public class MpegTsPlayer + implements ExoPlayer.Listener, + MediaCodecVideoTrackRenderer.EventListener, + Ac3DefaultTrackRenderer.EventListener, + Ac3MediaCodecTrackRenderer.Ac3EventListener { private int mCaptionServiceNumber = Cea708Data.EMPTY_SERVICE_NUMBER; /** @@ -304,8 +305,10 @@ public class MpegTsPlayer implements ExoPlayer.Listener, MediaCodecVideoTrackRen SoftPreconditions.checkState(supportSmoothTrickPlay(playbackParams.getSpeed())); mPlayer.setPlayWhenReady(true); mTrickplayRunning = true; - if (mAudioRenderer instanceof Ac3PassthroughTrackRenderer) { - mPlayer.sendMessage(mAudioRenderer, Ac3PassthroughTrackRenderer.MSG_SET_PLAYBACK_SPEED, + if (mAudioRenderer instanceof Ac3DefaultTrackRenderer) { + mPlayer.sendMessage( + mAudioRenderer, + Ac3DefaultTrackRenderer.MSG_SET_PLAYBACK_SPEED, playbackParams.getSpeed()); } else { mPlayer.sendMessage(mAudioRenderer, @@ -317,10 +320,9 @@ public class MpegTsPlayer implements ExoPlayer.Listener, MediaCodecVideoTrackRen private void stopSmoothTrickplay(boolean calledBySeek) { if (mTrickplayRunning) { mTrickplayRunning = false; - if (mAudioRenderer instanceof Ac3PassthroughTrackRenderer) { - mPlayer.sendMessage(mAudioRenderer, - Ac3PassthroughTrackRenderer.MSG_SET_PLAYBACK_SPEED, - 1.0f); + if (mAudioRenderer instanceof Ac3DefaultTrackRenderer) { + mPlayer.sendMessage( + mAudioRenderer, Ac3DefaultTrackRenderer.MSG_SET_PLAYBACK_SPEED, 1.0f); } else { mPlayer.sendMessage(mAudioRenderer, MediaCodecAudioTrackRenderer.MSG_SET_PLAYBACK_PARAMS, @@ -423,8 +425,8 @@ public class MpegTsPlayer implements ExoPlayer.Listener, MediaCodecVideoTrackRen */ public void setVolume(float volume) { mVolume = volume; - if (mAudioRenderer instanceof Ac3PassthroughTrackRenderer) { - mPlayer.sendMessage(mAudioRenderer, Ac3PassthroughTrackRenderer.MSG_SET_VOLUME, volume); + if (mAudioRenderer instanceof Ac3DefaultTrackRenderer) { + mPlayer.sendMessage(mAudioRenderer, Ac3DefaultTrackRenderer.MSG_SET_VOLUME, volume); } else { mPlayer.sendMessage(mAudioRenderer, MediaCodecAudioTrackRenderer.MSG_SET_VOLUME, volume); @@ -437,9 +439,9 @@ public class MpegTsPlayer implements ExoPlayer.Listener, MediaCodecVideoTrackRen * @param enable enables the audio when {@code true}, disables otherwise. */ public void setAudioTrack(boolean enable) { - if (mAudioRenderer instanceof Ac3PassthroughTrackRenderer) { - mPlayer.sendMessage(mAudioRenderer, Ac3PassthroughTrackRenderer.MSG_SET_AUDIO_TRACK, - enable ? 1 : 0); + if (mAudioRenderer instanceof Ac3DefaultTrackRenderer) { + mPlayer.sendMessage( + mAudioRenderer, Ac3DefaultTrackRenderer.MSG_SET_AUDIO_TRACK, enable ? 1 : 0); } else { mPlayer.sendMessage(mAudioRenderer, MediaCodecAudioTrackRenderer.MSG_SET_VOLUME, enable ? mVolume : 0.0f); @@ -494,6 +496,28 @@ public class MpegTsPlayer implements ExoPlayer.Listener, MediaCodecVideoTrackRen mPlayer.setSelectedTrack(rendererIndex, trackIndex); } + /** + * Returns the index of the currently selected track for the specified renderer. + * + * @param rendererIndex The index of the renderer. + * @return The selected track. A negative value or a value greater than or equal to the renderer's + * track count indicates that the renderer is disabled. + */ + public int getSelectedTrack(int rendererIndex) { + return mPlayer.getSelectedTrack(rendererIndex); + } + + /** + * Returns the format of a track. + * + * @param rendererIndex The index of the renderer. + * @param trackIndex The index of the track. + * @return The format of the track. + */ + public MediaFormat getTrackFormat(int rendererIndex, int trackIndex) { + return mPlayer.getTrackFormat(rendererIndex, trackIndex); + } + /** * Gets the main handler of the player. */ @@ -650,4 +674,4 @@ public class MpegTsPlayer implements ExoPlayer.Listener, MediaCodecVideoTrackRen } } } -} +} \ No newline at end of file diff --git a/src/com/android/tv/tuner/exoplayer/MpegTsRendererBuilder.java b/src/com/android/tv/tuner/exoplayer/MpegTsRendererBuilder.java index 0e46c9cf..a1a97d3d 100644 --- a/src/com/android/tv/tuner/exoplayer/MpegTsRendererBuilder.java +++ b/src/com/android/tv/tuner/exoplayer/MpegTsRendererBuilder.java @@ -21,9 +21,10 @@ import android.content.Context; import com.google.android.exoplayer.SampleSource; import com.google.android.exoplayer.TrackRenderer; import com.google.android.exoplayer.upstream.DataSource; +import com.android.tv.Features; import com.android.tv.tuner.exoplayer.MpegTsPlayer.RendererBuilder; import com.android.tv.tuner.exoplayer.MpegTsPlayer.RendererBuilderCallback; -import com.android.tv.tuner.exoplayer.ac3.Ac3PassthroughTrackRenderer; +import com.android.tv.tuner.exoplayer.ac3.Ac3DefaultTrackRenderer; import com.android.tv.tuner.exoplayer.buffer.BufferManager; import com.android.tv.tuner.tvinput.PlaybackBufferListener; @@ -52,10 +53,12 @@ public class MpegTsRendererBuilder implements RendererBuilder { SampleSource sampleSource = new MpegTsSampleSource(extractor); MpegTsVideoTrackRenderer videoRenderer = new MpegTsVideoTrackRenderer(mContext, sampleSource, mpegTsPlayer.getMainHandler(), mpegTsPlayer); - // TODO: Only using Ac3PassthroughTrackRenderer for A/V sync issue. We will use - // {@link Ac3TrackRenderer} when we use ExoPlayer's extractor. - TrackRenderer audioRenderer = new Ac3PassthroughTrackRenderer(sampleSource, - mpegTsPlayer.getMainHandler(), mpegTsPlayer); + // TODO: Only using Ac3DefaultTrackRenderer for A/V sync issue. We will use + // {@link Ac3MediaCodecTrackRenderer} when we use ExoPlayer's extractor. + TrackRenderer audioRenderer = + new Ac3DefaultTrackRenderer( + sampleSource, mpegTsPlayer.getMainHandler(), mpegTsPlayer, + !Features.AC3_SOFTWARE_DECODE.isEnabled(mContext)); Cea708TextTrackRenderer textRenderer = new Cea708TextTrackRenderer(sampleSource); TrackRenderer[] renderers = new TrackRenderer[MpegTsPlayer.RENDERER_COUNT]; diff --git a/src/com/android/tv/tuner/exoplayer/ac3/Ac3DefaultTrackRenderer.java b/src/com/android/tv/tuner/exoplayer/ac3/Ac3DefaultTrackRenderer.java new file mode 100644 index 00000000..d442fde8 --- /dev/null +++ b/src/com/android/tv/tuner/exoplayer/ac3/Ac3DefaultTrackRenderer.java @@ -0,0 +1,602 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.tuner.exoplayer.ac3; + +import android.os.Handler; +import android.os.SystemClock; +import android.util.Log; + +import com.google.android.exoplayer.CodecCounters; +import com.google.android.exoplayer.ExoPlaybackException; +import com.google.android.exoplayer.MediaClock; +import com.google.android.exoplayer.MediaFormat; +import com.google.android.exoplayer.MediaFormatHolder; +import com.google.android.exoplayer.SampleHolder; +import com.google.android.exoplayer.SampleSource; +import com.google.android.exoplayer.TrackRenderer; +import com.google.android.exoplayer.audio.AudioTrack; +import com.google.android.exoplayer.util.Assertions; +import com.google.android.exoplayer.util.MimeTypes; +import com.google.android.exoplayer2.decoder.SimpleOutputBuffer; +import com.android.tv.tuner.tvinput.TunerDebug; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; + +/** + * Decodes and renders AC3 audio. Supports passthrough playback and ffmpeg based software decoding. + */ +public class Ac3DefaultTrackRenderer extends TrackRenderer implements MediaClock { + public static final int MSG_SET_VOLUME = 10000; + public static final int MSG_SET_AUDIO_TRACK = MSG_SET_VOLUME + 1; + public static final int MSG_SET_PLAYBACK_SPEED = MSG_SET_VOLUME + 2; + + // ATSC/53 allows sample rate to be only 48Khz. + // One AC3 sample has 1536 frames, and its duration is 32ms. + public static final long AC3_SAMPLE_DURATION_US = 32000; + + // This is around 150ms, 150ms is big enough not to under-run AudioTrack, + // and 150ms is also small enough to fill the buffer rapidly. + static int BUFFERED_SAMPLES_IN_AUDIOTRACK = 5; + public static final long INITIAL_AUDIO_BUFFERING_TIME_US = + BUFFERED_SAMPLES_IN_AUDIOTRACK * AC3_SAMPLE_DURATION_US; + + + private static final String TAG = "Ac3DefaultTrackRenderer"; + private static final boolean DEBUG = false; + + /** + * Interface definition for a callback to be notified of + * {@link com.google.android.exoplayer.audio.AudioTrack} error. + */ + public interface EventListener { + void onAudioTrackInitializationError(AudioTrack.InitializationException e); + void onAudioTrackWriteError(AudioTrack.WriteException e); + } + + private static final int DEFAULT_INPUT_BUFFER_SIZE = 16384 * 2; + private static final int DEFAULT_OUTPUT_BUFFER_SIZE = 1024*1024; + private static final int MONITOR_DURATION_MS = 1000; + private static final int AC3_HEADER_BITRATE_OFFSET = 4; + + // Keep this as static in order to prevent new framework AudioTrack creation + // while old AudioTrack is being released. + private static final AudioTrackWrapper AUDIO_TRACK = new AudioTrackWrapper(); + private static final long KEEP_ALIVE_AFTER_EOS_DURATION_MS = 3000; + + // Ignore AudioTrack backward movement if duration of movement is below the threshold. + private static final long BACKWARD_AUDIO_TRACK_MOVE_THRESHOLD_US = 3000; + + // AudioTrack position cannot go ahead beyond this limit. + private static final long CURRENT_POSITION_FROM_PTS_LIMIT_US = 1000000; + + // Since MediaCodec processing and AudioTrack playing add delay, + // PTS interpolated time should be delayed reasonably when AudioTrack is not used. + private static final long ESTIMATED_TRACK_RENDERING_DELAY_US = 500000; + + private final CodecCounters mCodecCounters; + private final SampleSource.SampleSourceReader mSource; + private final SampleHolder mSampleHolder; + private final MediaFormatHolder mFormatHolder; + private final EventListener mEventListener; + private final Handler mEventHandler; + private final AudioTrackMonitor mMonitor; + private final AudioClock mAudioClock; + + private MediaFormat mFormat; + private boolean mFormatConfigured; + private int mSampleSize; + private final ByteBuffer mOutputBuffer; + private boolean mOutputReady; + private int mTrackIndex; + private boolean mSourceStateReady; + private boolean mInputStreamEnded; + private boolean mOutputStreamEnded; + private long mEndOfStreamMs; + private long mCurrentPositionUs; + private int mPresentationCount; + private long mPresentationTimeUs; + private long mInterpolatedTimeUs; + private long mPreviousPositionUs; + private boolean mIsStopped; + private boolean mEnabled = true; + private boolean mIsMuted; + private ArrayList mTracksIndex; + + public Ac3DefaultTrackRenderer( + SampleSource source, + Handler eventHandler, + EventListener listener, + boolean usePassthrough) { + mSource = source.register(); + mEventHandler = eventHandler; + mEventListener = listener; + mTrackIndex = -1; + mSampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_DIRECT); + mSampleHolder.ensureSpaceForWrite(DEFAULT_INPUT_BUFFER_SIZE); + mOutputBuffer = ByteBuffer.allocate(DEFAULT_OUTPUT_BUFFER_SIZE); + mFormatHolder = new MediaFormatHolder(); + AUDIO_TRACK.restart(); + mCodecCounters = new CodecCounters(); + mMonitor = new AudioTrackMonitor(); + mAudioClock = new AudioClock(); + mTracksIndex = new ArrayList<>(); + } + + @Override + protected MediaClock getMediaClock() { + return this; + } + + private static boolean handlesMimeType(String mimeType) { + return mimeType.equals(MimeTypes.AUDIO_AC3) || mimeType.equals(MimeTypes.AUDIO_E_AC3); + } + + @Override + protected boolean doPrepare(long positionUs) throws ExoPlaybackException { + boolean sourcePrepared = mSource.prepare(positionUs); + if (!sourcePrepared) { + return false; + } + for (int i = 0; i < mSource.getTrackCount(); i++) { + if (handlesMimeType(mSource.getFormat(i).mimeType)) { + if (mTrackIndex < 0) { + mTrackIndex = i; + } + mTracksIndex.add(i); + } + } + + // TODO: Check this case. Source does not have the proper mime type. + return true; + } + + @Override + protected int getTrackCount() { + return mTracksIndex.size(); + } + + @Override + protected MediaFormat getFormat(int track) { + Assertions.checkArgument(track >= 0 && track < mTracksIndex.size()); + return mSource.getFormat(mTracksIndex.get(track)); + } + + @Override + protected void onEnabled(int track, long positionUs, boolean joining) { + Assertions.checkArgument(track >= 0 && track < mTracksIndex.size()); + mTrackIndex = mTracksIndex.get(track); + mSource.enable(mTrackIndex, positionUs); + seekToInternal(positionUs); + } + + @Override + protected void onDisabled() { + AUDIO_TRACK.resetSessionId(); + clearDecodeState(); + mFormat = null; + mSource.disable(mTrackIndex); + } + + @Override + protected void onReleased() { + AUDIO_TRACK.release(); + mSource.release(); + } + + @Override + protected boolean isEnded() { + return mOutputStreamEnded && AUDIO_TRACK.isEnded(); + } + + @Override + protected boolean isReady() { + return AUDIO_TRACK.isReady() || (mFormat != null && (mSourceStateReady || mOutputReady)); + } + + private void seekToInternal(long positionUs) { + mMonitor.reset(MONITOR_DURATION_MS); + mSourceStateReady = false; + mInputStreamEnded = false; + mOutputStreamEnded = false; + mPresentationTimeUs = positionUs; + mPresentationCount = 0; + mPreviousPositionUs = 0; + mCurrentPositionUs = Long.MIN_VALUE; + mInterpolatedTimeUs = Long.MIN_VALUE; + mAudioClock.setPositionUs(positionUs); + } + + @Override + protected void seekTo(long positionUs) { + mSource.seekToUs(positionUs); + AUDIO_TRACK.reset(); + // resetSessionId() will create a new framework AudioTrack instead of reusing old one. + AUDIO_TRACK.resetSessionId(); + seekToInternal(positionUs); + } + + @Override + protected void onStarted() { + AUDIO_TRACK.play(); + mAudioClock.start(); + mIsStopped = false; + } + + @Override + protected void onStopped() { + AUDIO_TRACK.pause(); + mAudioClock.stop(); + mIsStopped = true; + } + + @Override + protected void maybeThrowError() throws ExoPlaybackException { + try { + mSource.maybeThrowError(); + } catch (IOException e) { + throw new ExoPlaybackException(e); + } + } + + @Override + protected void doSomeWork(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { + mMonitor.maybeLog(); + try { + if (mEndOfStreamMs != 0) { + // Ensure playback stops, after EoS was notified. + // Sometimes MediaCodecTrackRenderer does not fetch EoS timely + // after EoS was notified here long before. + long diff = SystemClock.elapsedRealtime() - mEndOfStreamMs; + if (diff >= KEEP_ALIVE_AFTER_EOS_DURATION_MS && !mIsStopped) { + throw new ExoPlaybackException("Much time has elapsed after EoS"); + } + } + boolean continueBuffering = mSource.continueBuffering(mTrackIndex, positionUs); + if (mSourceStateReady != continueBuffering) { + mSourceStateReady = continueBuffering; + if (DEBUG) { + Log.d(TAG, "mSourceStateReady: " + String.valueOf(mSourceStateReady)); + } + } + long discontinuity = mSource.readDiscontinuity(mTrackIndex); + if (discontinuity != SampleSource.NO_DISCONTINUITY) { + AUDIO_TRACK.handleDiscontinuity(); + mPresentationTimeUs = discontinuity; + mPresentationCount = 0; + clearDecodeState(); + return; + } + if (mFormat == null) { + readFormat(); + return; + } + + // Process only one sample at a time for doSomeWork() + if (processOutput()) { + if (!mOutputReady) { + while (feedInputBuffer()) { + if (mOutputReady) break; + } + } + } + mCodecCounters.ensureUpdated(); + } catch (IOException e) { + throw new ExoPlaybackException(e); + } + } + + private void ensureAudioTrackInitialized() { + if (!AUDIO_TRACK.isInitialized()) { + try { + if (DEBUG) { + Log.d(TAG, "AudioTrack initialized"); + } + AUDIO_TRACK.initialize(); + } catch (AudioTrack.InitializationException e) { + Log.e(TAG, "Error on AudioTrack initialization", e); + notifyAudioTrackInitializationError(e); + + // Do not throw exception here but just disabling audioTrack to keep playing + // video without audio. + AUDIO_TRACK.setStatus(false); + } + if (getState() == TrackRenderer.STATE_STARTED) { + if (DEBUG) { + Log.d(TAG, "AudioTrack played"); + } + AUDIO_TRACK.play(); + } + } + } + + private void clearDecodeState() { + mOutputReady = false; + AUDIO_TRACK.reset(); + } + + private void readFormat() throws IOException, ExoPlaybackException { + int result = mSource.readData(mTrackIndex, mCurrentPositionUs, + mFormatHolder, mSampleHolder); + if (result == SampleSource.FORMAT_READ) { + onInputFormatChanged(mFormatHolder); + } + } + + private MediaFormat convertMediaFormatToRaw(MediaFormat format) { + return MediaFormat.createAudioFormat( + format.trackId, + MimeTypes.AUDIO_RAW, + format.bitrate, + format.maxInputSize, + format.durationUs, + format.channelCount, + format.sampleRate, + format.initializationData, + format.language); + } + + private void onInputFormatChanged(MediaFormatHolder formatHolder) + throws ExoPlaybackException { + mFormat = formatHolder.format; + mFormatConfigured = true; + if (DEBUG) { + Log.d(TAG, "AudioTrack was configured to FORMAT: " + mFormat.toString()); + } + clearDecodeState(); + AUDIO_TRACK.reconfigure(mFormat.getFrameworkMediaFormatV16(), 0); + } + + private void onSampleSizeChanged(int sampleSize) { + if (DEBUG) { + Log.d(TAG, "Sample size was changed to : " + sampleSize); + } + clearDecodeState(); + int audioBufferSize = sampleSize * BUFFERED_SAMPLES_IN_AUDIOTRACK; + mSampleSize = sampleSize; + AUDIO_TRACK.reconfigure(mFormat.getFrameworkMediaFormatV16(), audioBufferSize); + } + + private boolean feedInputBuffer() throws IOException, ExoPlaybackException { + if (mInputStreamEnded) { + return false; + } + + mSampleHolder.data.clear(); + mSampleHolder.size = 0; + int result = mSource.readData(mTrackIndex, mPresentationTimeUs, mFormatHolder, + mSampleHolder); + switch (result) { + case SampleSource.NOTHING_READ: { + return false; + } + case SampleSource.FORMAT_READ: { + Log.i(TAG, "Format was read again"); + onInputFormatChanged(mFormatHolder); + return true; + } + case SampleSource.END_OF_STREAM: { + Log.i(TAG, "End of stream from SampleSource"); + mInputStreamEnded = true; + return false; + } + default: { + if (mSampleHolder.size != mSampleSize && mFormatConfigured) { + onSampleSizeChanged(mSampleHolder.size); + } + mSampleHolder.data.flip(); + decodeDone(mSampleHolder.data, mSampleHolder.timeUs); + return true; + } + } + } + + private boolean processOutput() throws ExoPlaybackException { + if (mOutputStreamEnded) { + return false; + } + if (!mOutputReady) { + if (mInputStreamEnded) { + mOutputStreamEnded = true; + mEndOfStreamMs = SystemClock.elapsedRealtime(); + return false; + } + return true; + } + + ensureAudioTrackInitialized(); + int handleBufferResult; + try { + // To reduce discontinuity, interpolate presentation time. + mInterpolatedTimeUs = mPresentationTimeUs + + mPresentationCount * AC3_SAMPLE_DURATION_US; + handleBufferResult = AUDIO_TRACK.handleBuffer(mOutputBuffer, + 0, mOutputBuffer.limit(), mInterpolatedTimeUs); + } catch (AudioTrack.WriteException e) { + notifyAudioTrackWriteError(e); + throw new ExoPlaybackException(e); + } + + if ((handleBufferResult & AudioTrack.RESULT_POSITION_DISCONTINUITY) != 0) { + Log.i(TAG, "Play discontinuity happened"); + mCurrentPositionUs = Long.MIN_VALUE; + } + if ((handleBufferResult & AudioTrack.RESULT_BUFFER_CONSUMED) != 0) { + mCodecCounters.renderedOutputBufferCount++; + mOutputReady = false; + return true; + } + return false; + } + + @Override + protected long getDurationUs() { + return mSource.getFormat(mTrackIndex).durationUs; + } + + @Override + protected long getBufferedPositionUs() { + long pos = mSource.getBufferedPositionUs(); + return pos == UNKNOWN_TIME_US || pos == END_OF_TRACK_US + ? pos : Math.max(pos, getPositionUs()); + } + + @Override + public long getPositionUs() { + if (!AUDIO_TRACK.isInitialized()) { + return mAudioClock.getPositionUs(); + } else if (!AUDIO_TRACK.isEnabled()) { + if (mInterpolatedTimeUs > 0) { + return mInterpolatedTimeUs - ESTIMATED_TRACK_RENDERING_DELAY_US; + } + return mPresentationTimeUs; + } + long audioTrackCurrentPositionUs = AUDIO_TRACK.getCurrentPositionUs(isEnded()); + if (audioTrackCurrentPositionUs == AudioTrack.CURRENT_POSITION_NOT_SET) { + mPreviousPositionUs = 0L; + if (DEBUG) { + long oldPositionUs = Math.max(mCurrentPositionUs, 0); + long currentPositionUs = Math.max(mPresentationTimeUs, mCurrentPositionUs); + Log.d(TAG, "Audio position is not set, diff in us: " + + String.valueOf(currentPositionUs - oldPositionUs)); + } + mCurrentPositionUs = Math.max(mPresentationTimeUs, mCurrentPositionUs); + } else { + if (mPreviousPositionUs + > audioTrackCurrentPositionUs + BACKWARD_AUDIO_TRACK_MOVE_THRESHOLD_US) { + Log.e(TAG, "audio_position BACK JUMP: " + + (mPreviousPositionUs - audioTrackCurrentPositionUs)); + mCurrentPositionUs = audioTrackCurrentPositionUs; + } else { + mCurrentPositionUs = Math.max(mCurrentPositionUs, audioTrackCurrentPositionUs); + } + mPreviousPositionUs = audioTrackCurrentPositionUs; + } + long upperBound = mPresentationTimeUs + CURRENT_POSITION_FROM_PTS_LIMIT_US; + if (mCurrentPositionUs > upperBound) { + mCurrentPositionUs = upperBound; + } + return mCurrentPositionUs; + } + + private void decodeDone(ByteBuffer outputBuffer, long presentationTimeUs) { + if (outputBuffer == null || mOutputBuffer == null) { + return; + } + if (presentationTimeUs < 0) { + Log.e(TAG, "decodeDone - invalid presentationTimeUs"); + return; + } + + if (TunerDebug.ENABLED) { + TunerDebug.setAudioPtsUs(presentationTimeUs); + } + + mOutputBuffer.clear(); + Assertions.checkState(mOutputBuffer.remaining() >= outputBuffer.limit()); + + mOutputBuffer.put(outputBuffer); + mMonitor.addPts(presentationTimeUs, mOutputBuffer.position(), + mOutputBuffer.get(AC3_HEADER_BITRATE_OFFSET)); + if (presentationTimeUs == mPresentationTimeUs) { + mPresentationCount++; + } else { + mPresentationCount = 0; + mPresentationTimeUs = presentationTimeUs; + } + mOutputBuffer.flip(); + mOutputReady = true; + } + + private void notifyAudioTrackInitializationError(final AudioTrack.InitializationException e) { + if (mEventHandler == null || mEventListener == null) { + return; + } + mEventHandler.post(new Runnable() { + @Override + public void run() { + mEventListener.onAudioTrackInitializationError(e); + } + }); + } + + private void notifyAudioTrackWriteError(final AudioTrack.WriteException e) { + if (mEventHandler == null || mEventListener == null) { + return; + } + mEventHandler.post(new Runnable() { + @Override + public void run() { + mEventListener.onAudioTrackWriteError(e); + } + }); + } + + @Override + public void handleMessage(int messageType, Object message) throws ExoPlaybackException { + switch (messageType) { + case MSG_SET_VOLUME: + float volume = (Float) message; + // Workaround: we cannot mute the audio track by setting the volume to 0, we need to + // disable the AUDIO_TRACK for this intent. However, enabling/disabling audio track + // whenever volume is being set might cause side effects, therefore we only handle + // "explicit mute operations", i.e., only after certain non-zero volume has been + // set, the subsequent volume setting operations will be consider as mute/un-mute + // operations and thus enable/disable the audio track. + if (mIsMuted && volume > 0) { + mIsMuted = false; + if (mEnabled) { + setStatus(true); + } + } else if (!mIsMuted && volume == 0) { + mIsMuted = true; + if (mEnabled) { + setStatus(false); + } + } + AUDIO_TRACK.setVolume(volume); + break; + case MSG_SET_AUDIO_TRACK: + mEnabled = (Integer) message == 1; + setStatus(mEnabled); + break; + case MSG_SET_PLAYBACK_SPEED: + mAudioClock.setPlaybackSpeed((Float) message); + break; + default: + super.handleMessage(messageType, message); + } + } + + private void setStatus(boolean enabled) { + if (enabled == AUDIO_TRACK.isEnabled()) { + return; + } + if (!enabled) { + // mAudioClock can be different from getPositionUs. In order to sync them, + // we set mAudioClock. + mAudioClock.setPositionUs(getPositionUs()); + } + AUDIO_TRACK.setStatus(enabled); + if (enabled) { + // When AUDIO_TRACK is enabled, we need to clear AUDIO_TRACK and seek to + // the current position. If not, AUDIO_TRACK has the obsolete data. + seekTo(mAudioClock.getPositionUs()); + } + } +} diff --git a/src/com/android/tv/tuner/exoplayer/ac3/Ac3MediaCodecTrackRenderer.java b/src/com/android/tv/tuner/exoplayer/ac3/Ac3MediaCodecTrackRenderer.java new file mode 100644 index 00000000..604959d1 --- /dev/null +++ b/src/com/android/tv/tuner/exoplayer/ac3/Ac3MediaCodecTrackRenderer.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.tuner.exoplayer.ac3; + +import android.os.Handler; + +import com.google.android.exoplayer.ExoPlaybackException; +import com.google.android.exoplayer.MediaCodecAudioTrackRenderer; +import com.google.android.exoplayer.MediaCodecSelector; +import com.google.android.exoplayer.SampleSource; + +/** + * MPEG-2 TS audio track renderer. + * + *

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

Since the audio output from {@link android.media.MediaExtractor} contains extra samples at - * the beginning, using original {@link MediaCodecAudioTrackRenderer} as audio renderer causes - * asynchronous Audio/Video outputs. - * This class calculates the offset of audio data and adjust the presentation times to avoid the - * asynchronous Audio/Video problem. - */ -public class Ac3TrackRenderer extends MediaCodecAudioTrackRenderer { - private final String TAG = "Ac3TrackRenderer"; - private final boolean DEBUG = false; - - private final Ac3EventListener mListener; - - public interface Ac3EventListener extends EventListener { - /** - * Invoked when a {@link android.media.PlaybackParams} set to an - * {@link android.media.AudioTrack} is not valid. - * - * @param e The corresponding exception. - */ - void onAudioTrackSetPlaybackParamsError(IllegalArgumentException e); - } - - public Ac3TrackRenderer(SampleSource source, MediaCodecSelector mediaCodecSelector, - Handler eventHandler, EventListener eventListener) { - super(source, mediaCodecSelector, eventHandler, eventListener); - mListener = (Ac3EventListener) eventListener; - } - - @Override - public void handleMessage(int messageType, Object message) throws ExoPlaybackException { - if (messageType == MSG_SET_PLAYBACK_PARAMS) { - try { - super.handleMessage(messageType, message); - } catch (IllegalArgumentException e) { - if (isAudioTrackSetPlaybackParamsError(e)) { - notifyAudioTrackSetPlaybackParamsError(e); - } - } - return; - } - super.handleMessage(messageType, message); - } - - private void notifyAudioTrackSetPlaybackParamsError(final IllegalArgumentException e) { - if (eventHandler != null && mListener != null) { - eventHandler.post(new Runnable() { - @Override - public void run() { - mListener.onAudioTrackSetPlaybackParamsError(e); - } - }); - } - } - - static private boolean isAudioTrackSetPlaybackParamsError(IllegalArgumentException e) { - if (e.getStackTrace() == null || e.getStackTrace().length < 1) { - return false; - } - for (StackTraceElement element : e.getStackTrace()) { - String elementString = element.toString(); - if (elementString.startsWith("android.media.AudioTrack.setPlaybackParams")) { - return true; - } - } - return false; - } -} \ No newline at end of file diff --git a/src/com/android/tv/tuner/exoplayer/ac3/AudioTrackMonitor.java b/src/com/android/tv/tuner/exoplayer/ac3/AudioTrackMonitor.java index bfdf08ac..6f152490 100644 --- a/src/com/android/tv/tuner/exoplayer/ac3/AudioTrackMonitor.java +++ b/src/com/android/tv/tuner/exoplayer/ac3/AudioTrackMonitor.java @@ -98,8 +98,8 @@ public class AudioTrackMonitor { long now = SystemClock.elapsedRealtime(); if (mExpireMs != 0 && now >= mExpireMs) { if (DEBUG) { - long sampleDuration = (mTotalCount - 1) * - Ac3PassthroughTrackRenderer.AC3_SAMPLE_DURATION_US / 1000; + long sampleDuration = + (mTotalCount - 1) * Ac3DefaultTrackRenderer.AC3_SAMPLE_DURATION_US / 1000; long totalDuration = now - mStartMs; StringBuilder ptsBuilder = new StringBuilder(); ptsBuilder.append("PTS received ").append(mSampleCount).append(", ") diff --git a/src/com/android/tv/tuner/exoplayer/ac3/AudioTrackWrapper.java b/src/com/android/tv/tuner/exoplayer/ac3/AudioTrackWrapper.java index bc3c5d00..393e12c3 100644 --- a/src/com/android/tv/tuner/exoplayer/ac3/AudioTrackWrapper.java +++ b/src/com/android/tv/tuner/exoplayer/ac3/AudioTrackWrapper.java @@ -18,6 +18,7 @@ package com.android.tv.tuner.exoplayer.ac3; import android.media.MediaFormat; +import com.google.android.exoplayer.C; import com.google.android.exoplayer.audio.AudioTrack; import java.nio.ByteBuffer; @@ -28,6 +29,10 @@ import java.nio.ByteBuffer; * This wrapper class will do nothing in disabled status for those operations. */ public class AudioTrackWrapper { + private static final int PCM16_FRAME_BYTES = 2; + private static final int AC3_FRAMES_IN_ONE_SAMPLE = 1536; + private static final int BUFFERED_SAMPLES_IN_AUDIOTRACK = + Ac3DefaultTrackRenderer.BUFFERED_SAMPLES_IN_AUDIOTRACK; private final AudioTrack mAudioTrack = new AudioTrack(); private int mAudioSessionID; private boolean mIsEnabled; @@ -106,7 +111,7 @@ public class AudioTrackWrapper { mAudioTrack.setVolume(volume); } - public void reconfigure(MediaFormat format) { + public void reconfigure(MediaFormat format, int audioBufferSize) { if (!mIsEnabled || format == null) { return; } @@ -117,9 +122,9 @@ public class AudioTrackWrapper { try { pcmEncoding = format.getInteger(MediaFormat.KEY_PCM_ENCODING); } catch (Exception e) { - pcmEncoding = com.google.android.exoplayer.MediaFormat.NO_VALUE; + pcmEncoding = C.ENCODING_PCM_16BIT; } - // TODO: Handle non-AC3 or non-passthrough audio. + // TODO: Handle non-AC3. if (MediaFormat.MIMETYPE_AUDIO_AC3.equalsIgnoreCase(mimeType) && channelCount != 2) { // Workarounds b/25955476. // Since all devices and platforms does not support passthrough for non-stereo AC3, @@ -127,7 +132,14 @@ public class AudioTrackWrapper { // In other words, the channel count should be always 2. channelCount = 2; } - mAudioTrack.configure(mimeType, channelCount, sampleRate, pcmEncoding); + if (MediaFormat.MIMETYPE_AUDIO_RAW.equalsIgnoreCase(mimeType)) { + audioBufferSize = + channelCount + * PCM16_FRAME_BYTES + * AC3_FRAMES_IN_ONE_SAMPLE + * BUFFERED_SAMPLES_IN_AUDIOTRACK; + } + mAudioTrack.configure(mimeType, channelCount, sampleRate, pcmEncoding, audioBufferSize); } public void handleDiscontinuity() { diff --git a/src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java b/src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java index eb596e93..112e9dc4 100644 --- a/src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java +++ b/src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java @@ -25,13 +25,14 @@ import android.util.Log; import android.util.Pair; import com.google.android.exoplayer.SampleHolder; +import com.android.tv.common.SoftPreconditions; import com.android.tv.tuner.exoplayer.SampleExtractor; import com.android.tv.util.Utils; import java.io.File; -import java.io.FileNotFoundException; import java.io.IOException; import java.util.ArrayList; +import java.util.ConcurrentModificationException; import java.util.LinkedList; import java.util.List; import java.util.Locale; @@ -59,7 +60,8 @@ public class BufferManager { private final SampleChunk.SampleChunkCreator mSampleChunkCreator; // Maps from track name to a map which maps from starting position to {@link SampleChunk}. - private final Map> mChunkMap = new ArrayMap<>(); + private final Map>> mChunkMap = + new ArrayMap<>(); private final Map mStartPositionMap = new ArrayMap<>(); private final Map mEvictListeners = new ArrayMap<>(); private final StorageManager mStorageManager; @@ -77,13 +79,11 @@ public class BufferManager { } }; - private volatile boolean mClosed = false; private int mMinSampleSizeForSpeedCheck = MINIMUM_SAMPLE_SIZE_FOR_SPEED_CHECK; private long mTotalWriteSize; private long mTotalWriteTimeNs; private float mWriteBandwidth = 0.0f; private volatile int mSpeedCheckCount; - private boolean mDisabled = false; public interface ChunkEvictedListener { void onChunkEvicted(String id, long createdTimeMs); @@ -173,6 +173,66 @@ public class BufferManager { void release() throws IOException; } + /** + * A Track format which will be loaded and saved from the permanent storage for recordings. + */ + public static class TrackFormat { + + /** + * The track id for the specified track. The track id will be used as a track identifier + * for recordings. + */ + public final String trackId; + + /** + * The {@link MediaFormat} for the specified track. + */ + public final MediaFormat format; + + /** + * Creates TrackFormat. + * @param trackId + * @param format + */ + public TrackFormat(String trackId, MediaFormat format) { + this.trackId = trackId; + this.format = format; + } + } + + /** + * A Holder for a sample position which will be loaded from the index file for recordings. + */ + public static class PositionHolder { + + /** + * The current sample position in microseconds. + * The position is identical to the PTS(presentation time stamp) of the sample. + */ + public final long positionUs; + + /** + * Base sample position for the current {@link SampleChunk}. + */ + public final long basePositionUs; + + /** + * The file offset for the current sample in the current {@link SampleChunk}. + */ + public final int offset; + + /** + * Creates a holder for a specific position in the recording. + * @param positionUs + * @param offset + */ + public PositionHolder(long positionUs, long basePositionUs, int offset) { + this.positionUs = positionUs; + this.basePositionUs = basePositionUs; + this.offset = offset; + } + } + /** * Storage configuration and policy manager for {@link BufferManager} */ @@ -185,11 +245,6 @@ public class BufferManager { */ File getBufferDir(); - /** - * Cleans up storage. - */ - void clearStorage(); - /** * Informs whether the storage is used for persistent use. (eg. dvr recording/play) * @@ -220,29 +275,27 @@ public class BufferManager { * Reads track name & {@link MediaFormat} from storage. * * @param isAudio {@code true} if it is for audio track - * @return {@link Pair} of track name & {@link MediaFormat} - * @throws IOException + * @return {@link List} of TrackFormat */ - Pair readTrackInfoFile(boolean isAudio) throws IOException; + List readTrackInfoFiles(boolean isAudio); /** - * Reads sample indexes for each written sample from storage. + * Reads key sample positions for each written sample from storage. * * @param trackId track name * @return indexes of the specified track * @throws IOException */ - ArrayList readIndexFile(String trackId) throws IOException; + ArrayList readIndexFile(String trackId) throws IOException; /** * Writes track information to storage. * - * @param trackId track name - * @param format {@link android.media.MediaFormat} of the track + * @param formatList {@list List} of TrackFormat * @param isAudio {@code true} if it is for audio track * @throws IOException */ - void writeTrackInfoFile(String trackId, MediaFormat format, boolean isAudio) + void writeTrackInfoFiles(List formatList, boolean isAudio) throws IOException; /** @@ -252,7 +305,7 @@ public class BufferManager { * @param index {@link SampleChunk} container * @throws IOException */ - void writeIndexFile(String trackName, SortedMap index) + void writeIndexFile(String trackName, SortedMap> index) throws IOException; } @@ -307,7 +360,6 @@ public class BufferManager { SampleChunk.SampleChunkCreator sampleChunkCreator) { mStorageManager = storageManager; mSampleChunkCreator = sampleChunkCreator; - clearBuffer(true); } public void registerChunkEvictedListener(String id, ChunkEvictedListener listener) { @@ -318,44 +370,44 @@ public class BufferManager { mEvictListeners.remove(id); } - private void clearBuffer(boolean deleteFiles) { - mChunkMap.clear(); - if (deleteFiles) { - mStorageManager.clearStorage(); - } - mBufferSize = 0; - } - private static String getFileName(String id, long positionUs) { return String.format(Locale.ENGLISH, "%s_%016x.chunk", id, positionUs); } /** - * Creates a new {@link SampleChunk} for caching samples. + * Creates a new {@link SampleChunk} for caching samples if it is needed. * * @param id the name of the track - * @param positionUs starting position of the {@link SampleChunk} in micro seconds. + * @param positionUs current position to write a sample in micro seconds. * @param samplePool {@link SamplePool} for the fast creation of samples. + * @param currentChunk the current {@link SampleChunk} to write, {@code null} when to create + * a new {@link SampleChunk}. + * @param currentOffset the current offset to write. * @return returns the created {@link SampleChunk}. * @throws IOException */ - public SampleChunk createNewWriteFile(String id, long positionUs, - SamplePool samplePool) throws IOException { + public SampleChunk createNewWriteFileIfNeeded(String id, long positionUs, SamplePool samplePool, + SampleChunk currentChunk, int currentOffset) throws IOException { if (!maybeEvictChunk()) { throw new IOException("Not enough storage space"); } - SortedMap map = mChunkMap.get(id); + SortedMap> map = mChunkMap.get(id); if (map == null) { map = new TreeMap<>(); mChunkMap.put(id, map); mStartPositionMap.put(id, positionUs); mPendingDelete.init(id); } - File file = new File(mStorageManager.getBufferDir(), getFileName(id, positionUs)); - SampleChunk sampleChunk = mSampleChunkCreator.createSampleChunk(samplePool, file, - positionUs, mChunkCallback); - map.put(positionUs, sampleChunk); - return sampleChunk; + if (currentChunk == null) { + File file = new File(mStorageManager.getBufferDir(), getFileName(id, positionUs)); + SampleChunk sampleChunk = mSampleChunkCreator + .createSampleChunk(samplePool, file, positionUs, mChunkCallback); + map.put(positionUs, new Pair(sampleChunk, 0)); + return sampleChunk; + } else { + map.put(positionUs, new Pair(currentChunk, currentOffset)); + return null; + } } /** @@ -366,10 +418,10 @@ public class BufferManager { * @throws IOException */ public void loadTrackFromStorage(String trackId, SamplePool samplePool) throws IOException { - ArrayList keyPositions = mStorageManager.readIndexFile(trackId); - long startPositionUs = keyPositions.size() > 0 ? keyPositions.get(0) : 0; + ArrayList keyPositions = mStorageManager.readIndexFile(trackId); + long startPositionUs = keyPositions.size() > 0 ? keyPositions.get(0).positionUs : 0; - SortedMap map = mChunkMap.get(trackId); + SortedMap> map = mChunkMap.get(trackId); if (map == null) { map = new TreeMap<>(); mChunkMap.put(trackId, map); @@ -377,11 +429,15 @@ public class BufferManager { mPendingDelete.init(trackId); } SampleChunk chunk = null; - for (long positionUs: keyPositions) { - chunk = mSampleChunkCreator.loadSampleChunkFromFile(samplePool, - mStorageManager.getBufferDir(), getFileName(trackId, positionUs), positionUs, - mChunkCallback, chunk); - map.put(positionUs, chunk); + long basePositionUs = -1; + for (PositionHolder position: keyPositions) { + if (position.basePositionUs != basePositionUs) { + chunk = mSampleChunkCreator.loadSampleChunkFromFile(samplePool, + mStorageManager.getBufferDir(), getFileName(trackId, position.positionUs), + position.positionUs, mChunkCallback, chunk); + basePositionUs = position.basePositionUs; + } + map.put(position.positionUs, new Pair(chunk, position.offset)); } } @@ -392,19 +448,19 @@ public class BufferManager { * @param positionUs the position. * @return returns the found {@link SampleChunk}. */ - public SampleChunk getReadFile(String id, long positionUs) { - SortedMap map = mChunkMap.get(id); + public Pair getReadFile(String id, long positionUs) { + SortedMap> map = mChunkMap.get(id); if (map == null) { return null; } - SampleChunk sampleChunk; - SortedMap headMap = map.headMap(positionUs + 1); + Pair ret; + SortedMap> headMap = map.headMap(positionUs + 1); if (!headMap.isEmpty()) { - sampleChunk = headMap.get(headMap.lastKey()); + ret = headMap.get(headMap.lastKey()); } else { - sampleChunk = map.get(map.firstKey()); + ret = map.get(map.firstKey()); } - return sampleChunk; + return ret; } /** @@ -439,15 +495,16 @@ public class BufferManager { // Since chunks are persistent, we cannot evict chunks. return false; } - SortedMap earliestChunkMap = null; + SortedMap> earliestChunkMap = null; SampleChunk earliestChunk = null; String earliestChunkId = null; - for (Map.Entry> entry : mChunkMap.entrySet()) { - SortedMap map = entry.getValue(); + for (Map.Entry>> entry : + mChunkMap.entrySet()) { + SortedMap> map = entry.getValue(); if (map.isEmpty()) { continue; } - SampleChunk chunk = map.get(map.firstKey()); + SampleChunk chunk = map.get(map.firstKey()).first; if (earliestChunk == null || chunk.getCreatedTimeMs() < earliestChunk.getCreatedTimeMs()) { earliestChunkMap = map; @@ -473,8 +530,9 @@ public class BufferManager { } pendingDelete = mPendingDelete.getSize(); } - for (Map.Entry> entry : mChunkMap.entrySet()) { - SortedMap map = entry.getValue(); + for (Map.Entry>> entry : + mChunkMap.entrySet()) { + SortedMap> map = entry.getValue(); if (map.isEmpty()) { continue; } @@ -489,70 +547,74 @@ public class BufferManager { * @return returns all track information which is found by {@link BufferManager.StorageManager}. * @throws IOException */ - public ArrayList> readTrackInfoFiles() throws IOException { - ArrayList> trackInfos = new ArrayList<>(); - try { - trackInfos.add(mStorageManager.readTrackInfoFile(false)); - } catch (FileNotFoundException e) { - // There can be a single track only recording. (eg. audio-only, video-only) - // So the exception should not stop the read. + public List readTrackInfoFiles() throws IOException { + List trackFormatList = new ArrayList<>(); + trackFormatList.addAll(mStorageManager.readTrackInfoFiles(false)); + trackFormatList.addAll(mStorageManager.readTrackInfoFiles(true)); + if (trackFormatList.isEmpty()) { + throw new IOException("No track information to load"); } - try { - trackInfos.add(mStorageManager.readTrackInfoFile(true)); - } catch (FileNotFoundException e) { - // See above catch block. - } - return trackInfos; + return trackFormatList; } /** * Writes track information and index information for all tracks. * - * @param audio audio information. - * @param video video information. + * @param audios list of audio track information + * @param videos list of audio track information * @throws IOException */ - public void writeMetaFiles(Pair audio, Pair video) + public void writeMetaFiles(List audios, List videos) throws IOException { - if (audio != null) { - mStorageManager.writeTrackInfoFile(audio.first, audio.second, true); - SortedMap map = mChunkMap.get(audio.first); - if (map == null) { - throw new IOException("Audio track index missing"); + if (audios.isEmpty() && videos.isEmpty()) { + throw new IOException("No track information to save"); + } + if (!audios.isEmpty()) { + mStorageManager.writeTrackInfoFiles(audios, true); + for (TrackFormat trackFormat : audios) { + SortedMap> map = + mChunkMap.get(trackFormat.trackId); + if (map == null) { + throw new IOException("Audio track index missing"); + } + mStorageManager.writeIndexFile(trackFormat.trackId, map); } - mStorageManager.writeIndexFile(audio.first, map); } - if (video != null) { - mStorageManager.writeTrackInfoFile(video.first, video.second, false); - SortedMap map = mChunkMap.get(video.first); - if (map == null) { - throw new IOException("Video track index missing"); + if (!videos.isEmpty()) { + mStorageManager.writeTrackInfoFiles(videos, false); + for (TrackFormat trackFormat : videos) { + SortedMap> map = + mChunkMap.get(trackFormat.trackId); + if (map == null) { + throw new IOException("Video track index missing"); + } + mStorageManager.writeIndexFile(trackFormat.trackId, map); } - mStorageManager.writeIndexFile(video.first, map); } } - /** - * Marks it is closed and it is not used anymore. - */ - public void close() { - // Clean-up may happen after this is called. - mClosed = true; - } - /** * Releases all the resources. */ public void release() { - mPendingDelete.release(); - for (Map.Entry> entry : mChunkMap.entrySet()) { - for (SampleChunk chunk : entry.getValue().values()) { - SampleChunk.IoState.release(chunk, !mStorageManager.isPersistent()); + try { + mPendingDelete.release(); + for (Map.Entry>> entry : + mChunkMap.entrySet()) { + SampleChunk toRelease = null; + for (Pair positions : entry.getValue().values()) { + if (toRelease != positions.first) { + toRelease = positions.first; + SampleChunk.IoState.release(toRelease, !mStorageManager.isPersistent()); + } + } } - } - mChunkMap.clear(); - if (mClosed) { - clearBuffer(!mStorageManager.isPersistent()); + mChunkMap.clear(); + } catch (ConcurrentModificationException | NullPointerException e) { + // TODO: remove this after it it confirmed that race condition issues are resolved. + // b/32492258, b/32373376 + SoftPreconditions.checkState(false, "Exception on BufferManager#release: ", + e.toString()); } } @@ -610,20 +672,6 @@ public class BufferManager { return ((float) mTotalWriteSize * 1000 / mTotalWriteTimeNs); } - /** - * Marks {@link BufferManager} object disabled to prevent it from the future use. - */ - public void disable() { - mDisabled = true; - } - - /** - * Returns if {@link BufferManager} object is disabled. - */ - public boolean isDisabled() { - return mDisabled; - } - /** * Returns if {@link BufferManager} has checked the write speed, * which is suitable for Trickplay. diff --git a/src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java b/src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java index 6a0502a7..bea3defd 100644 --- a/src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java +++ b/src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java @@ -17,8 +17,12 @@ package com.android.tv.tuner.exoplayer.buffer; import android.media.MediaFormat; +import android.util.Log; import android.util.Pair; +import com.android.tv.tuner.data.Track.AtscCaptionTrack; +import com.google.protobuf.nano.MessageNano; + import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.File; @@ -28,18 +32,25 @@ import java.io.IOException; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.List; +import java.util.Map; import java.util.SortedMap; /** * Manages DVR storage. */ public class DvrStorageManager implements BufferManager.StorageManager { + private static final String TAG = "DvrStorageManager"; // TODO: make serializable classes and use protobuf after internal data structure is finalized. private static final String KEY_PIXEL_WIDTH_HEIGHT_RATIO = "com.google.android.videos.pixelWidthHeightRatio"; + private static final String META_FILE_TYPE_AUDIO = "audio"; + private static final String META_FILE_TYPE_VIDEO = "video"; + private static final String META_FILE_TYPE_CAPTION = "caption"; private static final String META_FILE_SUFFIX = ".meta"; private static final String IDX_FILE_SUFFIX = ".idx"; + private static final String IDX_FILE_SUFFIX_V2 = IDX_FILE_SUFFIX + "2"; // Size of minimum reserved storage buffer which will be used to save meta files // and index files after actual recording finished. @@ -58,18 +69,6 @@ public class DvrStorageManager implements BufferManager.StorageManager { mIsRecording = isRecording; } - @Override - public void clearStorage() { - if (mIsRecording) { - File[] files = mBufferDir.listFiles(); - if (files != null && files.length > 0) { - for (File file : files) { - file.delete(); - } - } - } - } - @Override public File getBufferDir() { return mBufferDir; @@ -132,6 +131,17 @@ public class DvrStorageManager implements BufferManager.StorageManager { } } + private void readFormatStringOptional(DataInputStream in, MediaFormat format, String key) { + try { + String str = readString(in); + if (str != null) { + format.setString(key, str); + } + } catch (IOException e) { + // Since we are reading optional field, ignore the exception. + } + } + private ByteBuffer readByteBuffer(DataInputStream in) throws IOException { int len = in.readInt(); if (len <= 0) { @@ -155,39 +165,104 @@ public class DvrStorageManager implements BufferManager.StorageManager { } @Override - public Pair readTrackInfoFile(boolean isAudio) throws IOException { - File file = new File(getBufferDir(), (isAudio ? "audio" : "video") + META_FILE_SUFFIX); - try (DataInputStream in = new DataInputStream(new FileInputStream(file))) { - String name = readString(in); - MediaFormat format = new MediaFormat(); - readFormatString(in, format, MediaFormat.KEY_MIME); - readFormatInt(in, format, MediaFormat.KEY_MAX_INPUT_SIZE); - readFormatInt(in, format, MediaFormat.KEY_WIDTH); - readFormatInt(in, format, MediaFormat.KEY_HEIGHT); - readFormatInt(in, format, MediaFormat.KEY_CHANNEL_COUNT); - readFormatInt(in, format, MediaFormat.KEY_SAMPLE_RATE); - readFormatFloat(in, format, KEY_PIXEL_WIDTH_HEIGHT_RATIO); - for (int i = 0; i < 3; ++i) { - readFormatByteBuffer(in, format, "csd-" + i); + public List readTrackInfoFiles(boolean isAudio) { + List trackFormatList = new ArrayList<>(); + int index = 0; + boolean trackNotFound = false; + do { + String fileName = (isAudio ? META_FILE_TYPE_AUDIO : META_FILE_TYPE_VIDEO) + + ((index == 0) ? META_FILE_SUFFIX : (index + META_FILE_SUFFIX)); + File file = new File(getBufferDir(), fileName); + try (DataInputStream in = new DataInputStream(new FileInputStream(file))) { + String name = readString(in); + MediaFormat format = new MediaFormat(); + readFormatString(in, format, MediaFormat.KEY_MIME); + readFormatInt(in, format, MediaFormat.KEY_MAX_INPUT_SIZE); + readFormatInt(in, format, MediaFormat.KEY_WIDTH); + readFormatInt(in, format, MediaFormat.KEY_HEIGHT); + readFormatInt(in, format, MediaFormat.KEY_CHANNEL_COUNT); + readFormatInt(in, format, MediaFormat.KEY_SAMPLE_RATE); + readFormatFloat(in, format, KEY_PIXEL_WIDTH_HEIGHT_RATIO); + for (int i = 0; i < 3; ++i) { + readFormatByteBuffer(in, format, "csd-" + i); + } + readFormatLong(in, format, MediaFormat.KEY_DURATION); + + // This is optional since language field is added later. + readFormatStringOptional(in, format, MediaFormat.KEY_LANGUAGE); + trackFormatList.add(new BufferManager.TrackFormat(name, format)); + } catch (IOException e) { + trackNotFound = true; } - readFormatLong(in, format, MediaFormat.KEY_DURATION); - return new Pair<>(name, format); + index++; + } while(!trackNotFound); + return trackFormatList; + } + + /** + * Reads caption information from files. + * + * @return a list of {@link AtscCaptionTrack} objects which store caption information. + */ + public List readCaptionInfoFiles() { + List tracks = new ArrayList<>(); + int index = 0; + boolean trackNotFound = false; + do { + String fileName = META_FILE_TYPE_CAPTION + + ((index == 0) ? META_FILE_SUFFIX : (index + META_FILE_SUFFIX)); + File file = new File(getBufferDir(), fileName); + try (DataInputStream in = new DataInputStream(new FileInputStream(file))) { + byte[] data = new byte[(int) file.length()]; + in.read(data); + tracks.add(AtscCaptionTrack.parseFrom(data)); + } catch (IOException e) { + trackNotFound = true; + } + index++; + } while(!trackNotFound); + return tracks; + } + + private ArrayList readOldIndexFile(File indexFile) + throws IOException { + ArrayList indices = new ArrayList<>(); + try (DataInputStream in = new DataInputStream(new FileInputStream(indexFile))) { + long count = in.readLong(); + for (long i = 0; i < count; ++i) { + long positionUs = in.readLong(); + indices.add(new BufferManager.PositionHolder(positionUs, positionUs, 0)); + } + return indices; } } - @Override - public ArrayList readIndexFile(String trackId) throws IOException { - ArrayList indices = new ArrayList<>(); - File file = new File(getBufferDir(), trackId + IDX_FILE_SUFFIX); - try (DataInputStream in = new DataInputStream(new FileInputStream(file))) { + private ArrayList readNewIndexFile(File indexFile) + throws IOException { + ArrayList indices = new ArrayList<>(); + try (DataInputStream in = new DataInputStream(new FileInputStream(indexFile))) { long count = in.readLong(); for (long i = 0; i < count; ++i) { - indices.add(in.readLong()); + long positionUs = in.readLong(); + long basePositionUs = in.readLong(); + int offset = in.readInt(); + indices.add(new BufferManager.PositionHolder(positionUs, basePositionUs, offset)); } return indices; } } + @Override + public ArrayList readIndexFile(String trackId) + throws IOException { + File file = new File(getBufferDir(), trackId + IDX_FILE_SUFFIX_V2); + if (file.exists()) { + return readNewIndexFile(file); + } else { + return readOldIndexFile(new File(getBufferDir(),trackId + IDX_FILE_SUFFIX)); + } + } + private void writeFormatInt(DataOutputStream out, MediaFormat format, String key) throws IOException { if (format.containsKey(key)) { @@ -254,33 +329,63 @@ public class DvrStorageManager implements BufferManager.StorageManager { } @Override - public void writeTrackInfoFile(String trackId, MediaFormat format, boolean isAudio) + public void writeTrackInfoFiles(List formatList, boolean isAudio) throws IOException { - File file = new File(getBufferDir(), (isAudio ? "audio" : "video") + META_FILE_SUFFIX); - try (DataOutputStream out = new DataOutputStream(new FileOutputStream(file))) { - writeString(out, trackId); - writeFormatString(out, format, MediaFormat.KEY_MIME); - writeFormatInt(out, format, MediaFormat.KEY_MAX_INPUT_SIZE); - writeFormatInt(out, format, MediaFormat.KEY_WIDTH); - writeFormatInt(out, format, MediaFormat.KEY_HEIGHT); - writeFormatInt(out, format, MediaFormat.KEY_CHANNEL_COUNT); - writeFormatInt(out, format, MediaFormat.KEY_SAMPLE_RATE); - writeFormatFloat(out, format, KEY_PIXEL_WIDTH_HEIGHT_RATIO); - for (int i = 0; i < 3; ++i) { - writeFormatByteBuffer(out, format, "csd-" + i); + for (int i = 0; i < formatList.size() ; ++i) { + BufferManager.TrackFormat trackFormat = formatList.get(i); + String fileName = (isAudio ? META_FILE_TYPE_AUDIO : META_FILE_TYPE_VIDEO) + + ((i == 0) ? META_FILE_SUFFIX : (i + META_FILE_SUFFIX)); + File file = new File(getBufferDir(), fileName); + try (DataOutputStream out = new DataOutputStream(new FileOutputStream(file))) { + writeString(out, trackFormat.trackId); + writeFormatString(out, trackFormat.format, MediaFormat.KEY_MIME); + writeFormatInt(out, trackFormat.format, MediaFormat.KEY_MAX_INPUT_SIZE); + writeFormatInt(out, trackFormat.format, MediaFormat.KEY_WIDTH); + writeFormatInt(out, trackFormat.format, MediaFormat.KEY_HEIGHT); + writeFormatInt(out, trackFormat.format, MediaFormat.KEY_CHANNEL_COUNT); + writeFormatInt(out, trackFormat.format, MediaFormat.KEY_SAMPLE_RATE); + writeFormatFloat(out, trackFormat.format, KEY_PIXEL_WIDTH_HEIGHT_RATIO); + for (int j = 0; j < 3; ++j) { + writeFormatByteBuffer(out, trackFormat.format, "csd-" + j); + } + writeFormatLong(out, trackFormat.format, MediaFormat.KEY_DURATION); + writeFormatString(out, trackFormat.format, MediaFormat.KEY_LANGUAGE); + } + } + } + + /** + * Writes caption information to files. + * + * @param tracks a list of {@link AtscCaptionTrack} objects which store caption information. + */ + public void writeCaptionInfoFiles(List tracks) { + if (tracks == null || tracks.isEmpty()) { + return; + } + for (int i = 0; i < tracks.size(); i++) { + AtscCaptionTrack track = tracks.get(i); + String fileName = META_FILE_TYPE_CAPTION + + ((i == 0) ? META_FILE_SUFFIX : (i + META_FILE_SUFFIX)); + File file = new File(getBufferDir(), fileName); + try (DataOutputStream out = new DataOutputStream(new FileOutputStream(file))) { + out.write(MessageNano.toByteArray(track)); + } catch (Exception e) { + Log.e(TAG, "Fail to write caption info to files", e); } - writeFormatLong(out, format, MediaFormat.KEY_DURATION); } } @Override - public void writeIndexFile(String trackName, SortedMap index) + public void writeIndexFile(String trackName, SortedMap> index) throws IOException { - File indexFile = new File(getBufferDir(), trackName + IDX_FILE_SUFFIX); + File indexFile = new File(getBufferDir(), trackName + IDX_FILE_SUFFIX_V2); try (DataOutputStream out = new DataOutputStream(new FileOutputStream(indexFile))) { out.writeLong(index.size()); - for (Long key : index.keySet()) { - out.writeLong(key); + for (Map.Entry> entry : index.entrySet()) { + out.writeLong(entry.getKey()); + out.writeLong(entry.getValue().first.getStartPositionUs()); + out.writeInt(entry.getValue().second); } } } diff --git a/src/com/android/tv/tuner/exoplayer/buffer/RecordingSampleBuffer.java b/src/com/android/tv/tuner/exoplayer/buffer/RecordingSampleBuffer.java index 4869b49f..af0c3f0d 100644 --- a/src/com/android/tv/tuner/exoplayer/buffer/RecordingSampleBuffer.java +++ b/src/com/android/tv/tuner/exoplayer/buffer/RecordingSampleBuffer.java @@ -66,9 +66,14 @@ public class RecordingSampleBuffer implements BufferManager.SampleBuffer, public static final int BUFFER_REASON_RECORDING = 2; /** - * The duration of a chunk of samples, {@link SampleChunk}. + * The minimum duration to support seek in Trickplay. */ - static final long CHUNK_DURATION_US = TimeUnit.MILLISECONDS.toMicros(500); + static final long MIN_SEEK_DURATION_US = TimeUnit.MILLISECONDS.toMicros(500); + + /** + * The duration of a {@link SampleChunk} for recordings. + */ + static final long RECORDING_CHUNK_DURATION_US = MIN_SEEK_DURATION_US * 1200; // 10 minutes private static final long BUFFER_WRITE_TIMEOUT_MS = 10 * 1000; // 10 seconds private static final long BUFFER_NEEDED_US = 1000L * Math.max(MpegTsPlayer.MIN_BUFFER_MS, MpegTsPlayer.MIN_REBUFFER_MS); @@ -79,7 +84,6 @@ public class RecordingSampleBuffer implements BufferManager.SampleBuffer, private int mTrackCount; private boolean[] mTrackSelected; - private List mIds; private List mReadSampleQueues; private final SamplePool mSamplePool = new SamplePool(); private long mLastBufferedPositionUs = C.UNKNOWN_TIME_US; @@ -130,7 +134,6 @@ public class RecordingSampleBuffer implements BufferManager.SampleBuffer, if (mTrackCount <= 0) { throw new IOException("No tracks to initialize"); } - mIds = ids; mTrackSelected = new boolean[mTrackCount]; mReadSampleQueues = new ArrayList<>(); mSampleChunkIoHelper = new SampleChunkIoHelper(ids, mediaFormats, mBufferReason, @@ -139,6 +142,9 @@ public class RecordingSampleBuffer implements BufferManager.SampleBuffer, mReadSampleQueues.add(i, new SampleQueue(mSamplePool)); } mSampleChunkIoHelper.init(); + for (int i = 0; i < mTrackCount; ++i) { + mBufferManager.registerChunkEvictedListener(ids.get(i), RecordingSampleBuffer.this); + } } @Override @@ -146,8 +152,6 @@ public class RecordingSampleBuffer implements BufferManager.SampleBuffer, if (!mTrackSelected[index]) { mTrackSelected[index] = true; mReadSampleQueues.get(index).clear(); - mBufferManager.registerChunkEvictedListener(mIds.get(index), - RecordingSampleBuffer.this); mSampleChunkIoHelper.openRead(index, mCurrentPlaybackPositionUs); } } @@ -157,7 +161,7 @@ public class RecordingSampleBuffer implements BufferManager.SampleBuffer, if (mTrackSelected[index]) { mTrackSelected[index] = false; mReadSampleQueues.get(index).clear(); - mBufferManager.unregisterChunkEvictedListener(mIds.get(index)); + mSampleChunkIoHelper.closeRead(index); } } @@ -193,7 +197,6 @@ public class RecordingSampleBuffer implements BufferManager.SampleBuffer, } // Disables buffering samples afterwards, and notifies the disk speed is slow. Log.w(TAG, "Disk is too slow for trickplay"); - mBufferManager.disable(); mBufferListener.onDiskTooSlow(); } @@ -205,7 +208,7 @@ public class RecordingSampleBuffer implements BufferManager.SampleBuffer, private boolean maybeReadSample(SampleQueue queue, int index) { if (queue.getLastQueuedPositionUs() != null && queue.getLastQueuedPositionUs() > mCurrentPlaybackPositionUs + BUFFER_NEEDED_US - && queue.isDurationGreaterThan(CHUNK_DURATION_US)) { + && queue.isDurationGreaterThan(MIN_SEEK_DURATION_US)) { // The speed of queuing samples can be higher than the playback speed. // If the duration of the samples in the queue is not limited, // samples can be accumulated and there can be out-of-memory issues. @@ -300,7 +303,7 @@ public class RecordingSampleBuffer implements BufferManager.SampleBuffer, public void onChunkEvicted(String id, long createdTimeMs) { if (mBufferListener != null) { mBufferListener.onBufferStartTimeChanged( - createdTimeMs + TimeUnit.MICROSECONDS.toMillis(CHUNK_DURATION_US)); + createdTimeMs + TimeUnit.MICROSECONDS.toMillis(MIN_SEEK_DURATION_US)); } } } diff --git a/src/com/android/tv/tuner/exoplayer/buffer/SampleChunk.java b/src/com/android/tv/tuner/exoplayer/buffer/SampleChunk.java index 552caaef..ab6d1a75 100644 --- a/src/com/android/tv/tuner/exoplayer/buffer/SampleChunk.java +++ b/src/com/android/tv/tuner/exoplayer/buffer/SampleChunk.java @@ -151,18 +151,23 @@ public class SampleChunk { mCurrentOffset = 0; } + private void reset(SampleChunk chunk, long offset) { + mChunk = chunk; + mCurrentOffset = offset; + } + /** * Prepares for read I/O operation from a new SampleChunk. * * @param chunk the new SampleChunk to read from * @throws IOException */ - void openRead(SampleChunk chunk) throws IOException { + void openRead(SampleChunk chunk, long offset) throws IOException { if (mChunk != null) { mChunk.closeRead(); } chunk.openRead(); - reset(chunk); + reset(chunk, offset); } /** @@ -240,6 +245,20 @@ public class SampleChunk { } } + /** + * Returns the current SampleChunk for subsequent I/O operation. + */ + SampleChunk getChunk() { + return mChunk; + } + + /** + * Returns the current offset of the current SampleChunk for subsequent I/O operation. + */ + long getOffset() { + return mCurrentOffset; + } + /** * Releases SampleChunk. the SampleChunk will not be used anymore. * diff --git a/src/com/android/tv/tuner/exoplayer/buffer/SampleChunkIoHelper.java b/src/com/android/tv/tuner/exoplayer/buffer/SampleChunkIoHelper.java index 37ae4022..ca97a91a 100644 --- a/src/com/android/tv/tuner/exoplayer/buffer/SampleChunkIoHelper.java +++ b/src/com/android/tv/tuner/exoplayer/buffer/SampleChunkIoHelper.java @@ -21,6 +21,7 @@ import android.os.ConditionVariable; import android.os.Handler; import android.os.HandlerThread; import android.os.Message; +import android.util.ArraySet; import android.util.Log; import android.util.Pair; @@ -31,7 +32,9 @@ import com.android.tv.common.SoftPreconditions; import com.android.tv.tuner.exoplayer.buffer.RecordingSampleBuffer.BufferReason; import java.io.IOException; +import java.util.LinkedList; import java.util.List; +import java.util.Set; import java.util.concurrent.ConcurrentLinkedQueue; /** @@ -46,11 +49,13 @@ public class SampleChunkIoHelper implements Handler.Callback { private static final int MSG_OPEN_READ = 1; private static final int MSG_OPEN_WRITE = 2; - private static final int MSG_CLOSE_WRITE = 3; - private static final int MSG_READ = 4; - private static final int MSG_WRITE = 5; - private static final int MSG_RELEASE = 6; + private static final int MSG_CLOSE_READ = 3; + private static final int MSG_CLOSE_WRITE = 4; + private static final int MSG_READ = 5; + private static final int MSG_WRITE = 6; + private static final int MSG_RELEASE = 7; + private final long mSampleChunkDurationUs; private final int mTrackCount; private final List mIds; private final List mMediaFormats; @@ -62,9 +67,11 @@ public class SampleChunkIoHelper implements Handler.Callback { private Handler mIoHandler; private final ConcurrentLinkedQueue mReadSampleBuffers[]; private final ConcurrentLinkedQueue mHandlerReadSampleBuffers[]; - private final long[] mWriteEndPositionUs; + private final long[] mWriteIndexEndPositionUs; + private final long[] mWriteChunkEndPositionUs; private final SampleChunk.IoState[] mReadIoStates; private final SampleChunk.IoState[] mWriteIoStates; + private final Set mSelectedTracks = new ArraySet<>(); private long mBufferDurationUs = 0; private boolean mWriteEnded; private boolean mErrorNotified; @@ -129,11 +136,20 @@ public class SampleChunkIoHelper implements Handler.Callback { mReadSampleBuffers = new ConcurrentLinkedQueue[mTrackCount]; mHandlerReadSampleBuffers = new ConcurrentLinkedQueue[mTrackCount]; - mWriteEndPositionUs = new long[mTrackCount]; + mWriteIndexEndPositionUs = new long[mTrackCount]; + mWriteChunkEndPositionUs = new long[mTrackCount]; mReadIoStates = new SampleChunk.IoState[mTrackCount]; mWriteIoStates = new SampleChunk.IoState[mTrackCount]; + + // Small chunk duration for live playback will give more fine grained storage usage + // and eviction handling for trickplay. + mSampleChunkDurationUs = + bufferReason == RecordingSampleBuffer.BUFFER_REASON_LIVE_PLAYBACK ? + RecordingSampleBuffer.MIN_SEEK_DURATION_US : + RecordingSampleBuffer.RECORDING_CHUNK_DURATION_US; for (int i = 0; i < mTrackCount; ++i) { - mWriteEndPositionUs[i] = RecordingSampleBuffer.CHUNK_DURATION_US; + mWriteIndexEndPositionUs[i] = RecordingSampleBuffer.MIN_SEEK_DURATION_US; + mWriteChunkEndPositionUs[i] = mSampleChunkDurationUs; mReadIoStates[i] = new SampleChunk.IoState(); mWriteIoStates[i] = new SampleChunk.IoState(); } @@ -203,6 +219,15 @@ public class SampleChunkIoHelper implements Handler.Callback { mIoHandler.sendMessage(mIoHandler.obtainMessage(MSG_OPEN_READ, params)); } + /** + * Closes read from the specified track. + * + * @param index track index + */ + public void closeRead(int index) { + mIoHandler.sendMessage(mIoHandler.obtainMessage(MSG_CLOSE_READ, index)); + } + /** * Notifies writes are finished. */ @@ -229,21 +254,19 @@ public class SampleChunkIoHelper implements Handler.Callback { try { if (mBufferReason == RecordingSampleBuffer.BUFFER_REASON_RECORDING && mTrackCount > 0) { // Saves meta information for recording. - Pair audio = null, video = null; + List audios = new LinkedList<>(); + List videos = new LinkedList<>(); for (int i = 0; i < mTrackCount; ++i) { android.media.MediaFormat format = mMediaFormats.get(i).getFrameworkMediaFormatV16(); format.setLong(android.media.MediaFormat.KEY_DURATION, mBufferDurationUs); - if (audio == null && MimeTypes.isAudio(mMediaFormats.get(i).mimeType)) { - audio = new Pair<>(mIds.get(i), format); - } else if (video == null && MimeTypes.isVideo(mMediaFormats.get(i).mimeType)) { - video = new Pair<>(mIds.get(i), format); - } - if (audio != null && video != null) { - break; + if (MimeTypes.isAudio(mMediaFormats.get(i).mimeType)) { + audios.add(new BufferManager.TrackFormat(mIds.get(i), format)); + } else if (MimeTypes.isVideo(mMediaFormats.get(i).mimeType)) { + videos.add(new BufferManager.TrackFormat(mIds.get(i), format)); } } - mBufferManager.writeMetaFiles(audio, video); + mBufferManager.writeMetaFiles(audios, videos); } } finally { mBufferManager.release(); @@ -265,6 +288,9 @@ public class SampleChunkIoHelper implements Handler.Callback { case MSG_OPEN_WRITE: doOpenWrite((int) message.obj); return true; + case MSG_CLOSE_READ: + doCloseRead((int) message.obj); + return true; case MSG_CLOSE_WRITE: doCloseWrite(); return true; @@ -291,14 +317,16 @@ public class SampleChunkIoHelper implements Handler.Callback { private void doOpenRead(IoParams params) throws IOException { int index = params.index; mIoHandler.removeMessages(MSG_READ, index); - SampleChunk chunk = mBufferManager.getReadFile(mIds.get(index), params.positionUs); - if (chunk == null) { + Pair readPosition = + mBufferManager.getReadFile(mIds.get(index), params.positionUs); + if (readPosition == null) { String errorMessage = "Chunk ID:" + mIds.get(index) + " pos:" + params.positionUs + "is not found"; - SoftPreconditions.checkNotNull(chunk, TAG, errorMessage); + SoftPreconditions.checkNotNull(readPosition, TAG, errorMessage); throw new IOException(errorMessage); } - mReadIoStates[index].openRead(chunk); + mSelectedTracks.add(index); + mReadIoStates[index].openRead(readPosition.first, (long) readPosition.second); if (mHandlerReadSampleBuffers[index] != null) { SampleHolder sample; while ((sample = mHandlerReadSampleBuffers[index].poll()) != null) { @@ -310,10 +338,22 @@ public class SampleChunkIoHelper implements Handler.Callback { } private void doOpenWrite(int index) throws IOException { - SampleChunk chunk = mBufferManager.createNewWriteFile(mIds.get(index), 0, mSamplePool); + SampleChunk chunk = mBufferManager.createNewWriteFileIfNeeded(mIds.get(index), 0, + mSamplePool, null, 0); mWriteIoStates[index].openWrite(chunk); } + private void doCloseRead(int index) { + mSelectedTracks.remove(index); + if (mHandlerReadSampleBuffers[index] != null) { + SampleHolder sample; + while ((sample = mHandlerReadSampleBuffers[index].poll()) != null) { + mSamplePool.releaseSample(sample); + } + } + mIoHandler.removeMessages(MSG_READ, index); + } + private void doRead(int index) throws IOException { mIoHandler.removeMessages(MSG_READ, index); if (mHandlerReadSampleBuffers[index].size() >= MAX_READ_BUFFER_SAMPLES) { @@ -357,13 +397,21 @@ public class SampleChunkIoHelper implements Handler.Callback { if (sample.timeUs > mBufferDurationUs) { mBufferDurationUs = sample.timeUs; } - - if (sample.timeUs >= mWriteEndPositionUs[index]) { - nextChunk = mBufferManager.createNewWriteFile(mIds.get(index), - mWriteEndPositionUs[index], mSamplePool); - mWriteEndPositionUs[index] = - ((sample.timeUs / RecordingSampleBuffer.CHUNK_DURATION_US) + 1) * - RecordingSampleBuffer.CHUNK_DURATION_US; + if (sample.timeUs >= mWriteIndexEndPositionUs[index]) { + SampleChunk currentChunk = sample.timeUs >= mWriteChunkEndPositionUs[index] ? + null : mWriteIoStates[params.index].getChunk(); + int currentOffset = (int) mWriteIoStates[params.index].getOffset(); + nextChunk = mBufferManager.createNewWriteFileIfNeeded( + mIds.get(index), mWriteIndexEndPositionUs[index], mSamplePool, + currentChunk, currentOffset); + mWriteIndexEndPositionUs[index] = + ((sample.timeUs / RecordingSampleBuffer.MIN_SEEK_DURATION_US) + 1) * + RecordingSampleBuffer.MIN_SEEK_DURATION_US; + if (nextChunk != null) { + mWriteChunkEndPositionUs[index] = + ((sample.timeUs / mSampleChunkDurationUs) + 1) + * mSampleChunkDurationUs; + } } } mWriteIoStates[params.index].write(params.sample, nextChunk); @@ -391,15 +439,22 @@ public class SampleChunkIoHelper implements Handler.Callback { mIoHandler.removeCallbacksAndMessages(null); mFinished = true; conditionVariable.open(); + mSelectedTracks.clear(); } private void releaseEvictedChunks() { - if (mBufferReason != RecordingSampleBuffer.BUFFER_REASON_LIVE_PLAYBACK) { + if (mBufferReason != RecordingSampleBuffer.BUFFER_REASON_LIVE_PLAYBACK + || mSelectedTracks.isEmpty()) { return; } + long currentStartPositionUs = Long.MAX_VALUE; + for (int trackIndex : mSelectedTracks) { + currentStartPositionUs = Math.min(currentStartPositionUs, + mReadIoStates[trackIndex].getStartPositionUs()); + } for (int i = 0; i < mTrackCount; ++i) { long evictEndPositionUs = Math.min(mBufferManager.getStartPositionUs(mIds.get(i)), - mReadIoStates[i].getStartPositionUs()); + currentStartPositionUs); mBufferManager.evictChunks(mIds.get(i), evictEndPositionUs); } } diff --git a/src/com/android/tv/tuner/exoplayer/buffer/SampleQueue.java b/src/com/android/tv/tuner/exoplayer/buffer/SampleQueue.java index 7b098f40..75eac5a2 100644 --- a/src/com/android/tv/tuner/exoplayer/buffer/SampleQueue.java +++ b/src/com/android/tv/tuner/exoplayer/buffer/SampleQueue.java @@ -43,6 +43,7 @@ public class SampleQueue { if (sampleFromQueue == null) { return SampleSource.NOTHING_READ; } + sample.ensureSpaceForWrite(sampleFromQueue.size); sample.size = sampleFromQueue.size; sample.flags = sampleFromQueue.flags; sample.timeUs = sampleFromQueue.timeUs; diff --git a/src/com/android/tv/tuner/exoplayer/buffer/SimpleSampleBuffer.java b/src/com/android/tv/tuner/exoplayer/buffer/SimpleSampleBuffer.java index 40c4ef95..0b219b41 100644 --- a/src/com/android/tv/tuner/exoplayer/buffer/SimpleSampleBuffer.java +++ b/src/com/android/tv/tuner/exoplayer/buffer/SimpleSampleBuffer.java @@ -19,6 +19,7 @@ package com.android.tv.tuner.exoplayer.buffer; import android.os.ConditionVariable; import android.support.annotation.NonNull; + import com.google.android.exoplayer.C; import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.SampleHolder; diff --git a/src/com/android/tv/tuner/exoplayer/buffer/TrickplayStorageManager.java b/src/com/android/tv/tuner/exoplayer/buffer/TrickplayStorageManager.java index 258a5cd0..9fe921b8 100644 --- a/src/com/android/tv/tuner/exoplayer/buffer/TrickplayStorageManager.java +++ b/src/com/android/tv/tuner/exoplayer/buffer/TrickplayStorageManager.java @@ -17,20 +17,23 @@ package com.android.tv.tuner.exoplayer.buffer; import android.content.Context; -import android.media.MediaFormat; import android.os.AsyncTask; -import android.os.Looper; import android.provider.Settings; +import android.support.annotation.NonNull; import android.util.Pair; +import com.android.tv.common.SoftPreconditions; + import java.io.File; import java.util.ArrayList; +import java.util.List; import java.util.SortedMap; /** * Manages Trickplay storage. */ public class TrickplayStorageManager implements BufferManager.StorageManager { + // TODO: Support multi-sessions. private static final String BUFFER_DIR = "timeshift"; // Copied from android.provider.Settings.Global (hidden fields) @@ -43,53 +46,68 @@ public class TrickplayStorageManager implements BufferManager.StorageManager { private static final int DEFAULT_THRESHOLD_PERCENTAGE = 10; private static final long DEFAULT_THRESHOLD_MAX_BYTES = 500L * 1024 * 1024; - private final File mBufferDir; + private static AsyncTask sLastCacheCleanUpTask; + private static File sBufferDir; + private static long sStorageBufferBytes; + private final long mMaxBufferSize; - private final long mStorageBufferBytes; - private static long getStorageBufferBytes(Context context, File path) { + private static void initParamsIfNeeded(Context context, @NonNull File path) { + // TODO: Support multi-sessions. + SoftPreconditions.checkState( + sBufferDir == null || sBufferDir.equals(path)); + if (path.equals(sBufferDir)) { + return; + } + sBufferDir = path; long lowPercentage = Settings.Global.getInt(context.getContentResolver(), SYS_STORAGE_THRESHOLD_PERCENTAGE, DEFAULT_THRESHOLD_PERCENTAGE); - long lowBytes = path.getTotalSpace() * lowPercentage / 100; + long lowPercentageToBytes = path.getTotalSpace() * lowPercentage / 100; long maxLowBytes = Settings.Global.getLong(context.getContentResolver(), SYS_STORAGE_THRESHOLD_MAX_BYTES, DEFAULT_THRESHOLD_MAX_BYTES); - return Math.min(lowBytes, maxLowBytes); + sStorageBufferBytes = Math.min(lowPercentageToBytes, maxLowBytes); } - public TrickplayStorageManager(Context context, File baseDir, long maxBufferSize) { - mBufferDir = new File(baseDir, BUFFER_DIR); - mBufferDir.mkdirs(); + public TrickplayStorageManager(Context context, @NonNull File baseDir, long maxBufferSize) { + initParamsIfNeeded(context, new File(baseDir, BUFFER_DIR)); + sBufferDir.mkdirs(); mMaxBufferSize = maxBufferSize; clearStorage(); - mStorageBufferBytes = getStorageBufferBytes(context, mBufferDir); } - @Override - public void clearStorage() { - File files[] = mBufferDir.listFiles(); - if (files == null || files.length == 0) { - return; + private void clearStorage() { + long now = System.currentTimeMillis(); + if (sLastCacheCleanUpTask != null) { + sLastCacheCleanUpTask.cancel(true); } - if (Looper.myLooper() == Looper.getMainLooper()) { - new AsyncTask() { - @Override - protected Void doInBackground(Void... params) { - for (File file : files) { + sLastCacheCleanUpTask = new AsyncTask() { + @Override + protected Void doInBackground(Void... params) { + if (isCancelled()) { + return null; + } + File files[] = sBufferDir.listFiles(); + if (files == null || files.length == 0) { + return null; + } + for (File file : files) { + if (isCancelled()) { + break; + } + long lastModified = file.lastModified(); + if (lastModified != 0 && lastModified < now) { file.delete(); } - return null; } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } else { - for (File file : files) { - file.delete(); + return null; } - } + }; + sLastCacheCleanUpTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } @Override public File getBufferDir() { - return mBufferDir; + return sBufferDir; } @Override @@ -104,25 +122,26 @@ public class TrickplayStorageManager implements BufferManager.StorageManager { @Override public boolean hasEnoughBuffer(long pendingDelete) { - return mBufferDir.getUsableSpace() + pendingDelete >= mStorageBufferBytes; + return sBufferDir.getUsableSpace() + pendingDelete >= sStorageBufferBytes; } @Override - public Pair readTrackInfoFile(boolean isAudio) { + public List readTrackInfoFiles(boolean isAudio) { return null; } @Override - public ArrayList readIndexFile(String trackId) { + public ArrayList readIndexFile(String trackId) { return null; } @Override - public void writeTrackInfoFile(String trackId, MediaFormat format, boolean isAudio) { + public void writeTrackInfoFiles(List formatList, boolean isAudio) { } @Override - public void writeIndexFile(String trackName, SortedMap index) { + public void writeIndexFile(String trackName, + SortedMap> index) { } } diff --git a/src/com/android/tv/tuner/setup/ConnectionTypeFragment.java b/src/com/android/tv/tuner/setup/ConnectionTypeFragment.java index 97d9ece3..53678a85 100644 --- a/src/com/android/tv/tuner/setup/ConnectionTypeFragment.java +++ b/src/com/android/tv/tuner/setup/ConnectionTypeFragment.java @@ -35,6 +35,24 @@ public class ConnectionTypeFragment extends SetupMultiPaneFragment { public static final String ACTION_CATEGORY = "com.android.tv.tuner.setup.ConnectionTypeFragment"; + @Override + public void onCreate(Bundle savedInstanceState) { + ((TunerSetupActivity) getActivity()).generateTunerHal(); + super.onCreate(savedInstanceState); + } + + @Override + public void onResume() { + ((TunerSetupActivity) getActivity()).generateTunerHal(); + super.onResume(); + } + + @Override + public void onDestroy() { + ((TunerSetupActivity) getActivity()).clearTunerHal(); + super.onDestroy(); + } + @Override protected SetupGuidedStepFragment onCreateContentFragment() { return new ContentFragment(); diff --git a/src/com/android/tv/tuner/setup/PostalCodeFragment.java b/src/com/android/tv/tuner/setup/PostalCodeFragment.java new file mode 100644 index 00000000..a4dd494c --- /dev/null +++ b/src/com/android/tv/tuner/setup/PostalCodeFragment.java @@ -0,0 +1,184 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.tuner.setup; + +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v17.leanback.widget.GuidanceStylist.Guidance; +import android.support.v17.leanback.widget.GuidedAction; +import android.support.v17.leanback.widget.GuidedActionsStylist; +import android.text.InputFilter; +import android.text.Spanned; +import android.text.TextUtils; +import android.view.View; +import android.widget.TextView; +import android.widget.Toast; + +import com.android.tv.R; +import com.android.tv.common.ui.setup.SetupGuidedStepFragment; +import com.android.tv.common.ui.setup.SetupMultiPaneFragment; +import com.android.tv.tuner.util.PostalCodeUtils; + +import java.util.List; + +/** + * A fragment for initial screen. + */ +public class PostalCodeFragment extends SetupMultiPaneFragment { + public static final String ACTION_CATEGORY = + "com.android.tv.tuner.setup.PostalCodeFragment"; + private static final int VIEW_TYPE_EDITABLE = 1; + + @Override + protected SetupGuidedStepFragment onCreateContentFragment() { + ContentFragment fragment = new ContentFragment(); + Bundle arguments = new Bundle(); + arguments.putBoolean(SetupGuidedStepFragment.KEY_THREE_PANE, true); + fragment.setArguments(arguments); + return fragment; + } + + @Override + protected String getActionCategory() { + return ACTION_CATEGORY; + } + + @Override + protected boolean needsDoneButton() { + return true; + } + + @Override + protected boolean needsSkipButton() { + return true; + } + + @Override + protected void setOnClickAction(View view, final String category, final int actionId) { + if (actionId == ACTION_DONE) { + view.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + CharSequence postalCode = + ((ContentFragment) getContentFragment()).mEditAction.getTitle(); + if (postalCode != null && postalCode.length() == 5) { + PostalCodeUtils.setLastPostalCode(getContext(), postalCode.toString()); + onActionClick(category, actionId); + } else { + ContentFragment contentFragment = (ContentFragment) getContentFragment(); + contentFragment.mEditAction.setDescription( + getString(R.string.postal_code_invalid_warning)); + contentFragment.notifyActionChanged(0); + contentFragment.mEditedActionView.performClick(); + } + } + }); + } else if (actionId == ACTION_SKIP) { + super.setOnClickAction(view, category, ACTION_SKIP); + } + } + + public static class ContentFragment extends SetupGuidedStepFragment { + private GuidedAction mEditAction; + private View mEditedActionView; + private View mDoneActionView; + private boolean mProceed; + + @Override + public void onGuidedActionFocused(GuidedAction action) { + if (action.equals(mEditAction)) { + if (mProceed) { + // "NEXT" in IME was just clicked, moves focus to Done button. + if (mDoneActionView == null) { + mDoneActionView = getActivity().findViewById(R.id.button_done); + } + mDoneActionView.requestFocus(); + mProceed = false; + } else { + // Directly opens IME to input postal/zip code. + if (mEditedActionView == null) { + mEditedActionView = getView().findViewById(R.id.guidedactions_editable); + ((TextView) mEditedActionView.findViewById(R.id.guidedactions_item_title)) + .setFilters(new InputFilter[]{new InputFilter() { + @Override + public CharSequence filter(CharSequence source, int start, + int end, Spanned dest, int dstart, int dend) { + try { + Integer.parseInt(source.toString()); + return null; + } catch (NumberFormatException e) { + return ""; + } + } + }, new InputFilter.LengthFilter(5)}); + } + mEditedActionView.performClick(); + } + } + } + + @Override + public long onGuidedActionEditedAndProceed(GuidedAction action) { + mProceed = true; + return 0; + } + + @NonNull + @Override + public Guidance onCreateGuidance(Bundle savedInstanceState) { + String title = getString(R.string.postal_code_guidance_title); + String description = getString(R.string.postal_code_guidance_description); + String breadcrumb = getString(R.string.ut_setup_breadcrumb); + return new Guidance(title, description, breadcrumb, null); + } + + @Override + public void onCreateActions(@NonNull List actions, + Bundle savedInstanceState) { + String description = getString(R.string.postal_code_action_description); + mEditAction = new GuidedAction.Builder(getActivity()).id(0).editable(true) + .description(description).build(); + actions.add(mEditAction); + } + + @Override + protected String getActionCategory() { + return ACTION_CATEGORY; + } + + @Override + public GuidedActionsStylist onCreateActionsStylist() { + return new GuidedActionsStylist() { + @Override + public int getItemViewType(GuidedAction action) { + if (action.isEditable()) { + return VIEW_TYPE_EDITABLE; + } + return super.getItemViewType(action); + } + + @Override + public int onProvideItemLayoutId(int viewType) { + if (viewType == VIEW_TYPE_EDITABLE) { + return R.layout.guided_action_editable; + } + return super.onProvideItemLayoutId(viewType); + } + }; + } + } +} \ No newline at end of file diff --git a/src/com/android/tv/tuner/setup/ScanFragment.java b/src/com/android/tv/tuner/setup/ScanFragment.java index 4b3ffe40..75b28e32 100644 --- a/src/com/android/tv/tuner/setup/ScanFragment.java +++ b/src/com/android/tv/tuner/setup/ScanFragment.java @@ -21,6 +21,7 @@ import android.app.Activity; import android.app.ProgressDialog; import android.content.Context; import android.os.AsyncTask; +import android.os.Build; import android.os.Bundle; import android.os.ConditionVariable; import android.os.Handler; @@ -35,12 +36,11 @@ import android.widget.ListView; import android.widget.ProgressBar; import android.widget.TextView; -import com.android.tv.common.AutoCloseableUtils; import com.android.tv.common.SoftPreconditions; import com.android.tv.common.ui.setup.SetupFragment; import com.android.tv.tuner.ChannelScanFileParser; -import com.android.tv.tuner.TunerHal; import com.android.tv.tuner.R; +import com.android.tv.tuner.TunerHal; import com.android.tv.tuner.TunerPreferences; import com.android.tv.tuner.data.Channel; import com.android.tv.tuner.data.PsipData; @@ -51,7 +51,6 @@ import com.android.tv.tuner.source.TsStreamer; import com.android.tv.tuner.source.TunerTsStreamer; import com.android.tv.tuner.tvinput.ChannelDataManager; import com.android.tv.tuner.tvinput.EventDetector; -import com.android.tv.tuner.util.TunerInputInfoUtils; import junit.framework.Assert; @@ -67,6 +66,7 @@ import java.util.concurrent.TimeUnit; public class ScanFragment extends SetupFragment { private static final String TAG = "ScanFragment"; private static final boolean DEBUG = false; + // In the fake mode, the connection to antenna or cable is not necessary. // Instead dummy channels are added. private static final boolean FAKE_MODE = false; @@ -98,6 +98,7 @@ public class ScanFragment extends SetupFragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + if (DEBUG) Log.d(TAG, "onCreateView"); View view = super.onCreateView(inflater, container, savedInstanceState); mChannelDataManager = new ChannelDataManager(getActivity()); mChannelDataManager.checkDataVersion(getActivity()); @@ -120,13 +121,19 @@ public class ScanFragment extends SetupFragment { } }); Bundle args = getArguments(); + int tunerType = (args == null ? 0 : args.getInt(TunerSetupActivity.KEY_TUNER_TYPE, 0)); // TODO: Handle the case when the fragment is restored. startScan(args == null ? 0 : args.getInt(EXTRA_FOR_CHANNEL_SCAN_FILE, 0)); TextView scanTitleView = (TextView) view.findViewById(R.id.tune_title); - if (TunerInputInfoUtils.isBuiltInTuner(getActivity())){ - scanTitleView.setText(R.string.bt_channel_scan); - } else { - scanTitleView.setText(R.string.ut_channel_scan); + switch (tunerType) { + case TunerHal.TUNER_TYPE_USB: + scanTitleView.setText(R.string.ut_channel_scan); + break; + case TunerHal.TUNER_TYPE_NETWORK: + scanTitleView.setText(R.string.nt_channel_scan); + break; + default: + scanTitleView.setText(R.string.bt_channel_scan); } return view; } @@ -147,12 +154,14 @@ public class ScanFragment extends SetupFragment { } @Override - public void onDetach() { + public void onPause() { + Log.d(TAG, "onPause"); if (mChannelScanTask != null) { // Ensure scan task will stop. + Log.w(TAG, "The activity went to the background. Stopping channel scan."); mChannelScanTask.stopScan(); } - super.onDetach(); + super.onPause(); } /** @@ -168,7 +177,9 @@ public class ScanFragment extends SetupFragment { new Handler().postDelayed(new Runnable() { @Override public void run() { - mChannelScanTask.showFinishingProgressDialog(); + if (mChannelScanTask != null) { + mChannelScanTask.showFinishingProgressDialog(); + } } }, SHOW_PROGRESS_DIALOG_DELAY_MS); @@ -255,7 +266,7 @@ public class ScanFragment extends SetupFragment { if (FAKE_MODE) { mScanTsStreamer = new FakeTsStreamer(this); } else { - TunerHal hal = TunerHal.createInstance(mActivity.getApplicationContext()); + TunerHal hal = ((TunerSetupActivity) mActivity).getTunerHal(); if (hal == null) { throw new RuntimeException("Failed to open a DVB device"); } @@ -316,10 +327,17 @@ public class ScanFragment extends SetupFragment { @Override protected void onProgressUpdate(Integer... values) { - mProgressBar.setProgress(values[0]); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + mProgressBar.setProgress(values[0], true); + } else { + mProgressBar.setProgress(values[0]); + } } private void stopScan() { + if (mLatch != null) { + mLatch.countDown(); + } mConditionStopped.open(); } @@ -360,11 +378,7 @@ public class ScanFragment extends SetupFragment { if (mConditionStopped.block(-1)) { break; } - onProgressUpdate(MAX_PROGRESS * i++ / mScanChannelList.size()); - } - if (mScanTsStreamer instanceof TunerTsStreamer) { - AutoCloseableUtils.closeQuietly( - ((TunerTsStreamer) mScanTsStreamer).getTunerHal()); + publishProgress(MAX_PROGRESS * i++ / mScanChannelList.size()); } mChannelDataManager.notifyScanCompleted(); if (!mConditionStopped.block(-1)) { @@ -454,7 +468,13 @@ public class ScanFragment extends SetupFragment { if (mFinishingProgressDialog != null) { mFinishingProgressDialog.dismiss(); } - onActionClick(ACTION_CATEGORY, mIsCanceled ? ACTION_CANCEL : ACTION_FINISH); + // If the fragment is not resumed, the next fragment (scan result page) can't be + // displayed. In that case, just close the activity. + if (isResumed()) { + onActionClick(ACTION_CATEGORY, mIsCanceled ? ACTION_CANCEL : ACTION_FINISH); + } else if (getActivity() != null) { + getActivity().finish(); + } mChannelScanTask = null; } } diff --git a/src/com/android/tv/tuner/setup/ScanResultFragment.java b/src/com/android/tv/tuner/setup/ScanResultFragment.java index 068543cd..3b8cd823 100644 --- a/src/com/android/tv/tuner/setup/ScanResultFragment.java +++ b/src/com/android/tv/tuner/setup/ScanResultFragment.java @@ -26,6 +26,7 @@ import android.support.v17.leanback.widget.GuidedAction; import com.android.tv.common.ui.setup.SetupGuidedStepFragment; import com.android.tv.common.ui.setup.SetupMultiPaneFragment; import com.android.tv.tuner.R; +import com.android.tv.tuner.TunerHal; import com.android.tv.tuner.TunerPreferences; import com.android.tv.tuner.util.TunerInputInfoUtils; @@ -76,11 +77,19 @@ public class ScanResultFragment extends SetupMultiPaneFragment { mChannelCountOnPreference, mChannelCountOnPreference); breadcrumb = null; } else { + Bundle args = getArguments(); + int tunerType = + (args == null ? 0 : args.getInt(TunerSetupActivity.KEY_TUNER_TYPE, 0)); title = getString(R.string.ut_result_not_found_title); - if (TunerInputInfoUtils.isBuiltInTuner(getActivity())) { - description = getString(R.string.bt_result_not_found_description); - } else { - description = getString(R.string.ut_result_not_found_description); + switch (tunerType) { + case TunerHal.TUNER_TYPE_USB: + description = getString(R.string.ut_result_not_found_description); + break; + case TunerHal.TUNER_TYPE_NETWORK: + description = getString(R.string.nt_result_not_found_description); + break; + default: + description = getString(R.string.bt_result_not_found_description); } breadcrumb = getString(R.string.ut_setup_breadcrumb); } diff --git a/src/com/android/tv/tuner/setup/TunerSetupActivity.java b/src/com/android/tv/tuner/setup/TunerSetupActivity.java index 78121bc5..f618c699 100644 --- a/src/com/android/tv/tuner/setup/TunerSetupActivity.java +++ b/src/com/android/tv/tuner/setup/TunerSetupActivity.java @@ -29,35 +29,53 @@ import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.media.tv.TvContract; +import android.os.AsyncTask; import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.VisibleForTesting; import android.support.v4.app.NotificationCompat; +import android.text.TextUtils; import android.util.Log; import android.view.KeyEvent; import android.widget.Toast; import com.android.tv.TvApplication; +import com.android.tv.common.AutoCloseableUtils; import com.android.tv.common.TvCommonConstants; import com.android.tv.common.TvCommonUtils; import com.android.tv.common.ui.setup.SetupActivity; import com.android.tv.common.ui.setup.SetupFragment; import com.android.tv.common.ui.setup.SetupMultiPaneFragment; +import com.android.tv.experiments.Experiments; import com.android.tv.tuner.R; import com.android.tv.tuner.TunerHal; import com.android.tv.tuner.TunerPreferences; import com.android.tv.tuner.tvinput.TunerTvInputService; -import com.android.tv.tuner.util.TunerInputInfoUtils; +import com.android.tv.tuner.util.PostalCodeUtils; +import com.android.tv.util.LocationUtils; + +import java.util.Locale; +import java.util.concurrent.Executor; /** * An activity that serves tuner setup process. */ public class TunerSetupActivity extends SetupActivity { - private final String TAG = "TunerSetupActivity"; + private static final String TAG = "TunerSetupActivity"; + private static final boolean DEBUG = false; + + /** + * Key for passing tuner type to sub-fragments. + */ + public static final String KEY_TUNER_TYPE = "TunerSetupActivity.tunerType"; + // For the recommendation card private static final String TV_ACTIVITY_CLASS_NAME = "com.android.tv.TvActivity"; private static final String NOTIFY_TAG = "TunerSetup"; private static final int NOTIFY_ID = 1000; private static final String TAG_DRAWABLE = "drawable"; private static final String TAG_ICON = "ic_launcher_s"; + private static final int PERMISSIONS_REQUEST_ACCESS_COARSE_LOCATION = 1; private static final int CHANNEL_MAP_SCAN_FILE[] = { R.raw.ut_us_atsc_center_frequencies_8vsb, @@ -69,9 +87,13 @@ public class TunerSetupActivity extends SetupActivity { R.raw.ut_kr_dev_cj_cable_center_frequencies_qam256}; private ScanFragment mLastScanFragment; + private Integer mTunerType; + private TunerHalFactory mTunerHalFactory; + private boolean mNeedToShowPostalCodeFragment; @Override protected void onCreate(Bundle savedInstanceState) { + if (DEBUG) Log.d(TAG, "onCreate"); TvApplication.setCurrentRunningProcess(this, false); super.onCreate(savedInstanceState); // TODO: check {@link shouldShowRequestPermissionRationale}. @@ -79,13 +101,49 @@ public class TunerSetupActivity extends SetupActivity { != PackageManager.PERMISSION_GRANTED) { // No need to check the request result. requestPermissions(new String[] {android.Manifest.permission.ACCESS_COARSE_LOCATION}, - 0); + PERMISSIONS_REQUEST_ACCESS_COARSE_LOCATION); + } + mTunerType = TunerHal.getTunerTypeAndCount(this).first; + if (mTunerType == null) { + finish(); + } else { + mTunerHalFactory = new TunerHalFactory(getApplicationContext()); + } + try { + // Updating postal code takes time, therefore we called it here for "warm-up". + PostalCodeUtils.setLastPostalCode(this, null); + PostalCodeUtils.updatePostalCode(this); + } catch (Exception e) { + // Do nothing. If the last known postal code is null, we'll show guided fragment to + // prompt users to input postal code before ConnectionTypeFragment is shown. + Log.i(TAG, "Can't get postal code:" + e); + } + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, + @NonNull int[] grantResults) { + if (requestCode == PERMISSIONS_REQUEST_ACCESS_COARSE_LOCATION) { + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED + && Experiments.CLOUD_EPG.get()) { + try { + // Updating postal code takes time, therefore we should update postal code + // right after the permission is granted, so that the subsequent operations, + // especially EPG fetcher, could get the newly updated postal code. + PostalCodeUtils.updatePostalCode(this); + } catch (Exception e) { + // Do nothing + } + } } } @Override protected Fragment onCreateInitialFragment() { SetupFragment fragment = new WelcomeFragment(); + Bundle args = new Bundle(); + args.putInt(KEY_TUNER_TYPE, mTunerType); + fragment.setArguments(args); fragment.setShortDistance(SetupFragment.FRAGMENT_EXIT_TRANSITION | SetupFragment.FRAGMENT_REENTER_TRANSITION); return fragment; @@ -102,33 +160,41 @@ public class TunerSetupActivity extends SetupActivity { finish(); break; default: { - SetupFragment fragment = new ConnectionTypeFragment(); - fragment.setShortDistance(SetupFragment.FRAGMENT_ENTER_TRANSITION - | SetupFragment.FRAGMENT_RETURN_TRANSITION); - showFragment(fragment, true); + if (mNeedToShowPostalCodeFragment + || Locale.US.getCountry().equalsIgnoreCase( + LocationUtils.getCurrentCountry(getApplicationContext())) + && TextUtils.isEmpty(PostalCodeUtils.getLastPostalCode(this))) { + // We cannot get postal code automatically. Postal code input fragment + // should always be shown even if users have input some valid postal + // code in this activity before. + mNeedToShowPostalCodeFragment = true; + showPostalCodeFragment(); + } else { + showConnectionTypeFragment(); + } break; } } return true; + case PostalCodeFragment.ACTION_CATEGORY: + if (actionId == SetupMultiPaneFragment.ACTION_DONE + || actionId == SetupMultiPaneFragment.ACTION_SKIP) { + showConnectionTypeFragment(); + } + return true; case ConnectionTypeFragment.ACTION_CATEGORY: - TunerHal hal = TunerHal.createInstance(getApplicationContext()); - if (hal == null) { + if (mTunerHalFactory.get() == null) { finish(); Toast.makeText(getApplicationContext(), R.string.ut_channel_scan_tuner_unavailable,Toast.LENGTH_LONG).show(); return true; } - try { - hal.close(); - } catch (Exception e) { - Log.e(TAG, "Tuner hal close failed", e); - return true; - } mLastScanFragment = new ScanFragment(); - Bundle args = new Bundle(); - args.putInt(ScanFragment.EXTRA_FOR_CHANNEL_SCAN_FILE, + Bundle args1 = new Bundle(); + args1.putInt(ScanFragment.EXTRA_FOR_CHANNEL_SCAN_FILE, CHANNEL_MAP_SCAN_FILE[actionId]); - mLastScanFragment.setArguments(args); + args1.putInt(KEY_TUNER_TYPE, mTunerType); + mLastScanFragment.setArguments(args1); showFragment(mLastScanFragment, true); return true; case ScanFragment.ACTION_CATEGORY: @@ -137,7 +203,11 @@ public class TunerSetupActivity extends SetupActivity { getFragmentManager().popBackStack(); return true; case ScanFragment.ACTION_FINISH: + mTunerHalFactory.clear(); SetupFragment fragment = new ScanResultFragment(); + Bundle args2 = new Bundle(); + args2.putInt(KEY_TUNER_TYPE, mTunerType); + fragment.setArguments(args2); fragment.setShortDistance(SetupFragment.FRAGMENT_EXIT_TRANSITION | SetupFragment.FRAGMENT_REENTER_TRANSITION); showFragment(fragment, true); @@ -213,7 +283,7 @@ public class TunerSetupActivity extends SetupActivity { String inputId = TvContract.buildInputId(new ComponentName(context.getPackageName(), TunerTvInputService.class.getName())); - // Make an intent to launch the setup activity of USB tuner TV input. + // Make an intent to launch the setup activity of TV tuner input. Intent intent = TvCommonUtils.createSetupIntent( new Intent(context, TunerSetupActivity.class), inputId); intent.putExtra(TvCommonConstants.EXTRA_INPUT_ID, inputId); @@ -223,6 +293,27 @@ public class TunerSetupActivity extends SetupActivity { return intent; } + /** + * Gets the currently used tuner HAL. + */ + TunerHal getTunerHal() { + return mTunerHalFactory.get(); + } + + /** + * Generates tuner HAL. + */ + void generateTunerHal() { + mTunerHalFactory.generate(); + } + + /** + * Clears the currently used tuner HAL. + */ + void clearTunerHal() { + mTunerHalFactory.clear(); + } + /** * Returns a {@link PendingIntent} to launch the tuner TV input service. * @@ -242,12 +333,19 @@ public class TunerSetupActivity extends SetupActivity { Resources resources = context.getResources(); String focusedTitle = resources.getString( R.string.ut_setup_recommendation_card_focused_title); - String title; - if (TunerInputInfoUtils.isBuiltInTuner(context)) { - title = resources.getString(R.string.bt_setup_recommendation_card_title); - } else { - title = resources.getString(R.string.ut_setup_recommendation_card_title); + int titleStringId = 0; + switch (TunerHal.getTunerTypeAndCount(context).first) { + case TunerHal.TUNER_TYPE_BUILT_IN: + titleStringId = R.string.bt_setup_recommendation_card_title; + break; + case TunerHal.TUNER_TYPE_USB: + titleStringId = R.string.ut_setup_recommendation_card_title; + break; + case TunerHal.TUNER_TYPE_NETWORK: + titleStringId = R.string.nt_setup_recommendation_card_title; + break; } + String title = resources.getString(titleStringId); Bitmap largeIcon = BitmapFactory.decodeResource(resources, R.drawable.recommendation_antenna); @@ -269,6 +367,20 @@ public class TunerSetupActivity extends SetupActivity { notificationManager.notify(NOTIFY_TAG, NOTIFY_ID, notification); } + private void showPostalCodeFragment() { + SetupFragment fragment = new PostalCodeFragment(); + fragment.setShortDistance(SetupFragment.FRAGMENT_ENTER_TRANSITION + | SetupFragment.FRAGMENT_RETURN_TRANSITION); + showFragment(fragment, true); + } + + private void showConnectionTypeFragment() { + SetupFragment fragment = new ConnectionTypeFragment(); + fragment.setShortDistance(SetupFragment.FRAGMENT_ENTER_TRANSITION + | SetupFragment.FRAGMENT_RETURN_TRANSITION); + showFragment(fragment, true); + } + /** * Cancels the previously shown recommendation card. * @@ -279,4 +391,80 @@ public class TunerSetupActivity extends SetupActivity { .getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.cancel(NOTIFY_TAG, NOTIFY_ID); } -} + + @VisibleForTesting + static class TunerHalFactory { + private Context mContext; + @VisibleForTesting + TunerHal mTunerHal; + private GenerateTunerHalTask mGenerateTunerHalTask; + private final Executor mExecutor; + + TunerHalFactory(Context context) { + this(context, AsyncTask.SERIAL_EXECUTOR); + } + + TunerHalFactory(Context context, Executor executor) { + mContext = context; + mExecutor = executor; + } + + /** + * Returns tuner HAL currently used. If it's {@code null} and tuner HAL is not generated + * before, tries to generate it synchronously. + */ + TunerHal get() { + if (mGenerateTunerHalTask != null + && mGenerateTunerHalTask.getStatus() != AsyncTask.Status.FINISHED) { + try { + return mGenerateTunerHalTask.get(); + } catch (Exception e) { + Log.e(TAG, "Cannot get Tuner HAL: " + e); + } + } else if (mGenerateTunerHalTask == null && mTunerHal == null) { + mTunerHal = createInstance(); + } + return mTunerHal; + } + + /** + * Generates tuner hal for scanning with asynchronous tasks. + */ + void generate() { + if (mGenerateTunerHalTask == null && mTunerHal == null) { + mGenerateTunerHalTask = new GenerateTunerHalTask(); + mGenerateTunerHalTask.executeOnExecutor(mExecutor); + } + } + + /** + * Clears the currently used tuner hal. + */ + void clear() { + if (mGenerateTunerHalTask != null) { + mGenerateTunerHalTask.cancel(true); + mGenerateTunerHalTask = null; + } + if (mTunerHal != null) { + AutoCloseableUtils.closeQuietly(mTunerHal); + mTunerHal = null; + } + } + + protected TunerHal createInstance() { + return TunerHal.createInstance(mContext); + } + + class GenerateTunerHalTask extends AsyncTask { + @Override + protected TunerHal doInBackground(Void... args) { + return createInstance(); + } + + @Override + protected void onPostExecute(TunerHal tunerHal) { + mTunerHal = tunerHal; + } + } + } +} \ No newline at end of file diff --git a/src/com/android/tv/tuner/setup/WelcomeFragment.java b/src/com/android/tv/tuner/setup/WelcomeFragment.java index 7e809411..a3dddc72 100644 --- a/src/com/android/tv/tuner/setup/WelcomeFragment.java +++ b/src/com/android/tv/tuner/setup/WelcomeFragment.java @@ -27,6 +27,7 @@ import android.view.ViewGroup; import com.android.tv.common.ui.setup.SetupGuidedStepFragment; import com.android.tv.common.ui.setup.SetupMultiPaneFragment; import com.android.tv.tuner.R; +import com.android.tv.tuner.TunerHal; import com.android.tv.tuner.TunerPreferences; import com.android.tv.tuner.util.TunerInputInfoUtils; @@ -41,7 +42,9 @@ public class WelcomeFragment extends SetupMultiPaneFragment { @Override protected SetupGuidedStepFragment onCreateContentFragment() { - return new ContentFragment(); + ContentFragment fragment = new ContentFragment(); + fragment.setArguments(getArguments()); + return fragment; } @Override @@ -70,20 +73,33 @@ public class WelcomeFragment extends SetupMultiPaneFragment { public Guidance onCreateGuidance(Bundle savedInstanceState) { String title; String description; + int tunerType = getArguments().getInt(TunerSetupActivity.KEY_TUNER_TYPE, + TunerHal.TUNER_TYPE_BUILT_IN); if (mChannelCountOnPreference == 0) { - if (TunerInputInfoUtils.isBuiltInTuner(getActivity())) { - title = getString(R.string.bt_setup_new_title); - description = getString(R.string.bt_setup_new_description); - } else { - title = getString(R.string.ut_setup_new_title); - description = getString(R.string.ut_setup_new_description); + switch (tunerType) { + case TunerHal.TUNER_TYPE_USB: + title = getString(R.string.ut_setup_new_title); + description = getString(R.string.ut_setup_new_description); + break; + case TunerHal.TUNER_TYPE_NETWORK: + title = getString(R.string.nt_setup_new_title); + description = getString(R.string.nt_setup_new_description); + break; + default: + title = getString(R.string.bt_setup_new_title); + description = getString(R.string.bt_setup_new_description); } } else { title = getString(R.string.bt_setup_again_title); - if (TunerInputInfoUtils.isBuiltInTuner(getActivity())) { - description = getString(R.string.bt_setup_again_description); - } else { - description = getString(R.string.ut_setup_again_description); + switch (tunerType) { + case TunerHal.TUNER_TYPE_USB: + description = getString(R.string.ut_setup_again_description); + break; + case TunerHal.TUNER_TYPE_NETWORK: + description = getString(R.string.nt_setup_again_description); + break; + default: + description = getString(R.string.bt_setup_again_description); } } return new Guidance(title, description, null, null); diff --git a/src/com/android/tv/tuner/source/FileTsStreamer.java b/src/com/android/tv/tuner/source/FileTsStreamer.java index 14997ee4..80ec8a56 100644 --- a/src/com/android/tv/tuner/source/FileTsStreamer.java +++ b/src/com/android/tv/tuner/source/FileTsStreamer.java @@ -256,7 +256,7 @@ public class FileTsStreamer implements TsStreamer { * Returns whether the current pid filter is empty or not. */ public boolean isFilterEmpty() { - return mPids.size() > 0; + return mPids.size() == 0; } /** diff --git a/src/com/android/tv/tuner/source/TsDataSourceManager.java b/src/com/android/tv/tuner/source/TsDataSourceManager.java index 7286cd8c..32504b95 100644 --- a/src/com/android/tv/tuner/source/TsDataSourceManager.java +++ b/src/com/android/tv/tuner/source/TsDataSourceManager.java @@ -17,7 +17,9 @@ 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.Channel; import com.android.tv.tuner.data.TunerChannel; import com.android.tv.tuner.tvinput.EventDetector; @@ -126,6 +128,14 @@ public class TsDataSourceManager { mKeepTuneStatus = keepTuneStatus; } + /** + * Add tuner hal into TunerTsStreamerManager for test. + */ + @VisibleForTesting + public void addTunerHalForTest(TunerHal tunerHal) { + mTunerStreamerManager.addTunerHal(tunerHal, mId); + } + /** * Releases persistent resources. */ diff --git a/src/com/android/tv/tuner/source/TunerTsStreamer.java b/src/com/android/tv/tuner/source/TunerTsStreamer.java index b24048e6..65b11a5a 100644 --- a/src/com/android/tv/tuner/source/TunerTsStreamer.java +++ b/src/com/android/tv/tuner/source/TunerTsStreamer.java @@ -42,15 +42,17 @@ public class TunerTsStreamer implements TsStreamer { private static final int MIN_READ_UNIT = 1500; private static final int READ_BUFFER_SIZE = MIN_READ_UNIT * 10; // ~15KB private static final int CIRCULAR_BUFFER_SIZE = MIN_READ_UNIT * 20000; // ~ 30MB + private static final int TS_PACKET_SIZE = 188; private static final int READ_TIMEOUT_MS = 5000; // 5 secs. private static final int BUFFER_UNDERRUN_SLEEP_MS = 10; + private static final int READ_ERROR_STREAMING_ENDED = -1; + private static final int READ_ERROR_BUFFER_OVERWRITTEN = -2; private final Object mCircularBufferMonitor = new Object(); private final byte[] mCircularBuffer = new byte[CIRCULAR_BUFFER_SIZE]; private long mBytesFetched; private final AtomicLong mLastReadPosition = new AtomicLong(); - private boolean mEndOfStreamSent; private boolean mStreaming; private final TunerHal mTunerHal; @@ -59,6 +61,7 @@ public class TunerTsStreamer implements TsStreamer { private final EventDetector mEventDetector; private final TsStreamWriter mTsStreamWriter; + private String mChannelNumber; public static class TunerDataSource extends TsDataSource { private final TunerTsStreamer mTsStreamer; @@ -103,6 +106,15 @@ public class TunerTsStreamer implements TsStreamer { offset, readLength); if (ret > 0) { mLastReadPosition.addAndGet(ret); + } else if (ret == READ_ERROR_BUFFER_OVERWRITTEN) { + long currentPosition = mStartBufferedPosition + mLastReadPosition.get(); + long endPosition = mTsStreamer.getBufferedPosition(); + long diff = ((endPosition - currentPosition + TS_PACKET_SIZE - 1) / TS_PACKET_SIZE) + * TS_PACKET_SIZE; + Log.w(TAG, "Demux position jump by overwritten buffer: " + diff); + mStartBufferedPosition = currentPosition + diff; + mLastReadPosition.set(0); + return 0; } return ret; } @@ -114,7 +126,10 @@ public class TunerTsStreamer implements TsStreamer { */ public TunerTsStreamer(TunerHal tunerHal, EventListener eventListener, Context context) { mTunerHal = tunerHal; - mEventDetector = new EventDetector(mTunerHal, eventListener); + mEventDetector = new EventDetector(mTunerHal); + if (eventListener != null) { + mEventDetector.registerListener(eventListener); + } mTsStreamWriter = context != null && TunerPreferences.getStoreTsStream(context) ? new TsStreamWriter(context) : null; } @@ -125,7 +140,8 @@ public class TunerTsStreamer implements TsStreamer { @Override public boolean startStream(TunerChannel channel) { - if (mTunerHal.tune(channel.getFrequency(), channel.getModulation())) { + if (mTunerHal.tune(channel.getFrequency(), channel.getModulation(), + channel.getDisplayNumber(false))) { if (channel.hasVideo()) { mTunerHal.addPidFilter(channel.getVideoPid(), TunerHal.FILTER_TYPE_VIDEO); @@ -148,6 +164,7 @@ public class TunerTsStreamer implements TsStreamer { channel.getProgramNumber()); } mChannel = channel; + mChannelNumber = channel.getDisplayNumber(); synchronized (mCircularBufferMonitor) { if (mStreaming) { Log.w(TAG, "Streaming should be stopped before start streaming"); @@ -156,7 +173,6 @@ public class TunerTsStreamer implements TsStreamer { mStreaming = true; mBytesFetched = 0; mLastReadPosition.set(0L); - mEndOfStreamSent = false; } if (mTsStreamWriter != null) { mTsStreamWriter.setChannel(mChannel); @@ -172,7 +188,7 @@ public class TunerTsStreamer implements TsStreamer { @Override public boolean startStream(ChannelScanFileParser.ScanChannel channel) { - if (mTunerHal.tune(channel.frequency, channel.modulation)) { + if (mTunerHal.tune(channel.frequency, channel.modulation, null)) { mEventDetector.startDetecting( channel.frequency, channel.modulation, EventDetector.ALL_PROGRAM_NUMBERS); synchronized (mCircularBufferMonitor) { @@ -183,7 +199,6 @@ public class TunerTsStreamer implements TsStreamer { mStreaming = true; mBytesFetched = 0; mLastReadPosition.set(0L); - mEndOfStreamSent = false; } mStreamingThread = new StreamingThread(); mStreamingThread.start(); @@ -258,6 +273,22 @@ public class TunerTsStreamer implements TsStreamer { } } + public String getStreamerInfo() { + return "Channel: " + mChannelNumber + ", Streaming: " + mStreaming; + } + + public void registerListener(EventListener listener) { + if (mEventDetector != null && listener != null) { + mEventDetector.registerListener(listener); + } + } + + public void unregisterListener(EventListener listener) { + if (mEventDetector != null) { + mEventDetector.unregisterListener(listener); + } + } + private class StreamingThread extends Thread { @Override public void run() { @@ -321,21 +352,14 @@ public class TunerTsStreamer implements TsStreamer { * @throws IOException */ public int readAt(long pos, byte[] buffer, int offset, int amount) throws IOException { - long readStartTime = System.currentTimeMillis(); while (true) { synchronized (mCircularBufferMonitor) { - if (mEndOfStreamSent || !mStreaming) { - return -1; + if (!mStreaming) { + return READ_ERROR_STREAMING_ENDED; } if (mBytesFetched - CIRCULAR_BUFFER_SIZE > pos) { - Log.e(TAG, "Demux is requesting the data which is already overwritten."); - return -1; - } - if (System.currentTimeMillis() - readStartTime > READ_TIMEOUT_MS) { - // Nothing was received during READ_TIMEOUT_MS before. - mEndOfStreamSent = true; - mCircularBufferMonitor.notifyAll(); - return -1; + Log.w(TAG, "Demux is requesting the data which is already overwritten."); + return READ_ERROR_BUFFER_OVERWRITTEN; } if (mBytesFetched < pos + amount) { try { diff --git a/src/com/android/tv/tuner/source/TunerTsStreamerManager.java b/src/com/android/tv/tuner/source/TunerTsStreamerManager.java index cf1f6dcf..fcd14116 100644 --- a/src/com/android/tv/tuner/source/TunerTsStreamerManager.java +++ b/src/com/android/tv/tuner/source/TunerTsStreamerManager.java @@ -42,6 +42,7 @@ class TunerTsStreamerManager { private final Object mCancelLock = new Object(); private final StreamerFinder mStreamerFinder = new StreamerFinder(); private final Map mCreators = new HashMap<>(); + private final Map mListeners = new HashMap<>(); private final Map mSourceToStreamerMap = new HashMap<>(); private final TunerHalManager mTunerHalManager = new TunerHalManager(); private static TunerTsStreamerManager sInstance; @@ -68,6 +69,8 @@ class TunerTsStreamerManager { mStreamerFinder.appendSessionLocked(channel, sessionId); TunerTsStreamer streamer = mStreamerFinder.getStreamerLocked(channel); TsDataSource source = streamer.createDataSource(); + mListeners.put(sessionId, listener); + streamer.registerListener(listener); mSourceToStreamerMap.put(source, streamer); return source; } @@ -83,6 +86,8 @@ class TunerTsStreamerManager { if (!creator.isCancelledLocked()) { mStreamerFinder.putLocked(channel, sessionId, streamer); TsDataSource source = streamer.createDataSource(); + mListeners.put(sessionId, listener); + streamer.registerListener(listener); mSourceToStreamerMap.put(source, streamer); return source; } @@ -104,6 +109,8 @@ class TunerTsStreamerManager { if (streamer == null) { return; } + EventDetector.EventListener listener = mListeners.remove(sessionId); + streamer.unregisterListener(listener); TunerChannel channel = streamer.getChannel(); SoftPreconditions.checkState(channel != null); mStreamerFinder.removeSessionLocked(channel, sessionId); @@ -125,6 +132,13 @@ class TunerTsStreamerManager { } } + /** + * Add tuner hal into TunerHalManager for test. + */ + void addTunerHal(TunerHal tunerHal, int sessionId) { + mTunerHalManager.addTunerHal(tunerHal, sessionId); + } + synchronized void release(int sessionId) { mTunerHalManager.releaseCachedHal(sessionId); } @@ -261,16 +275,16 @@ class TunerTsStreamerManager { } private void releaseTunerHal(TunerHal hal, int sessionId, boolean reuse) { - if (!reuse) { + if (!reuse || !hal.isReusable()) { AutoCloseableUtils.closeQuietly(hal); return; } TunerHal cachedHal = mTunerHals.get(sessionId); if (cachedHal != hal) { mTunerHals.put(sessionId, hal); - } - if (cachedHal != null && cachedHal != hal) { - AutoCloseableUtils.closeQuietly(cachedHal); + if (cachedHal != null) { + AutoCloseableUtils.closeQuietly(cachedHal); + } } } @@ -283,5 +297,9 @@ class TunerTsStreamerManager { AutoCloseableUtils.closeQuietly(hal); } } + + private void addTunerHal(TunerHal tunerHal, int sessionId) { + mTunerHals.put(sessionId, tunerHal); + } } } \ No newline at end of file diff --git a/src/com/android/tv/tuner/ts/SectionParser.java b/src/com/android/tv/tuner/ts/SectionParser.java index 5d3e728a..fe972cd1 100644 --- a/src/com/android/tv/tuner/ts/SectionParser.java +++ b/src/com/android/tv/tuner/ts/SectionParser.java @@ -367,6 +367,10 @@ public class SectionParser { mParsedEttItems.clear(); } + public void resetVersionNumbers() { + mSectionVersionMap.clear(); + } + private void parseSection(byte[] data) { if (!checkSanity(data)) { Log.d(TAG, "Bad CRC!"); @@ -510,10 +514,8 @@ public class SectionParser { pos += 11 + descriptorsLength; results.add(new MgtItem(tableType, tableTypePid)); } - if ((data[pos] & 0xf0) != 0xf0) { - Log.e(TAG, "Broken MGT."); - return false; - } + // Skip the remaining descriptor part which we don't use. + if (mListener != null) { mListener.onMgtParsed(results); } @@ -717,6 +719,9 @@ public class SectionParser { if (audioDescriptor.getLanguage() != null) { audioTrack.language = audioDescriptor.getLanguage(); } + if (audioTrack.language == null) { + audioTrack.language = ""; + } audioTrack.audioType = AtscAudioTrack.AUDIOTYPE_UNDEFINED; audioTrack.channelCount = audioDescriptor.getNumChannels(); audioTrack.sampleRate = audioDescriptor.getSampleRate(); @@ -948,6 +953,7 @@ public class SectionParser { pos += 3; boolean ccType = (data[pos] & 0x80) != 0; if (!ccType) { + pos +=3; continue; } int captionServiceNumber = data[pos] & 0x3f; diff --git a/src/com/android/tv/tuner/ts/TsParser.java b/src/com/android/tv/tuner/ts/TsParser.java index c24c2a21..21b5a942 100644 --- a/src/com/android/tv/tuner/ts/TsParser.java +++ b/src/com/android/tv/tuner/ts/TsParser.java @@ -102,6 +102,7 @@ public class TsParser { } protected abstract void handleData(byte[] data, boolean startIndicator); + protected abstract void resetDataVersions(); } private class SectionStream extends Stream { @@ -138,6 +139,11 @@ public class TsParser { mSectionParser.parseSections(mPacket); } + @Override + protected void resetDataVersions() { + mSectionParser.resetVersionNumbers(); + } + private final OutputListener mSectionListener = new OutputListener() { @Override public void onPatParsed(List items) { @@ -451,4 +457,16 @@ public class TsParser { } return incompleteChannels; } + + /** + * Reset the versions so that data with old version number can be handled. + */ + public void resetDataVersions() { + for (int eitPid : mEITPids) { + Stream stream = mStreamMap.get(eitPid); + if (stream != null) { + stream.resetDataVersions(); + } + } + } } diff --git a/src/com/android/tv/tuner/tvinput/ChannelDataManager.java b/src/com/android/tv/tuner/tvinput/ChannelDataManager.java index a16bc522..885cef9f 100644 --- a/src/com/android/tv/tuner/tvinput/ChannelDataManager.java +++ b/src/com/android/tv/tuner/tvinput/ChannelDataManager.java @@ -30,6 +30,7 @@ import android.os.HandlerThread; import android.os.Message; import android.os.RemoteException; import android.support.annotation.Nullable; +import android.support.v4.os.BuildCompat; import android.text.format.DateUtils; import android.util.Log; @@ -37,6 +38,7 @@ import com.android.tv.tuner.TunerPreferences; import com.android.tv.tuner.data.PsipData.EitItem; import com.android.tv.tuner.data.TunerChannel; import com.android.tv.tuner.util.ConvertUtils; +import com.android.tv.util.PermissionUtils; import java.util.ArrayList; import java.util.Collections; @@ -192,11 +194,14 @@ public class ChannelDataManager implements Handler.Callback { public void release() { mHandler.removeCallbacksAndMessages(null); - mHandlerThread.quitSafely(); + releaseSafely(); } public void releaseSafely() { mHandlerThread.quitSafely(); + mListener = null; + mChannelScanListener = null; + mChannelScanHandler = null; } public TunerChannel getChannel(long channelId) { @@ -435,7 +440,7 @@ public class ChannelDataManager implements Handler.Callback { } } ops.add(buildContentProviderOperation(ContentProviderOperation.newInsert( - TvContract.Programs.CONTENT_URI), newItem, channel.getChannelId())); + TvContract.Programs.CONTENT_URI), newItem, channel)); if (ops.size() >= BATCH_OPERATION_COUNT) { applyBatch(channel.getName(), ops); ops.clear(); @@ -505,7 +510,7 @@ public class ChannelDataManager implements Handler.Callback { continue; } ops.add(buildContentProviderOperation(ContentProviderOperation.newInsert( - TvContract.Programs.CONTENT_URI), item, channel.getChannelId())); + TvContract.Programs.CONTENT_URI), item, channel)); if (ops.size() >= BATCH_OPERATION_COUNT) { applyBatch(channel.getName(), ops); ops.clear(); @@ -516,9 +521,13 @@ public class ChannelDataManager implements Handler.Callback { } private ContentProviderOperation buildContentProviderOperation( - ContentProviderOperation.Builder builder, EitItem item, Long channelId) { - if (channelId != null) { - builder.withValue(TvContract.Programs.COLUMN_CHANNEL_ID, channelId); + ContentProviderOperation.Builder builder, EitItem item, TunerChannel channel) { + if (channel != null) { + builder.withValue(TvContract.Programs.COLUMN_CHANNEL_ID, channel.getChannelId()); + if (BuildCompat.isAtLeastN()) { + builder.withValue(TvContract.Programs.COLUMN_RECORDING_PROHIBITED, + channel.isRecordingProhibited() ? 1 : 0); + } } if (item != null) { builder.withValue(TvContract.Programs.COLUMN_TITLE, item.getTitleText()) @@ -556,7 +565,10 @@ public class ChannelDataManager implements Handler.Callback { values.put(TvContract.Channels.COLUMN_DISPLAY_NAME, channel.getName()); values.put(TvContract.Channels.COLUMN_INTERNAL_PROVIDER_DATA, channel.toByteArray()); values.put(TvContract.Channels.COLUMN_DESCRIPTION, channel.getDescription()); + values.put(TvContract.Channels.COLUMN_VIDEO_FORMAT, channel.getVideoFormat()); values.put(TvContract.Channels.COLUMN_INTERNAL_PROVIDER_FLAG1, VERSION); + values.put(TvContract.Channels.COLUMN_INTERNAL_PROVIDER_FLAG2, + channel.isRecordingProhibited() ? 1 : 0); if (channelId <= 0) { values.put(TvContract.Channels.COLUMN_INPUT_ID, mInputId); @@ -598,13 +610,29 @@ public class ChannelDataManager implements Handler.Callback { } private void checkVersion() { - String selection = TvContract.Channels.COLUMN_INTERNAL_PROVIDER_FLAG1 + "<>?"; - try (Cursor cursor = mContext.getContentResolver().query(mChannelsUri, - CHANNEL_DATA_SELECTION_ARGS, selection, - new String[] {Integer.toString(VERSION)}, null)) { - if (cursor != null && cursor.moveToFirst()) { - // The stored channel data seem outdated. Delete them all. - clearChannels(); + if (PermissionUtils.hasAccessAllEpg(mContext)) { + String selection = TvContract.Channels.COLUMN_INTERNAL_PROVIDER_FLAG1 + "<>?"; + try (Cursor cursor = mContext.getContentResolver().query(mChannelsUri, + CHANNEL_DATA_SELECTION_ARGS, selection, + new String[] {Integer.toString(VERSION)}, null)) { + if (cursor != null && cursor.moveToFirst()) { + // The stored channel data seem outdated. Delete them all. + clearChannels(); + } + } + } else { + try (Cursor cursor = mContext.getContentResolver().query(mChannelsUri, + new String[] { TvContract.Channels.COLUMN_INTERNAL_PROVIDER_FLAG1 }, + null, null, null)) { + if (cursor != null) { + while (cursor.moveToNext()) { + int version = cursor.getInt(0); + if (version != VERSION) { + clearChannels(); + break; + } + } + } } } } diff --git a/src/com/android/tv/tuner/tvinput/EventDetector.java b/src/com/android/tv/tuner/tvinput/EventDetector.java index 27bbb8c7..96b20a4b 100644 --- a/src/com/android/tv/tuner/tvinput/EventDetector.java +++ b/src/com/android/tv/tuner/tvinput/EventDetector.java @@ -51,7 +51,7 @@ public class EventDetector { private final SparseArray mChannelMap = new SparseArray<>(); private final SparseBooleanArray mVctCaptionTracksFound = new SparseBooleanArray(); private final SparseBooleanArray mEitCaptionTracksFound = new SparseBooleanArray(); - private final EventListener mEventListener; + private final List mEventListeners = new ArrayList<>(); private int mFrequency; private String mModulation; private int mProgramNumber = ALL_PROGRAM_NUMBERS; @@ -105,8 +105,10 @@ public class EventDetector { item.setHasCaptionTrack(); } } - if (tunerChannel != null && mEventListener != null) { - mEventListener.onEventDetected(tunerChannel, items); + if (tunerChannel != null && !mEventListeners.isEmpty()) { + for (EventListener eventListener : mEventListeners) { + eventListener.onEventDetected(tunerChannel, items); + } } } @@ -117,8 +119,10 @@ public class EventDetector { @Override public void onAllVctItemsParsed() { - if (mEventListener != null) { - mEventListener.onChannelScanDone(); + if (!mEventListeners.isEmpty()) { + for (EventListener eventListener : mEventListeners) { + eventListener.onChannelScanDone(); + } } } @@ -161,8 +165,10 @@ public class EventDetector { if (!found) { mVctProgramNumberSet.add(channelProgramNumber); } - if (mEventListener != null) { - mEventListener.onChannelDetected(tunerChannel, !found); + if (!mEventListeners.isEmpty()) { + for (EventListener eventListener : mEventListeners) { + eventListener.onChannelDetected(tunerChannel, !found); + } } } }; @@ -197,11 +203,9 @@ public class EventDetector { /** * Creates a detector for ATSC TV channles and program information. * @param usbTunerInteface {@link TunerHal} - * @param listener for ATSC TV channels and program information */ - public EventDetector(TunerHal usbTunerInteface, EventListener listener) { + public EventDetector(TunerHal usbTunerInteface) { mTunerHal = usbTunerInteface; - mEventListener = listener; } private void reset() { @@ -258,4 +262,28 @@ public class EventDetector { public List getMalFormedChannels() { return mTsParser.getMalFormedChannels(); } + + /** + * Registers an EventListener. + * @param eventListener the listener to be registered + */ + public void registerListener(EventListener eventListener) { + if (mTsParser != null) { + // Resets the version numbers so that the new listener can receive the EIT items. + // Otherwise, each EIT session is handled only once unless there is a new version. + mTsParser.resetDataVersions(); + } + mEventListeners.add(eventListener); + } + + /** + * Unregisters an EventListener. + * @param eventListener the listener to be unregistered + */ + public void unregisterListener(EventListener eventListener) { + boolean removed = mEventListeners.remove(eventListener); + if (!removed && DEBUG) { + Log.d(TAG, "Cannot unregister a non-registered listener!"); + } + } } diff --git a/src/com/android/tv/tuner/tvinput/TunerRecordingSessionWorker.java b/src/com/android/tv/tuner/tvinput/TunerRecordingSessionWorker.java index 6ec55e4f..0be29f25 100644 --- a/src/com/android/tv/tuner/tvinput/TunerRecordingSessionWorker.java +++ b/src/com/android/tv/tuner/tvinput/TunerRecordingSessionWorker.java @@ -33,13 +33,17 @@ import android.support.annotation.MainThread; import android.support.annotation.Nullable; import android.util.Log; +import android.util.Pair; +import com.google.android.exoplayer.C; import com.android.tv.TvApplication; import com.android.tv.common.SoftPreconditions; import com.android.tv.common.recording.RecordingCapability; import com.android.tv.dvr.DvrStorageStatusManager; -import com.android.tv.dvr.RecordedProgram; +import com.android.tv.dvr.data.RecordedProgram; import com.android.tv.tuner.DvbDeviceAccessor; import com.android.tv.tuner.data.PsipData; +import com.android.tv.tuner.data.PsipData.EitItem; +import com.android.tv.tuner.data.Track.AtscCaptionTrack; import com.android.tv.tuner.data.TunerChannel; import com.android.tv.tuner.exoplayer.ExoPlayerSampleExtractor; import com.android.tv.tuner.exoplayer.SampleExtractor; @@ -53,10 +57,10 @@ import java.io.File; import java.io.IOException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Random; -import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; /** @@ -71,6 +75,7 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener, private static final String SORT_BY_TIME = TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS + ", " + TvContract.Programs.COLUMN_CHANNEL_ID + ", " + TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS; + private static final long TUNING_RETRY_INTERVAL_MS = TimeUnit.SECONDS.toMillis(4); private static final long STORAGE_MONITOR_INTERVAL_MS = TimeUnit.SECONDS.toMillis(4); private static final long MIN_PARTIAL_RECORDING_DURATION_MS = TimeUnit.SECONDS.toMillis(10); private static final long PREPARE_RECORDER_POLL_MS = 50; @@ -80,20 +85,23 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener, private static final int MSG_STOP_RECORDING = 4; private static final int MSG_MONITOR_STORAGE_STATUS = 5; private static final int MSG_RELEASE = 6; + private static final int MSG_UPDATE_CC_INFO = 7; private final RecordingCapability mCapabilities; public RecordingCapability getCapabilities() { return mCapabilities; } - @IntDef({STATE_IDLE, STATE_TUNED, STATE_RECORDING}) + @IntDef({STATE_IDLE, STATE_TUNING, STATE_TUNED, STATE_RECORDING}) @Retention(RetentionPolicy.SOURCE) public @interface DvrSessionState {} private static final int STATE_IDLE = 1; - private static final int STATE_TUNED = 2; - private static final int STATE_RECORDING = 3; + private static final int STATE_TUNING = 2; + private static final int STATE_TUNED = 3; + private static final int STATE_RECORDING = 4; private static final long CHANNEL_ID_NONE = -1; + private static final int MAX_TUNING_RETRY = 6; private final Context mContext; private final ChannelDataManager mChannelDataManager; @@ -108,13 +116,16 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener, private long mRecordStartTime; private long mRecordEndTime; private boolean mRecorderRunning; - private BufferManager mBufferManager; private SampleExtractor mRecorder; private final TunerRecordingSession mSession; @DvrSessionState private int mSessionState = STATE_IDLE; private final String mInputId; private Uri mProgramUri; + private PsipData.EitItem mCurrenProgram; + private List mCaptionTracks; + private DvrStorageManager mDvrStorageManager; + public TunerRecordingSessionWorker(Context context, String inputId, ChannelDataManager dataManager, TunerRecordingSession session) { mRandom.setSeed(System.nanoTime()); @@ -157,6 +168,7 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener, if (mChannel == null || mChannel.compareTo(channel) != 0) { return; } + mHandler.obtainMessage(MSG_UPDATE_CC_INFO, new Pair<>(channel, items)).sendToTarget(); mChannelDataManager.notifyEventDetected(channel, items); } @@ -178,7 +190,7 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener, @MainThread public void tune(Uri channelUri) { mHandler.removeCallbacksAndMessages(null); - mHandler.obtainMessage(MSG_TUNE, channelUri).sendToTarget(); + mHandler.obtainMessage(MSG_TUNE, 0, 0, channelUri).sendToTarget(); } /** @@ -211,11 +223,22 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener, switch (msg.what) { case MSG_TUNE: { Uri channelUri = (Uri) msg.obj; + int retryCount = msg.arg1; if (DEBUG) Log.d(TAG, "Tune to " + channelUri); if (doTune(channelUri)) { - mSession.onTuned(channelUri); - } else { - reset(); + if (mSessionState == STATE_TUNED) { + mSession.onTuned(channelUri); + } else { + Log.w(TAG, "Tuner stream cannot be created due to resource shortage."); + if (retryCount < MAX_TUNING_RETRY) { + Message tuneMsg = + mHandler.obtainMessage(MSG_TUNE, retryCount + 1, 0, channelUri); + mHandler.sendMessageDelayed(tuneMsg, TUNING_RETRY_INTERVAL_MS); + } else { + mSession.onError(TvInputManager.RECORDING_ERROR_RESOURCE_BUSY); + reset(); + } + } } return true; } @@ -281,6 +304,12 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener, mHandler.getLooper().quitSafely(); return true; } + case MSG_UPDATE_CC_INFO: { + Pair> pair = + (Pair>) msg.obj; + updateCaptionTracks(pair.first, pair.second); + return true; + } } return false; } @@ -310,20 +339,17 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener, mRecorder.release(); mRecorder = null; } - if (mBufferManager != null) { - mBufferManager.close(); - mBufferManager = null; - } if (mTunerSource != null) { mSourceManager.releaseDataSource(mTunerSource); mTunerSource = null; } + mDvrStorageManager = null; mSessionState = STATE_IDLE; mRecorderRunning = false; } private boolean doTune(Uri channelUri) { - if (mSessionState != STATE_IDLE) { + if (mSessionState != STATE_IDLE && mSessionState != STATE_TUNING) { mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN); Log.e(TAG, "Tuning was requested from wrong status."); return false; @@ -333,6 +359,10 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener, mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN); Log.w(TAG, "Failed to start recording. Couldn't find the channel for " + mChannel); return false; + } else if (mChannel.isRecordingProhibited()) { + mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN); + Log.w(TAG, "Failed to start recording. Not a recordable channel: " + mChannel); + return false; } if (!mDvrStorageStatusManager.isStorageSufficient()) { mSession.onError(TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE); @@ -341,9 +371,9 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener, } mTunerSource = mSourceManager.createDataSource(mContext, mChannel, this); if (mTunerSource == null) { - mSession.onError(TvInputManager.RECORDING_ERROR_RESOURCE_BUSY); - Log.w(TAG, "Tuner stream cannot be created due to resource shortage."); - return false; + // Retry tuning in this case. + mSessionState = STATE_TUNING; + return true; } mSessionState = STATE_TUNED; return true; @@ -365,10 +395,10 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener, } // Since tuning might be happened a while ago, shifts the start position of tuned source. mTunerSource.shiftStartPosition(mTunerSource.getBufferedPosition()); - mBufferManager = new BufferManager(new DvrStorageManager(mStorageDir, true)); mRecordStartTime = System.currentTimeMillis(); - mRecorder = new ExoPlayerSampleExtractor(Uri.EMPTY, mTunerSource, mBufferManager, this, - true); + mDvrStorageManager = new DvrStorageManager(mStorageDir, true); + mRecorder = new ExoPlayerSampleExtractor(Uri.EMPTY, mTunerSource, + new BufferManager(mDvrStorageManager), this, true); mRecorder.setOnCompletionListener(this, mHandler); mProgramUri = programUri; mSessionState = STATE_RECORDING; @@ -392,6 +422,34 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener, Log.i(TAG, "Recording stopped"); } + private void updateCaptionTracks(TunerChannel channel, List items) { + if (mChannel == null || channel == null || mChannel.compareTo(channel) != 0 + || items == null || items.isEmpty()) { + return; + } + PsipData.EitItem currentProgram = getCurrentProgram(items); + if (currentProgram == null || !currentProgram.hasCaptionTrack() + || mCurrenProgram != null && mCurrenProgram.compareTo(currentProgram) == 0) { + return; + } + mCurrenProgram = currentProgram; + mCaptionTracks = new ArrayList<>(currentProgram.getCaptionTracks()); + if (DEBUG) { + Log.d(TAG, "updated " + mCaptionTracks.size() + " caption tracks for " + + currentProgram); + } + } + + private PsipData.EitItem getCurrentProgram(List items) { + for (PsipData.EitItem item : items) { + if (mRecordStartTime >= item.getStartTimeUtcMillis() + && mRecordStartTime < item.getEndTimeUtcMillis()) { + return item; + } + } + return null; + } + private static class Program { private final long mChannelId; private final String mTitle; @@ -566,15 +624,25 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener, return; } Log.i(TAG, "recording finished " + (success ? "completely" : "partially")); - Uri uri = insertRecordedProgram(getRecordedProgram(), mChannel.getChannelId(), - Uri.fromFile(mStorageDir).toString(), 1024 * 1024, mRecordStartTime, - mRecordStartTime + TimeUnit.MICROSECONDS.toMillis(lastExtractedPositionUs)); + long recordEndTime = + (lastExtractedPositionUs == C.UNKNOWN_TIME_US) + ? System.currentTimeMillis() + : mRecordStartTime + lastExtractedPositionUs / 1000; + Uri uri = + insertRecordedProgram( + getRecordedProgram(), + mChannel.getChannelId(), + Uri.fromFile(mStorageDir).toString(), + 1024 * 1024, + mRecordStartTime, + recordEndTime); if (uri == null) { new DeleteRecordingTask().execute(mStorageDir); mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN); Log.e(TAG, "Inserting a recording to DB failed"); return; } + mDvrStorageManager.writeCaptionInfoFiles(mCaptionTracks); mSession.onRecordFinished(uri); } diff --git a/src/com/android/tv/tuner/tvinput/TunerSession.java b/src/com/android/tv/tuner/tvinput/TunerSession.java index abfd2b30..d1ee3c6f 100644 --- a/src/com/android/tv/tuner/tvinput/TunerSession.java +++ b/src/com/android/tv/tuner/tvinput/TunerSession.java @@ -81,8 +81,7 @@ public class TunerSession extends TvInputService.Session implements Handler.Call private boolean mPlayPaused; private long mTuneStartTimestamp; - public TunerSession(Context context, ChannelDataManager channelDataManager, - BufferManager bufferManager) { + public TunerSession(Context context, ChannelDataManager channelDataManager) { super(context); mContext = context; mUiHandler = new Handler(this); @@ -97,12 +96,9 @@ public class TunerSession extends TvInputService.Session implements Handler.Call mStatusView.setVisibility(showDebug ? View.VISIBLE : View.INVISIBLE); mAudioStatusView = (TextView) mOverlayView.findViewById(R.id.audio_status); mAudioStatusView.setVisibility(View.INVISIBLE); - mAudioStatusView.setText(Html.fromHtml(StatusTextUtils.getAudioWarningInHTML( - context.getString(R.string.ut_surround_sound_disabled)))); CaptionLayout captionLayout = (CaptionLayout) mOverlayView.findViewById(R.id.caption); mCaptionTrackRenderer = new CaptionTrackRenderer(captionLayout); - mSessionWorker = new TunerSessionWorker(context, channelDataManager, - bufferManager, this); + mSessionWorker = new TunerSessionWorker(context, channelDataManager, this); } public boolean isReleased() { @@ -272,10 +268,13 @@ public class TunerSession extends TvInputService.Session implements Handler.Call // setting is "never". final int value = GlobalSettingsUtils.getEncodedSurroundOutputSettings(mContext); if (value == GlobalSettingsUtils.ENCODED_SURROUND_OUTPUT_NEVER) { - mAudioStatusView.setVisibility(View.VISIBLE); + mAudioStatusView.setText(Html.fromHtml(StatusTextUtils.getAudioWarningInHTML( + mContext.getString(R.string.ut_surround_sound_disabled)))); } else { - Log.e(TAG, "Audio is unavailable, surround sound setting is " + value); + mAudioStatusView.setText(Html.fromHtml(StatusTextUtils.getAudioWarningInHTML( + mContext.getString(R.string.audio_passthrough_not_supported)))); } + mAudioStatusView.setVisibility(View.VISIBLE); return true; } case MSG_UI_HIDE_AUDIO_UNPLAYABLE: { diff --git a/src/com/android/tv/tuner/tvinput/TunerSessionWorker.java b/src/com/android/tv/tuner/tvinput/TunerSessionWorker.java index c0a613a4..41f8ce5f 100644 --- a/src/com/android/tv/tuner/tvinput/TunerSessionWorker.java +++ b/src/com/android/tv/tuner/tvinput/TunerSessionWorker.java @@ -35,6 +35,7 @@ import android.support.annotation.AnyThread; import android.support.annotation.MainThread; import android.support.annotation.WorkerThread; import android.text.Html; +import android.text.TextUtils; import android.util.Log; import android.util.Pair; import android.util.SparseArray; @@ -55,20 +56,22 @@ import com.android.tv.tuner.data.Track.AtscCaptionTrack; import com.android.tv.tuner.data.TunerChannel; import com.android.tv.tuner.exoplayer.MpegTsRendererBuilder; import com.android.tv.tuner.exoplayer.buffer.BufferManager; +import com.android.tv.tuner.exoplayer.buffer.BufferManager.StorageManager; import com.android.tv.tuner.exoplayer.buffer.DvrStorageManager; import com.android.tv.tuner.exoplayer.MpegTsPlayer; +import com.android.tv.tuner.exoplayer.buffer.TrickplayStorageManager; import com.android.tv.tuner.source.TsDataSource; import com.android.tv.tuner.source.TsDataSourceManager; import com.android.tv.tuner.util.StatusTextUtils; +import com.android.tv.tuner.util.SystemPropertiesProxy; import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Objects; -import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; /** * {@link TunerSessionWorker} implements a handler thread which processes TV input jobs @@ -82,6 +85,9 @@ public class TunerSessionWorker implements PlaybackBufferListener, private static final boolean DEBUG = false; private static final boolean ENABLE_PROFILER = true; private static final String PLAY_FROM_CHANNEL = "channel"; + private static final String MAX_BUFFER_SIZE_KEY = "tv.tuner.buffersize_mbytes"; + private static final int MAX_BUFFER_SIZE_DEF = 2 * 1024; // 2GB + private static final int MIN_BUFFER_SIZE_DEF = 256; // 256MB // Public messages public static final int MSG_SELECT_TRACK = 1; @@ -147,10 +153,18 @@ public class TunerSessionWorker implements PlaybackBufferListener, private static final int EXPECTED_KEY_FRAME_INTERVAL_MS = 500; private static final int MIN_TRICKPLAY_SEEK_INTERVAL_MS = 20; private static final int TRICKPLAY_MONITOR_INTERVAL_MS = 250; + private static final int RELEASE_WAIT_INTERVAL_MS = 50; + + // Since release() is done asynchronously, synchronization between multiple TunerSessionWorker + // creation/release is required. + // This is used to guarantee that at most one active TunerSessionWorker exists at any give time. + private static Semaphore sActiveSessionSemaphore = new Semaphore(1); private final Context mContext; private final ChannelDataManager mChannelDataManager; private final TsDataSourceManager mSourceManager; + private final int mMaxTrickplayBufferSizeMb; + private final File mTrickplayBufferDir; private volatile Surface mSurface; private volatile float mVolume = 1.0f; private volatile boolean mCaptionEnabled; @@ -159,6 +173,7 @@ public class TunerSessionWorker implements PlaybackBufferListener, private volatile Long mRecordingDuration; private volatile long mRecordStartTimeMs; private volatile long mBufferStartTimeMs; + private volatile boolean mTrickplayDisabled; private String mRecordingId; private final Handler mHandler; private int mRetryCount; @@ -177,19 +192,19 @@ public class TunerSessionWorker implements PlaybackBufferListener, private TvContentRating mUnblockedContentRating; private long mLastPositionMs; private AudioCapabilities mAudioCapabilities; - private final CountDownLatch mReleaseLatch = new CountDownLatch(1); private long mLastLimitInBytes; - private long mLastPositionInBytes; - private final BufferManager mBufferManager; private final TvContentRatingCache mTvContentRatingCache = TvContentRatingCache.getInstance(); private final TunerSession mSession; private int mPlayerState = ExoPlayer.STATE_IDLE; private long mPreparingStartTimeMs; private long mBufferingStartTimeMs; private long mReadyStartTimeMs; + private boolean mIsActiveSession; + private boolean mReleaseRequested; // Guarded by mReleaseLock + private final Object mReleaseLock = new Object(); public TunerSessionWorker(Context context, ChannelDataManager channelDataManager, - BufferManager bufferManager, TunerSession tunerSession) { + TunerSession tunerSession) { if (DEBUG) Log.d(TAG, "TunerSessionWorker created"); mContext = context; @@ -211,7 +226,10 @@ public class TunerSessionWorker implements PlaybackBufferListener, (CaptioningManager) context.getSystemService(Context.CAPTIONING_SERVICE); mCaptionEnabled = captioningManager.isEnabled(); mPlaybackParams.setSpeed(1.0f); - mBufferManager = bufferManager; + mMaxTrickplayBufferSizeMb = + SystemPropertiesProxy.getInt(MAX_BUFFER_SIZE_KEY, MAX_BUFFER_SIZE_DEF); + mTrickplayBufferDir = context.getCacheDir(); + mTrickplayDisabled = mTrickplayBufferDir == null; mPreparingStartTimeMs = INVALID_TIME; mBufferingStartTimeMs = INVALID_TIME; mReadyStartTimeMs = INVALID_TIME; @@ -285,24 +303,21 @@ public class TunerSessionWorker implements PlaybackBufferListener, } private Long getDurationForRecording(String recordingId) { - try { - DvrStorageManager storageManager = + DvrStorageManager storageManager = new DvrStorageManager(new File(getRecordingPath()), false); - Pair trackInfo = null; - try { - trackInfo = storageManager.readTrackInfoFile(false); - } catch (FileNotFoundException e) { - } - if (trackInfo == null) { - trackInfo = storageManager.readTrackInfoFile(true); - } - Long durationUs = trackInfo.second.getLong(MediaFormat.KEY_DURATION); + List trackFormatList = + storageManager.readTrackInfoFiles(false); + if (trackFormatList.isEmpty()) { + trackFormatList = storageManager.readTrackInfoFiles(true); + } + if (!trackFormatList.isEmpty()) { + BufferManager.TrackFormat trackFormat = trackFormatList.get(0); + Long durationUs = trackFormat.format.getLong(MediaFormat.KEY_DURATION); // we need duration by milli for trickplay notification. return durationUs != null ? durationUs / 1000 : null; - } catch (IOException e) { - Log.e(TAG, "meta file for recording was not found: " + recordingId); - return null; } + Log.e(TAG, "meta file for recording was not found: " + recordingId); + return null; } @MainThread @@ -341,16 +356,12 @@ public class TunerSessionWorker implements PlaybackBufferListener, @MainThread public void release() { if (DEBUG) Log.d(TAG, "release()"); + synchronized (mReleaseLock) { + mReleaseRequested = true; + } mChannelDataManager.setListener(null); mHandler.removeCallbacksAndMessages(null); mHandler.sendEmptyMessage(MSG_RELEASE); - try { - mReleaseLatch.await(); - } catch (InterruptedException e) { - Log.e(TAG, "Couldn't wait for finish of MSG_RELEASE", e); - } finally { - mHandler.getLooper().quitSafely(); - } } // MpegTsPlayer.Listener @@ -367,7 +378,7 @@ public class TunerSessionWorker implements PlaybackBufferListener, if (playbackState == ExoPlayer.STATE_READY) { if (DEBUG) Log.d(TAG, "ExoPlayer ready"); if (!mPlayerStarted) { - sendMessage(MSG_START_PLAYBACK, mPlayer); + sendMessage(MSG_START_PLAYBACK, System.identityHashCode(mPlayer)); } mReadyStartTimeMs = SystemClock.elapsedRealtime(); } else if (playbackState == ExoPlayer.STATE_PREPARING) { @@ -379,7 +390,7 @@ public class TunerSessionWorker implements PlaybackBufferListener, // notification of STATE_ENDED from MpegTsPlayer will be ignored afterwards. Log.i(TAG, "Player ended: end of stream"); if (mChannel != null) { - sendMessage(MSG_RETRY_PLAYBACK, mPlayer); + sendMessage(MSG_RETRY_PLAYBACK, System.identityHashCode(mPlayer)); } } mPlayerState = playbackState; @@ -397,7 +408,8 @@ public class TunerSessionWorker implements PlaybackBufferListener, // If we are playing live stream, retrying playback maybe helpful. But for recorded stream, // retrying playback is not helpful. if (mChannel != null) { - mHandler.obtainMessage(MSG_RETRY_PLAYBACK, mPlayer).sendToTarget(); + mHandler.obtainMessage(MSG_RETRY_PLAYBACK, System.identityHashCode(mPlayer)) + .sendToTarget(); } } @@ -415,8 +427,12 @@ public class TunerSessionWorker implements PlaybackBufferListener, public void onDrawnToSurface(MpegTsPlayer player, Surface surface) { if (mSurface != null && mPlayerStarted) { if (DEBUG) Log.d(TAG, "MSG_DRAWN_TO_SURFACE"); - mBufferStartTimeMs = mRecordStartTimeMs = - (mRecordingId != null) ? 0 : System.currentTimeMillis(); + if (mRecordingId != null) { + // Workaround of b/33298048: set it to 1 instead of 0. + mBufferStartTimeMs = mRecordStartTimeMs = 1; + } else { + mBufferStartTimeMs = mRecordStartTimeMs = System.currentTimeMillis(); + } notifyVideoAvailable(); mReportedDrawnToSurface = true; @@ -499,7 +515,8 @@ public class TunerSessionWorker implements PlaybackBufferListener, @Override public void onDiskTooSlow() { - sendMessage(MSG_RETRY_PLAYBACK, mPlayer); + mTrickplayDisabled = true; + sendMessage(MSG_RETRY_PLAYBACK, System.identityHashCode(mPlayer)); } // EventDetector.EventListener @@ -602,6 +619,28 @@ public class TunerSessionWorker implements PlaybackBufferListener, return true; } notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING); + if (!mIsActiveSession) { + // Wait until release is finished if there is a pending release. + try { + while (!sActiveSessionSemaphore.tryAcquire( + RELEASE_WAIT_INTERVAL_MS, TimeUnit.MILLISECONDS)) { + synchronized (mReleaseLock) { + if (mReleaseRequested) { + return true; + } + } + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + synchronized (mReleaseLock) { + if (mReleaseRequested) { + sActiveSessionSemaphore.release(); + return true; + } + } + mIsActiveSession = true; + } Uri channelUri = (Uri) msg.obj; String recording = null; long channelId = parseChannel(channelUri); @@ -616,7 +655,8 @@ public class TunerSessionWorker implements PlaybackBufferListener, notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN); return true; } - mHandler.removeCallbacksAndMessages(null); + clearCallbacksAndMessagesSafely(); + mChannelDataManager.removeAllCallbacksAndMessages(); if (channel != null) { mChannelDataManager.requestProgramsData(channel); } @@ -624,8 +664,8 @@ public class TunerSessionWorker implements PlaybackBufferListener, // TODO: Need to refactor. notifyContentAllowed() should not be called if parental // control is turned on. mSession.notifyContentAllowed(); - resetPlayback(); resetTvTracks(); + resetPlayback(); mHandler.sendEmptyMessageDelayed(MSG_RESCHEDULE_PROGRAMS, RESCHEDULE_PROGRAMS_INITIAL_DELAY_MS); return true; @@ -633,7 +673,7 @@ public class TunerSessionWorker implements PlaybackBufferListener, case MSG_STOP_TUNE: { if (DEBUG) Log.d(TAG, "MSG_STOP_TUNE"); mChannel = null; - stopPlayback(); + stopPlayback(true); stopCaptionTrack(); resetTvTracks(); notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN); @@ -642,14 +682,17 @@ public class TunerSessionWorker implements PlaybackBufferListener, case MSG_RELEASE: { if (DEBUG) Log.d(TAG, "MSG_RELEASE"); mHandler.removeCallbacksAndMessages(null); - stopPlayback(); + stopPlayback(true); stopCaptionTrack(); mSourceManager.release(); - mReleaseLatch.countDown(); + mHandler.getLooper().quitSafely(); + if (mIsActiveSession) { + sActiveSessionSemaphore.release(); + } return true; } case MSG_RETRY_PLAYBACK: { - if (mPlayer == msg.obj) { + if (System.identityHashCode(mPlayer) == (int) msg.obj) { Log.i(TAG, "Retrying the playback for channel: " + mChannel); mHandler.removeMessages(MSG_RETRY_PLAYBACK); // When there is a request of retrying playback, don't reuse TunerHal. @@ -658,13 +701,14 @@ public class TunerSessionWorker implements PlaybackBufferListener, if (DEBUG) { Log.d(TAG, "MSG_RETRY_PLAYBACK " + mRetryCount); } + mChannelDataManager.removeAllCallbacksAndMessages(); if (mRetryCount <= MAX_IMMEDIATE_RETRY_COUNT) { resetPlayback(); } else { // When it reaches this point, it may be due to an error that occurred in // the tuner device. Calling stopPlayback() resets the tuner device // to recover from the error. - stopPlayback(); + stopPlayback(false); stopCaptionTrack(); notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL); @@ -679,13 +723,14 @@ public class TunerSessionWorker implements PlaybackBufferListener, } case MSG_RESET_PLAYBACK: { if (DEBUG) Log.d(TAG, "MSG_RESET_PLAYBACK"); + mChannelDataManager.removeAllCallbacksAndMessages(); resetPlayback(); return true; } case MSG_START_PLAYBACK: { if (DEBUG) Log.d(TAG, "MSG_START_PLAYBACK"); if (mChannel != null || mRecordingId != null) { - startPlayback(msg.obj); + startPlayback((int) msg.obj); } return true; } @@ -790,7 +835,11 @@ public class TunerSessionWorker implements PlaybackBufferListener, return true; } case MSG_RESCHEDULE_PROGRAMS: { - doReschedulePrograms(); + if (mHandler.hasMessages(MSG_SCHEDULE_OF_PROGRAMS)) { + mHandler.sendEmptyMessage(MSG_RESCHEDULE_PROGRAMS); + } else { + doReschedulePrograms(); + } return true; } case MSG_PARENTAL_CONTROLS: { @@ -814,11 +863,8 @@ public class TunerSessionWorker implements PlaybackBufferListener, return true; } case MSG_SELECT_TRACK: { - if (mChannel != null) { + if (mChannel != null || mRecordingId != null) { doSelectTrack(msg.arg1, (String) msg.obj); - } else if (mRecordingId != null) { - // TODO : mChannel == null && mRecordingId != null - Log.d(TAG, "track selected for recording"); } return true; } @@ -909,7 +955,6 @@ public class TunerSessionWorker implements PlaybackBufferListener, } TsDataSource source = mPlayer.getDataSource(); long limitInBytes = source != null ? source.getBufferedPosition() : 0L; - long positionInBytes = source != null ? source.getLastReadPosition() : 0L; if (TunerDebug.ENABLED) { TunerDebug.calculateDiff(); mSession.sendUiMessage(TunerSession.MSG_UI_SET_STATUS_TEXT, @@ -927,14 +972,8 @@ public class TunerSessionWorker implements PlaybackBufferListener, TunerDebug.getVideoPtsUsRate() ))); } - if (DEBUG) { - Log.d(TAG, String.format("MSG_CHECK_SIGNAL position: %d, limit: %d", - positionInBytes, limitInBytes)); - } mSession.sendUiMessage(TunerSession.MSG_UI_HIDE_MESSAGE); long currentTime = SystemClock.elapsedRealtime(); - boolean noBufferRead = positionInBytes == mLastPositionInBytes - && limitInBytes == mLastLimitInBytes; boolean isBufferingTooLong = mBufferingStartTimeMs != INVALID_TIME && currentTime - mBufferingStartTimeMs > PLAYBACK_STATE_CHANGED_WAITING_THRESHOLD_MS; @@ -943,11 +982,11 @@ public class TunerSessionWorker implements PlaybackBufferListener, > PLAYBACK_STATE_CHANGED_WAITING_THRESHOLD_MS; boolean isWeakSignal = source != null && mChannel.getType() == Channel.TYPE_TUNER - && (noBufferRead || isBufferingTooLong || isPreparingTooLong); + && (isBufferingTooLong || isPreparingTooLong); if (isWeakSignal && !mReportedWeakSignal) { if (!mHandler.hasMessages(MSG_RETRY_PLAYBACK)) { - mHandler.sendMessageDelayed(mHandler.obtainMessage( - MSG_RETRY_PLAYBACK, mPlayer), PLAYBACK_RETRY_DELAY_MS); + mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_RETRY_PLAYBACK, + System.identityHashCode(mPlayer)), PLAYBACK_RETRY_DELAY_MS); } if (mPlayer != null) { mPlayer.setAudioTrack(false); @@ -966,7 +1005,6 @@ public class TunerSessionWorker implements PlaybackBufferListener, } } mLastLimitInBytes = limitInBytes; - mLastPositionInBytes = positionInBytes; mHandler.sendEmptyMessageDelayed(MSG_CHECK_SIGNAL, CHECK_NO_SIGNAL_PERIOD_MS); return true; } @@ -999,15 +1037,8 @@ public class TunerSessionWorker implements PlaybackBufferListener, if (trackId == null) { return; } - AtscAudioTrack audioTrack = mAudioTrackMap.get(numTrackId); - if (audioTrack == null) { - return; - } - int oldAudioPid = mChannel.getAudioPid(); - mChannel.selectAudioTrack(audioTrack.index); - int newAudioPid = mChannel.getAudioPid(); - if (oldAudioPid != newAudioPid) { - mPlayer.setSelectedTrack(MpegTsPlayer.TRACK_TYPE_AUDIO, audioTrack.index); + if (numTrackId != mPlayer.getSelectedTrack(MpegTsPlayer.TRACK_TYPE_AUDIO)) { + mPlayer.setSelectedTrack(MpegTsPlayer.TRACK_TYPE_AUDIO, numTrackId); } mSession.notifyTrackSelected(type, trackId); } else if (type == TvTrackInfo.TYPE_SUBTITLE) { @@ -1030,11 +1061,22 @@ public class TunerSessionWorker implements PlaybackBufferListener, } } - private MpegTsPlayer createPlayer(AudioCapabilities capabilities, BufferManager bufferManager) { + private MpegTsPlayer createPlayer(AudioCapabilities capabilities) { if (capabilities == null) { Log.w(TAG, "No Audio Capabilities"); } - + BufferManager bufferManager = null; + if (mRecordingId != null) { + StorageManager storageManager = + new DvrStorageManager(new File(getRecordingPath()), false); + bufferManager = new BufferManager(storageManager); + updateCaptionTracks(((DvrStorageManager)storageManager).readCaptionInfoFiles()); + } else if (!mTrickplayDisabled && mMaxTrickplayBufferSizeMb >= MIN_BUFFER_SIZE_DEF) { + bufferManager = new BufferManager(new TrickplayStorageManager(mContext, + mTrickplayBufferDir, 1024L * 1024 * mMaxTrickplayBufferSizeMb)); + } else { + Log.w(TAG, "Trickplay is disabled."); + } MpegTsPlayer player = new MpegTsPlayer( new MpegTsRendererBuilder(mContext, bufferManager, this), mHandler, mSourceManager, capabilities, this); @@ -1069,24 +1111,26 @@ public class TunerSessionWorker implements PlaybackBufferListener, } private void updateTvTracks(TvTracksInterface tvTracksInterface, boolean fromPmt) { - if (DEBUG) { - Log.d(TAG, "UpdateTvTracks " + tvTracksInterface); - } - List audioTracks = tvTracksInterface.getAudioTracks(); - List captionTracks = tvTracksInterface.getCaptionTracks(); - // According to ATSC A/69 chapter 6.9, both PMT and EIT should have descriptors for audio - // tracks, but in real world, we see some bogus audio track info in EIT, so, we trust audio - // track info in PMT more and use info in EIT only when we have nothing. - if (audioTracks != null && !audioTracks.isEmpty() - && (mChannel.getAudioTracks() == null || fromPmt)) { - updateAudioTracks(audioTracks); - } - if (captionTracks == null || captionTracks.isEmpty()) { - if (tvTracksInterface.hasCaptionTrack()) { + synchronized (tvTracksInterface) { + if (DEBUG) { + Log.d(TAG, "UpdateTvTracks " + tvTracksInterface); + } + List audioTracks = tvTracksInterface.getAudioTracks(); + List captionTracks = tvTracksInterface.getCaptionTracks(); + // According to ATSC A/69 chapter 6.9, both PMT and EIT should have descriptors for audio + // tracks, but in real world, we see some bogus audio track info in EIT, so, we trust audio + // track info in PMT more and use info in EIT only when we have nothing. + if (audioTracks != null && !audioTracks.isEmpty() + && (mChannel == null || mChannel.getAudioTracks() == null || fromPmt)) { + updateAudioTracks(audioTracks); + } + if (captionTracks == null || captionTracks.isEmpty()) { + if (tvTracksInterface.hasCaptionTrack()) { + updateCaptionTracks(captionTracks); + } + } else { updateCaptionTracks(captionTracks); } - } else { - updateCaptionTracks(captionTracks); } } @@ -1132,25 +1176,24 @@ public class TunerSessionWorker implements PlaybackBufferListener, int audioTrackCount = mPlayer.getTrackCount(MpegTsPlayer.TRACK_TYPE_AUDIO); removeTvTracks(TvTrackInfo.TYPE_AUDIO); for (int i = 0; i < audioTrackCount; i++) { - AtscAudioTrack audioTrack = mAudioTrackMap.get(i); - if (audioTrack == null) { - continue; - } - String language = audioTrack.language; - if (language == null && mChannel.getAudioTracks() != null - && mChannel.getAudioTracks().size() == mAudioTrackMap.size()) { - // If a language is not present, use a language field in PMT section parsed. - language = mChannel.getAudioTracks().get(i).language; - } - // Save the index to the audio track. - // Later, when an audio track is selected, both the audio pid and its audio stream - // type reside in the selected index position of the tuner channel's audio data. - audioTrack.index = i; + // We use language information from EIT/VCT only when the player does not provide + // languages. + com.google.android.exoplayer.MediaFormat infoFromPlayer = + mPlayer.getTrackFormat(MpegTsPlayer.TRACK_TYPE_AUDIO, i); + AtscAudioTrack infoFromEit = mAudioTrackMap.get(i); + AtscAudioTrack infoFromVct = (mChannel != null + && mChannel.getAudioTracks().size() == mAudioTrackMap.size() + && i < mChannel.getAudioTracks().size()) + ? mChannel.getAudioTracks().get(i) : null; + String language = !TextUtils.isEmpty(infoFromPlayer.language) ? infoFromPlayer.language + : (infoFromEit != null && infoFromEit.language != null) ? infoFromEit.language + : (infoFromVct != null && infoFromVct.language != null) + ? infoFromVct.language : null; TvTrackInfo.Builder builder = new TvTrackInfo.Builder( TvTrackInfo.TYPE_AUDIO, AUDIO_TRACK_PREFIX + i); builder.setLanguage(language); - builder.setAudioChannelCount(audioTrack.channelCount); - builder.setAudioSampleRate(audioTrack.sampleRate); + builder.setAudioChannelCount(infoFromPlayer.channelCount); + builder.setAudioSampleRate(infoFromPlayer.sampleRate); TvTrackInfo track = builder.build(); mTvTracks.add(track); } @@ -1226,8 +1269,10 @@ public class TunerSessionWorker implements PlaybackBufferListener, } } - private void stopPlayback() { - mChannelDataManager.removeAllCallbacksAndMessages(); + private void stopPlayback(boolean removeChannelDataCallbacks) { + if (removeChannelDataCallbacks) { + mChannelDataManager.removeAllCallbacksAndMessages(); + } if (mPlayer != null) { mPlayer.setPlayWhenReady(false); mPlayer.release(); @@ -1244,9 +1289,9 @@ public class TunerSessionWorker implements PlaybackBufferListener, } } - private void startPlayback(Object playerObj) { + private void startPlayback(int playerHashCode) { // TODO: provide hasAudio()/hasVideo() for play recordings. - if (mPlayer == null || mPlayer != playerObj) { + if (mPlayer == null || System.identityHashCode(mPlayer) != playerHashCode) { return; } if (mChannel != null && !mChannel.hasAudio()) { @@ -1259,7 +1304,7 @@ public class TunerSessionWorker implements PlaybackBufferListener, if (mChannel != null && ((mChannel.hasAudio() && !mPlayer.hasAudio()) || (mChannel.hasVideo() && !mPlayer.hasVideo()))) { // Tracks haven't been detected in the extractor. Try again. - sendMessage(MSG_RETRY_PLAYBACK, mPlayer); + sendMessage(MSG_RETRY_PLAYBACK, System.identityHashCode(mPlayer)); return; } // Since mSurface is volatile, we define a local variable surface to keep the same value @@ -1286,9 +1331,7 @@ public class TunerSessionWorker implements PlaybackBufferListener, return; } mSourceManager.setKeepTuneStatus(true); - BufferManager bufferManager = mChannel != null ? mBufferManager : new BufferManager( - new DvrStorageManager(new File(getRecordingPath()), false)); - MpegTsPlayer player = createPlayer(mAudioCapabilities, bufferManager); + MpegTsPlayer player = createPlayer(mAudioCapabilities); player.setCaptionServiceNumber(Cea708Data.EMPTY_SERVICE_NUMBER); player.setVideoEventListener(this); player.setCaptionServiceNumber(mCaptionTrack != null ? @@ -1300,8 +1343,8 @@ public class TunerSessionWorker implements PlaybackBufferListener, // When prepare failed, there may be some errors related to hardware. In that // case, retry playback immediately may not help. notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL); - mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_RETRY_PLAYBACK, mPlayer), - PLAYBACK_RETRY_DELAY_MS); + mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_RETRY_PLAYBACK, + System.identityHashCode(mPlayer)), PLAYBACK_RETRY_DELAY_MS); } } else { mPlayer = player; @@ -1314,7 +1357,7 @@ public class TunerSessionWorker implements PlaybackBufferListener, private void resetPlayback() { long timestamp, oldTimestamp; timestamp = SystemClock.elapsedRealtime(); - stopPlayback(); + stopPlayback(false); stopCaptionTrack(); if (ENABLE_PROFILER) { oldTimestamp = timestamp; @@ -1336,8 +1379,12 @@ public class TunerSessionWorker implements PlaybackBufferListener, mRecordingDuration = recording != null ? getDurationForRecording(recording) : null; mProgram = null; mPrograms = null; - mBufferStartTimeMs = mRecordStartTimeMs = - (mRecordingId != null) ? 0 : System.currentTimeMillis(); + if (mRecordingId != null) { + // Workaround of b/33298048: set it to 1 instead of 0. + mBufferStartTimeMs = mRecordStartTimeMs = 1; + } else { + mBufferStartTimeMs = mRecordStartTimeMs = System.currentTimeMillis(); + } mLastPositionMs = 0; mCaptionTrack = null; mHandler.sendEmptyMessage(MSG_PARENTAL_CONTROLS); @@ -1544,15 +1591,15 @@ public class TunerSessionWorker implements PlaybackBufferListener, } mChannelBlocked = channelBlocked; if (mChannelBlocked) { - mHandler.removeCallbacksAndMessages(null); - stopPlayback(); + clearCallbacksAndMessagesSafely(); + stopPlayback(true); resetTvTracks(); if (contentRating != null) { mSession.notifyContentBlocked(contentRating); } mHandler.sendEmptyMessageDelayed(MSG_PARENTAL_CONTROLS, PARENTAL_CONTROLS_INTERVAL_MS); } else { - mHandler.removeCallbacksAndMessages(null); + clearCallbacksAndMessagesSafely(); resetPlayback(); mSession.notifyContentAllowed(); mHandler.sendEmptyMessageDelayed(MSG_RESCHEDULE_PROGRAMS, @@ -1562,6 +1609,17 @@ public class TunerSessionWorker implements PlaybackBufferListener, } } + @WorkerThread + private void clearCallbacksAndMessagesSafely() { + // If MSG_RELEASE is removed, TunerSessionWorker will hang forever. + // Do not remove messages, after release is requested from MainThread. + synchronized (mReleaseLock) { + if (!mReleaseRequested) { + mHandler.removeCallbacksAndMessages(null); + } + } + } + private boolean hasEnoughBackwardBuffer() { return mPlayer.getCurrentPosition() + BUFFER_UNDERFLOW_BUFFER_MS >= mBufferStartTimeMs - mRecordStartTimeMs; diff --git a/src/com/android/tv/tuner/tvinput/TunerTvInputService.java b/src/com/android/tv/tuner/tvinput/TunerTvInputService.java index 684ebdbd..6594e089 100644 --- a/src/com/android/tv/tuner/tvinput/TunerTvInputService.java +++ b/src/com/android/tv/tuner/tvinput/TunerTvInputService.java @@ -28,9 +28,6 @@ import com.google.android.exoplayer.audio.AudioCapabilities; import com.google.android.exoplayer.audio.AudioCapabilitiesReceiver; import com.android.tv.TvApplication; import com.android.tv.common.feature.CommonFeatures; -import com.android.tv.tuner.exoplayer.buffer.BufferManager; -import com.android.tv.tuner.exoplayer.buffer.TrickplayStorageManager; -import com.android.tv.tuner.util.SystemPropertiesProxy; import java.util.Collections; import java.util.Set; @@ -45,9 +42,6 @@ public class TunerTvInputService extends TvInputService private static final String TAG = "TunerTvInputService"; private static final boolean DEBUG = false; - private static final String MAX_BUFFER_SIZE_KEY = "tv.tuner.buffersize_mbytes"; - private static final int MAX_BUFFER_SIZE_DEF = 2 * 1024; // 2GB - private static final int MIN_BUFFER_SIZE_DEF = 256; // 256MB private static final int DVR_STORAGE_CLEANUP_JOB_ID = 100; // WeakContainer for {@link TvInputSessionImpl} @@ -55,7 +49,6 @@ public class TunerTvInputService extends TvInputService private ChannelDataManager mChannelDataManager; private AudioCapabilitiesReceiver mAudioCapabilitiesReceiver; private AudioCapabilities mAudioCapabilities; - private BufferManager mBufferManager; @Override public void onCreate() { @@ -65,7 +58,6 @@ public class TunerTvInputService extends TvInputService mChannelDataManager = new ChannelDataManager(getApplicationContext()); mAudioCapabilitiesReceiver = new AudioCapabilitiesReceiver(getApplicationContext(), this); mAudioCapabilitiesReceiver.register(); - mBufferManager = createBufferManager(); if (CommonFeatures.DVR.isEnabled(this)) { JobScheduler jobScheduler = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE); @@ -79,11 +71,6 @@ public class TunerTvInputService extends TvInputService jobScheduler.schedule(job); } } - if (mBufferManager == null) { - Log.i(TAG, "Trickplay is disabled"); - } else { - Log.i(TAG, "Trickplay is enabled"); - } } @Override @@ -92,9 +79,6 @@ public class TunerTvInputService extends TvInputService super.onDestroy(); mChannelDataManager.release(); mAudioCapabilitiesReceiver.unregister(); - if (mBufferManager != null) { - mBufferManager.close(); - } } @Override @@ -106,8 +90,7 @@ public class TunerTvInputService extends TvInputService public Session onCreateSession(String inputId) { if (DEBUG) Log.d(TAG, "onCreateSession"); try { - final TunerSession session = new TunerSession( - this, mChannelDataManager, mBufferManager); + final TunerSession session = new TunerSession(this, mChannelDataManager); mTunerSessions.add(session); session.setAudioCapabilities(mAudioCapabilities); session.setOverlayViewEnabled(true); @@ -129,17 +112,6 @@ public class TunerTvInputService extends TvInputService } } - private BufferManager createBufferManager() { - int maxBufferSizeMb = - SystemPropertiesProxy.getInt(MAX_BUFFER_SIZE_KEY, MAX_BUFFER_SIZE_DEF); - if (maxBufferSizeMb >= MIN_BUFFER_SIZE_DEF) { - return new BufferManager( - new TrickplayStorageManager(getApplicationContext(), getCacheDir(), - 1024L * 1024 * maxBufferSizeMb)); - } - return null; - } - public static String getInputId(Context context) { return TvContract.buildInputId(new ComponentName(context, TunerTvInputService.class)); } diff --git a/src/com/android/tv/tuner/util/PostalCodeUtils.java b/src/com/android/tv/tuner/util/PostalCodeUtils.java new file mode 100644 index 00000000..3942ce95 --- /dev/null +++ b/src/com/android/tv/tuner/util/PostalCodeUtils.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.tuner.util; + +import android.content.Context; +import android.location.Address; +import android.support.annotation.Nullable; +import android.text.TextUtils; +import android.util.Log; + +import com.android.tv.tuner.TunerPreferences; +import com.android.tv.util.LocationUtils; + +import java.io.IOException; +import java.util.Locale; + +/** + * A utility class to update, get, and set the last known postal or zip code. + */ +public class PostalCodeUtils { + private static final String TAG = "PostalCodeUtils"; + private static final String SUPPORTED_COUNTRY_CODE = Locale.US.getCountry(); + + /** Returns {@code true} if postal code has been changed */ + public static boolean updatePostalCode(Context context) + throws IOException, SecurityException, NoPostalCodeException { + String postalCode = getPostalCode(context); + String lastPostalCode = getLastPostalCode(context); + if (TextUtils.isEmpty(postalCode)) { + if (TextUtils.isEmpty(lastPostalCode)) { + throw new NoPostalCodeException(); + } + } else if (!TextUtils.equals(postalCode, lastPostalCode)) { + setLastPostalCode(context, postalCode); + return true; + } + return false; + } + + /** + * Gets the last stored postal or zip code, which might be decided by {@link LocationUtils} or + * input by users. + */ + public static String getLastPostalCode(Context context) { + return TunerPreferences.getLastPostalCode(context); + } + + /** + * Sets the last stored postal or zip code. This method will overwrite the value written by + * calling {@link #updatePostalCode(Context)}. + */ + public static void setLastPostalCode(Context context, String postalCode) { + Log.i(TAG, "Set Postal Code:" + postalCode); + TunerPreferences.setLastPostalCode(context, postalCode); + } + + @Nullable + private static String getPostalCode(Context context) throws IOException, SecurityException { + Address address = LocationUtils.getCurrentAddress(context); + if (address != null) { + Log.i(TAG, "Current country and postal code is " + address.getCountryName() + ", " + + address.getPostalCode()); + if (TextUtils.equals(address.getCountryCode(), SUPPORTED_COUNTRY_CODE)) { + return address.getPostalCode(); + } + } + return null; + } + + /** An {@link java.lang.Exception} class to notify no valid postal or zip code is available. */ + public static class NoPostalCodeException extends Exception { + public NoPostalCodeException() { + } + } +} \ No newline at end of file diff --git a/src/com/android/tv/tuner/util/StringUtils.java b/src/com/android/tv/tuner/util/StringUtils.java deleted file mode 100644 index 15571e75..00000000 --- a/src/com/android/tv/tuner/util/StringUtils.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.tuner.util; - -/** - * Utility class for handling {@link String}. - */ -public final class StringUtils { - - private StringUtils() { } - - /** - * Returns compares two strings lexicographically and handles null values quietly. - */ - public static int compare(String a, String b) { - if (a == null) { - return b == null ? 0 : -1; - } - if (b == null) { - return 1; - } - return a.compareTo(b); - } -} diff --git a/src/com/android/tv/tuner/util/SystemPropertiesProxy.java b/src/com/android/tv/tuner/util/SystemPropertiesProxy.java index 62a64361..2817ccbf 100644 --- a/src/com/android/tv/tuner/util/SystemPropertiesProxy.java +++ b/src/com/android/tv/tuner/util/SystemPropertiesProxy.java @@ -58,4 +58,20 @@ public class SystemPropertiesProxy { } return def; } + + public static String getString(String key, String def) throws IllegalArgumentException { + try { + Class SystemPropertiesClass = Class.forName("android.os.SystemProperties"); + Method getIntMethod = + SystemPropertiesClass.getDeclaredMethod("get", String.class, String.class); + getIntMethod.setAccessible(true); + return (String) getIntMethod.invoke(SystemPropertiesClass, key, def); + } catch (InvocationTargetException + | IllegalAccessException + | NoSuchMethodException + | ClassNotFoundException e) { + Log.e(TAG, "Failed to invoke SystemProperties.get()", e); + } + return def; + } } diff --git a/src/com/android/tv/tuner/util/TunerInputInfoUtils.java b/src/com/android/tv/tuner/util/TunerInputInfoUtils.java index 5c411f64..fd9ec77d 100644 --- a/src/com/android/tv/tuner/util/TunerInputInfoUtils.java +++ b/src/com/android/tv/tuner/util/TunerInputInfoUtils.java @@ -25,6 +25,7 @@ import android.os.Build; import android.support.annotation.Nullable; import android.support.v4.os.BuildCompat; import android.util.Log; +import android.util.Pair; import com.android.tv.common.feature.CommonFeatures; import com.android.tv.tuner.R; @@ -43,23 +44,31 @@ public class TunerInputInfoUtils { */ @Nullable @TargetApi(Build.VERSION_CODES.N) - public static TvInputInfo buildTunerInputInfo(Context context, boolean fromBuiltInTuner) { - int numOfDevices = TunerHal.getTunerCount(context); - if (numOfDevices == 0) { + public static TvInputInfo buildTunerInputInfo(Context context) { + Pair tunerTypeAndCount = TunerHal.getTunerTypeAndCount(context); + if (tunerTypeAndCount.first == null || tunerTypeAndCount.second == 0) { return null; } - TvInputInfo.Builder builder = new TvInputInfo.Builder(context, new ComponentName(context, - TunerTvInputService.class)); - if (fromBuiltInTuner) { - builder.setLabel(R.string.bt_app_name); - } else { - builder.setLabel(R.string.ut_app_name); + int inputLabelId = 0; + switch (tunerTypeAndCount.first) { + case TunerHal.TUNER_TYPE_BUILT_IN: + inputLabelId = R.string.bt_app_name; + break; + case TunerHal.TUNER_TYPE_USB: + inputLabelId = R.string.ut_app_name; + break; + case TunerHal.TUNER_TYPE_NETWORK: + inputLabelId = R.string.nt_app_name; + break; } try { - return builder.setCanRecord(CommonFeatures.DVR.isEnabled(context)) - .setTunerCount(numOfDevices) + TvInputInfo.Builder builder = new TvInputInfo.Builder(context, + new ComponentName(context, TunerTvInputService.class)); + return builder.setLabel(inputLabelId) + .setCanRecord(CommonFeatures.DVR.isEnabled(context)) + .setTunerCount(tunerTypeAndCount.second) .build(); - } catch (NullPointerException e) { + } catch (IllegalArgumentException | NullPointerException e) { // TunerTvInputService is not enabled. return null; } @@ -73,7 +82,7 @@ public class TunerInputInfoUtils { public static void updateTunerInputInfo(Context context) { if (BuildCompat.isAtLeastN()) { if (DEBUG) Log.d(TAG, "updateTunerInputInfo()"); - TvInputInfo info = buildTunerInputInfo(context, isBuiltInTuner(context)); + TvInputInfo info = buildTunerInputInfo(context); if (info != null) { ((TvInputManager) context.getSystemService(Context.TV_INPUT_SERVICE)) .updateTvInputInfo(info); @@ -88,13 +97,4 @@ public class TunerInputInfoUtils { } } } - - /** - * Returns if the current tuner service is for a built-in tuner. - * - * @param context {@link Context} instance - */ - public static boolean isBuiltInTuner(Context context) { - return TunerHal.getTunerType(context) == TunerHal.TUNER_TYPE_BUILT_IN; - } -} +} \ No newline at end of file diff --git a/src/com/android/tv/ui/AppLayerTvView.java b/src/com/android/tv/ui/AppLayerTvView.java index 09acb36b..6a83c541 100644 --- a/src/com/android/tv/ui/AppLayerTvView.java +++ b/src/com/android/tv/ui/AppLayerTvView.java @@ -22,6 +22,7 @@ import android.util.AttributeSet; import android.view.SurfaceView; import android.view.View; +import com.android.tv.util.Debug; import com.android.tv.experiments.Experiments; /** @@ -59,4 +60,13 @@ public class AppLayerTvView extends TvView { } super.onViewAdded(child); } + + @Override + public void getLocationOnScreen(int[] outLocation) { + super.getLocationOnScreen(outLocation); + + // The TvView.MySessionCallback.onSessionCreated() will call this method indirectly. + Debug.getTimer(Debug.TAG_START_UP_TIMER).log( + "AppLayerTvView.getLocationOnScreen, session created"); + } } diff --git a/src/com/android/tv/ui/ChannelBannerView.java b/src/com/android/tv/ui/ChannelBannerView.java index 3cf4de83..eed536a8 100644 --- a/src/com/android/tv/ui/ChannelBannerView.java +++ b/src/com/android/tv/ui/ChannelBannerView.java @@ -57,7 +57,7 @@ import com.android.tv.data.Channel; import com.android.tv.data.Program; import com.android.tv.data.StreamInfo; import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.ScheduledRecording; +import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.parental.ContentRatingsManager; import com.android.tv.util.ImageCache; import com.android.tv.util.ImageLoader; @@ -98,8 +98,8 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage private static final String EMPTY_STRING = ""; - private static Program sNoProgram; - private static Program sLockedChannelProgram; + private Program mNoProgram; + private Program mLockedChannelProgram; private static String sClosedCaptionMark; private final MainActivity mMainActivity; @@ -123,6 +123,7 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage private String mProgramDescriptionText; private View mAnchorView; private Channel mCurrentChannel; + private boolean mCurrentChannelLogoExists; private Program mLastUpdatedProgram; private final Handler mHandler = new Handler(); private final DvrManager mDvrManager; @@ -130,6 +131,7 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage private TvContentRating mBlockingContentRating; private int mLockType; + private boolean mUpdateOnTune; private Animator mResizeAnimator; private int mCurrentHeight; @@ -192,7 +194,7 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage @Override public void run() { removeCallbacks(this); - updateViews(null); + updateViews(false); } }; @@ -243,18 +245,14 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage mContentRatingsManager = TvApplication.getSingletons(getContext()) .getTvInputManagerHelper().getContentRatingsManager(); - if (sNoProgram == null) { - sNoProgram = new Program.Builder() - .setTitle(context.getString(R.string.channel_banner_no_title)) - .setDescription(EMPTY_STRING) - .build(); - } - if (sLockedChannelProgram == null){ - sLockedChannelProgram = new Program.Builder() - .setTitle(context.getString(R.string.channel_banner_locked_channel_title)) - .setDescription(EMPTY_STRING) - .build(); - } + mNoProgram = new Program.Builder() + .setTitle(context.getString(R.string.channel_banner_no_title)) + .setDescription(EMPTY_STRING) + .build(); + mLockedChannelProgram = new Program.Builder() + .setTitle(context.getString(R.string.channel_banner_locked_channel_title)) + .setDescription(EMPTY_STRING) + .build(); if (sClosedCaptionMark == null) { sClosedCaptionMark = context.getString(R.string.closed_caption); } @@ -345,19 +343,17 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage * Set new lock type. * * @param lockType Any of LOCK_NONE, LOCK_PROGRAM_DETAIL, or LOCK_CHANNEL_INFO. - * @return {@code true} only if lock type is changed + * @return the previous lock type of the channel banner. * @throws IllegalArgumentException if lockType is invalid. */ - public boolean setLockType(int lockType) { + public int setLockType(int lockType) { if (lockType != LOCK_NONE && lockType != LOCK_CHANNEL_INFO && lockType != LOCK_PROGRAM_DETAIL) { throw new IllegalArgumentException("No such lock type " + lockType); } - if (mLockType != lockType) { - mLockType = lockType; - return true; - } - return false; + int previousLockType = mLockType; + mLockType = lockType; + return previousLockType; } /** @@ -372,31 +368,34 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage /** * Update channel banner view. * - * @param info A StreamInfo that includes stream information. - * If it's {@code null}, only program information will be updated. + * @param updateOnTune {@false} denotes the channel banner is updated due to other reasons than + * tuning. The channel info will not be updated in this case. */ - public void updateViews(StreamInfo info) { + public void updateViews(boolean updateOnTune) { resetAnimationEffects(); - Channel channel = mMainActivity.getCurrentChannel(); - if (!Objects.equals(mCurrentChannel, channel)) { - mBlockingContentRating = null; + mUpdateOnTune = updateOnTune; + if (mUpdateOnTune) { if (isShown()) { scheduleHide(); } - } - mCurrentChannel = channel; - mChannelView.setVisibility(VISIBLE); - if (info != null) { - // If the current channels between ChannelTuner and TvView are different, - // the stream information should not be seen. - updateStreamInfo(channel != null && channel.equals(info.getCurrentChannel()) ? info - : null); + mBlockingContentRating = null; + mCurrentChannel = mMainActivity.getCurrentChannel(); + mCurrentChannelLogoExists = + mCurrentChannel != null && mCurrentChannel.channelLogoExists(); + updateStreamInfo(null); updateChannelInfo(); } updateProgramInfo(mMainActivity.getCurrentProgram()); + mChannelView.setVisibility(VISIBLE); + mUpdateOnTune = false; } - private void updateStreamInfo(StreamInfo info) { + /** + * Update channel banner view with stream info. + * + * @param info A StreamInfo that includes stream information. + */ + public void updateStreamInfo(StreamInfo info) { // Update stream information in a channel. if (mLockType != LOCK_CHANNEL_INFO && info != null) { updateText(mClosedCaptionTextView, info.hasClosedCaption() ? sClosedCaptionMark @@ -414,9 +413,6 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage mAspectRatioTextView.setVisibility(View.GONE); mResolutionTextView.setVisibility(View.GONE); mAudioChannelTextView.setVisibility(View.GONE); - for (int i = 0; i < DISPLAYED_CONTENT_RATINGS_COUNT; i++) { - mContentRatingsTextViews[i].setVisibility(View.GONE); - } } } @@ -467,7 +463,7 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage } mChannelLogoImageView.setImageBitmap(null); mChannelLogoImageView.setVisibility(View.GONE); - if (mCurrentChannel != null) { + if (mCurrentChannel != null && mCurrentChannelLogoExists) { mCurrentChannel.loadBitmap(getContext(), Channel.LOAD_IMAGE_TYPE_CHANNEL_LOGO, mChannelLogoImageViewWidth, mChannelLogoImageViewHeight, createChannelLogoCallback(this, mCurrentChannel)); @@ -550,8 +546,9 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage if (mResizeAnimator == null) { String description = mProgramDescriptionTextView.getText().toString(); - boolean needFadeAnimation = !description.equals(mProgramDescriptionText); - updateBannerHeight(needFadeAnimation); + boolean programDescriptionNeedFadeAnimation = + !description.equals(mProgramDescriptionText) && !mUpdateOnTune; + updateBannerHeight(programDescriptionNeedFadeAnimation); } else { mProgramInfoUpdatePendingByResizing = true; } @@ -559,9 +556,9 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage private void updateProgramInfo(Program program) { if (mLockType == LOCK_CHANNEL_INFO) { - program = sLockedChannelProgram; + program = mLockedChannelProgram; } else if (program == null || !program.isValid() || TextUtils.isEmpty(program.getTitle())) { - program = sNoProgram; + program = mNoProgram; } if (mLastUpdatedProgram == null @@ -590,9 +587,9 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage mProgramDescriptionText = program.getDescription(); } String description = mProgramDescriptionTextView.getText().toString(); - boolean needFadeAnimation = isProgramChanged - || !description.equals(mProgramDescriptionText); - updateBannerHeight(needFadeAnimation); + boolean programDescriptionNeedFadeAnimation = (isProgramChanged + || !description.equals(mProgramDescriptionText)) && !mUpdateOnTune; + updateBannerHeight(programDescriptionNeedFadeAnimation); } else { mProgramInfoUpdatePendingByResizing = true; } @@ -603,7 +600,7 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage if (program == null) { return; } - updateProgramTextView(program == sLockedChannelProgram, program.getTitle(), + updateProgramTextView(program == mLockedChannelProgram, program.getTitle(), program.getEpisodeDisplayTitle(getContext())); } @@ -630,9 +627,8 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); mProgramTextView.setText(text); } - int width = mProgramDescriptionTextViewWidth - - ((mChannelLogoImageView.getVisibility() != View.VISIBLE) - ? 0 : mChannelLogoImageViewWidth + mChannelLogoImageViewMarginStart); + int width = mProgramDescriptionTextViewWidth + (mCurrentChannelLogoExists ? + 0 : mChannelLogoImageViewWidth + mChannelLogoImageViewMarginStart); ViewGroup.LayoutParams lp = mProgramTextView.getLayoutParams(); lp.width = width; mProgramTextView.setLayoutParams(lp); @@ -655,23 +651,27 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage } private void updateProgramRatings(Program program) { - if (mBlockingContentRating != null) { + if (mLockType == LOCK_CHANNEL_INFO) { + for (int i = 0; i < DISPLAYED_CONTENT_RATINGS_COUNT; i++) { + mContentRatingsTextViews[i].setVisibility(View.GONE); + } + } else if (mBlockingContentRating != null) { mContentRatingsTextViews[0].setText( mContentRatingsManager.getDisplayNameForRating(mBlockingContentRating)); mContentRatingsTextViews[0].setVisibility(View.VISIBLE); for (int i = 1; i < DISPLAYED_CONTENT_RATINGS_COUNT; i++) { mContentRatingsTextViews[i].setVisibility(View.GONE); } - return; - } - TvContentRating[] ratings = (program == null) ? null : program.getContentRatings(); - for (int i = 0; i < DISPLAYED_CONTENT_RATINGS_COUNT; i++) { - if (ratings == null || ratings.length <= i) { - mContentRatingsTextViews[i].setVisibility(View.GONE); - } else { - mContentRatingsTextViews[i].setText( - mContentRatingsManager.getDisplayNameForRating(ratings[i])); - mContentRatingsTextViews[i].setVisibility(View.VISIBLE); + } else { + TvContentRating[] ratings = (program == null) ? null : program.getContentRatings(); + for (int i = 0; i < DISPLAYED_CONTENT_RATINGS_COUNT; i++) { + if (ratings == null || ratings.length <= i) { + mContentRatingsTextViews[i].setVisibility(View.GONE); + } else { + mContentRatingsTextViews[i].setText( + mContentRatingsManager.getDisplayNameForRating(ratings[i])); + mContentRatingsTextViews[i].setVisibility(View.VISIBLE); + } } } } @@ -769,7 +769,7 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage mLastUpdatedProgram = program; } - private void updateBannerHeight(boolean needFadeAnimation) { + private void updateBannerHeight(boolean needProgramDescriptionFadeAnimation) { Assert.assertNull(mResizeAnimator); // Need to measure the layout height with the new description text. CharSequence oldDescription = mProgramDescriptionTextView.getText(); @@ -785,12 +785,13 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage layoutParams.height = targetHeight; setLayoutParams(layoutParams); } - } else if (mCurrentHeight != targetHeight || needFadeAnimation) { + } else if (mCurrentHeight != targetHeight || needProgramDescriptionFadeAnimation) { // Restore description text for fade in/out animation. - if (needFadeAnimation) { + if (needProgramDescriptionFadeAnimation) { mProgramDescriptionTextView.setText(oldDescription); } - mResizeAnimator = createResizeAnimator(targetHeight, needFadeAnimation); + mResizeAnimator = + createResizeAnimator(targetHeight, needProgramDescriptionFadeAnimation); mResizeAnimator.start(); } } diff --git a/src/com/android/tv/ui/KeypadChannelSwitchView.java b/src/com/android/tv/ui/KeypadChannelSwitchView.java index abc05bad..ac5d841d 100644 --- a/src/com/android/tv/ui/KeypadChannelSwitchView.java +++ b/src/com/android/tv/ui/KeypadChannelSwitchView.java @@ -39,7 +39,7 @@ import android.widget.TextView; import com.android.tv.MainActivity; import com.android.tv.R; import com.android.tv.TvApplication; -import com.android.tv.analytics.DurationTimer; +import com.android.tv.util.DurationTimer; import com.android.tv.analytics.Tracker; import com.android.tv.common.SoftPreconditions; import com.android.tv.data.Channel; diff --git a/src/com/android/tv/ui/SelectInputView.java b/src/com/android/tv/ui/SelectInputView.java index 5e25ae43..88872a0a 100644 --- a/src/com/android/tv/ui/SelectInputView.java +++ b/src/com/android/tv/ui/SelectInputView.java @@ -18,7 +18,6 @@ package com.android.tv.ui; import android.content.Context; import android.content.res.Resources; -import android.hardware.hdmi.HdmiDeviceInfo; import android.media.tv.TvInputInfo; import android.media.tv.TvInputManager; import android.media.tv.TvInputManager.TvInputCallback; @@ -37,14 +36,13 @@ import android.widget.TextView; import com.android.tv.ApplicationSingletons; import com.android.tv.R; import com.android.tv.TvApplication; -import com.android.tv.analytics.DurationTimer; +import com.android.tv.util.DurationTimer; import com.android.tv.analytics.Tracker; import com.android.tv.data.Channel; import com.android.tv.util.TvInputManagerHelper; import java.util.ArrayList; import java.util.Collections; -import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -58,7 +56,7 @@ public class SelectInputView extends VerticalGridView implements private final TvInputManagerHelper mTvInputManagerHelper; private final List mInputList = new ArrayList<>(); - private final InputsComparator mComparator = new InputsComparator(); + private final TvInputManagerHelper.InputComparator mComparator; private final Tracker mTracker; private final DurationTimer mViewDurationTimer = new DurationTimer(); private final TvInputCallback mTvInputCallback = new TvInputCallback() { @@ -149,6 +147,7 @@ public class SelectInputView extends VerticalGridView implements ApplicationSingletons appSingletons = TvApplication.getSingletons(context); mTracker = appSingletons.getTracker(); mTvInputManagerHelper = appSingletons.getTvInputManagerHelper(); + mComparator = new TvInputManagerHelper.InputComparator(context, mTvInputManagerHelper); Resources resources = context.getResources(); mInputItemHeight = resources.getDimensionPixelSize(R.dimen.input_banner_item_height); @@ -385,72 +384,6 @@ public class SelectInputView extends VerticalGridView implements } } - private class InputsComparator implements Comparator { - @Override - public int compare(TvInputInfo lhs, TvInputInfo rhs) { - if (lhs == null) { - return (rhs == null) ? 0 : 1; - } - if (rhs == null) { - return -1; - } - - boolean enabledL = isInputEnabled(lhs); - boolean enabledR = isInputEnabled(rhs); - if (enabledL != enabledR) { - return enabledL ? -1 : 1; - } - - int priorityL = getPriority(lhs); - int priorityR = getPriority(rhs); - if (priorityL != priorityR) { - return priorityR - priorityL; - } - - String customLabelL = (String) lhs.loadCustomLabel(getContext()); - String customLabelR = (String) rhs.loadCustomLabel(getContext()); - if (!TextUtils.equals(customLabelL, customLabelR)) { - customLabelL = customLabelL == null ? "" : customLabelL; - customLabelR = customLabelR == null ? "" : customLabelR; - return customLabelL.compareToIgnoreCase(customLabelR); - } - - String labelL = (String) lhs.loadLabel(getContext()); - String labelR = (String) rhs.loadLabel(getContext()); - labelL = labelL == null ? "" : labelL; - labelR = labelR == null ? "" : labelR; - return labelL.compareToIgnoreCase(labelR); - } - - private int getPriority(TvInputInfo info) { - switch (info.getType()) { - case TvInputInfo.TYPE_TUNER: - return 9; - case TvInputInfo.TYPE_HDMI: - HdmiDeviceInfo hdmiInfo = info.getHdmiDeviceInfo(); - if (hdmiInfo != null && hdmiInfo.isCecDevice()) { - return 8; - } - return 7; - case TvInputInfo.TYPE_DVI: - return 6; - case TvInputInfo.TYPE_COMPONENT: - return 5; - case TvInputInfo.TYPE_SVIDEO: - return 4; - case TvInputInfo.TYPE_COMPOSITE: - return 3; - case TvInputInfo.TYPE_DISPLAY_PORT: - return 2; - case TvInputInfo.TYPE_VGA: - return 1; - case TvInputInfo.TYPE_SCART: - default: - return 0; - } - } - } - /** * A callback interface for the input selection. */ diff --git a/src/com/android/tv/ui/TunableTvView.java b/src/com/android/tv/ui/TunableTvView.java index cbe459fb..8f4f40f5 100644 --- a/src/com/android/tv/ui/TunableTvView.java +++ b/src/com/android/tv/ui/TunableTvView.java @@ -19,9 +19,16 @@ package com.android.tv.ui; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.TimeInterpolator; -import android.annotation.SuppressLint; +import android.app.AlertDialog; +import android.app.ApplicationErrorReport; import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; import android.content.pm.PackageManager; +import android.graphics.Bitmap; +import android.graphics.PorterDuff; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; import android.media.PlaybackParams; import android.media.tv.TvContentRating; import android.media.tv.TvInputInfo; @@ -38,7 +45,6 @@ import android.support.annotation.IntDef; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.UiThread; -import android.support.v4.os.BuildCompat; import android.text.TextUtils; import android.text.format.DateUtils; import android.util.AttributeSet; @@ -56,14 +62,20 @@ import com.android.tv.InputSessionManager; import com.android.tv.InputSessionManager.TvViewSession; import com.android.tv.R; import com.android.tv.TvApplication; -import com.android.tv.analytics.DurationTimer; +import com.android.tv.data.Program; +import com.android.tv.data.ProgramDataManager; +import com.android.tv.parental.ParentalControlSettings; +import com.android.tv.util.DurationTimer; +import com.android.tv.util.Debug; import com.android.tv.analytics.Tracker; +import com.android.tv.common.BuildConfig; import com.android.tv.common.feature.CommonFeatures; import com.android.tv.data.Channel; import com.android.tv.data.StreamInfo; import com.android.tv.data.WatchedHistoryManager; import com.android.tv.parental.ContentRatingsManager; import com.android.tv.recommendation.NotificationService; +import com.android.tv.util.ImageLoader; import com.android.tv.util.NetworkUtils; import com.android.tv.util.PermissionUtils; import com.android.tv.util.TvInputManagerHelper; @@ -105,14 +117,13 @@ public class TunableTvView extends FrameLayout implements StreamInfo { private static final int FADING_IN = 2; private static final int FADING_OUT = 3; - // It is too small to see the description text without PIP_BLOCK_SCREEN_SCALE_FACTOR. - private static final float PIP_BLOCK_SCREEN_SCALE_FACTOR = 1.2f; - private AppLayerTvView mTvView; private TvViewSession mTvViewSession; private Channel mCurrentChannel; private TvInputManagerHelper mInputManagerHelper; private ContentRatingsManager mContentRatingsManager; + private ParentalControlSettings mParentalControlSettings; + private ProgramDataManager mProgramDataManager; @Nullable private WatchedHistoryManager mWatchedHistoryManager; private boolean mStarted; @@ -126,6 +137,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo { private int mAudioChannelCount = StreamInfo.AUDIO_CHANNEL_COUNT_UNKNOWN; private boolean mHasClosedCaption = false; private boolean mVideoAvailable; + private boolean mAudioAvailable; private boolean mScreenBlocked; private OnScreenBlockingChangedListener mOnScreenBlockedListener; private TvContentRating mBlockedContentRating; @@ -136,10 +148,8 @@ public class TunableTvView extends FrameLayout implements StreamInfo { private boolean mParentControlEnabled; private int mFixedSurfaceWidth; private int mFixedSurfaceHeight; - private boolean mIsPip; - private int mScreenHeight; - private int mShrunkenTvViewHeight; private final boolean mCanModifyParentalControls; + private boolean mIsUnderShrunken; @TimeShiftState private int mTimeShiftState = TIME_SHIFT_STATE_NONE; private TimeShiftListener mTimeShiftListener; @@ -156,12 +166,14 @@ public class TunableTvView extends FrameLayout implements StreamInfo { // A View to hide screen when there's problem in video playback. private final BlockScreenView mHideScreenView; - - // A View to block screen until onContentAllowed is received if parental control is on. - private final View mBlockScreenForTuneView; + private final int mHideScreenImageColorFilter; // A spinner view to show buffering status. private final View mBufferingSpinnerView; + private TuningBlockView mTuningBlockView; + + // A View to block screen until onContentAllowed is received if parental control is on. + private final View mBlockScreenForTuneView; // A View for fade-in/out animation private final View mDimScreenView; @@ -286,14 +298,66 @@ public class TunableTvView extends FrameLayout implements StreamInfo { @Override public void onVideoAvailable(String inputId) { + if (DEBUG) Log.d(TAG, "onVideoAvailable: {inputId=" + inputId + "}"); + Debug.getTimer(Debug.TAG_START_UP_TIMER).log("Start up of Live TV ends," + + " TunableTvView.onVideoAvailable resets timer"); + long startUpDurationTime = Debug.getTimer(Debug.TAG_START_UP_TIMER).reset(); + Debug.removeTimer(Debug.TAG_START_UP_TIMER); + if (BuildConfig.ENG + && startUpDurationTime > Debug.TIME_START_UP_DURATION_THRESHOLD) { + showAlertDialogForLongStartUp(); + } unhideScreenByVideoAvailability(); if (mOnTuneListener != null) { mOnTuneListener.onStreamInfoChanged(TunableTvView.this); } } + private void showAlertDialogForLongStartUp() { + new AlertDialog.Builder(getContext()).setTitle( + getContext().getString(R.string.settings_send_feedback)) + .setMessage("Because the start up time of Live channels is too long," + + " please send feedback") + .setPositiveButton(android.R.string.ok, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + Intent intent = new Intent(Intent.ACTION_APP_ERROR); + ApplicationErrorReport report = new ApplicationErrorReport(); + report.packageName = report.processName = getContext() + .getApplicationContext().getPackageName(); + report.time = System.currentTimeMillis(); + report.type = ApplicationErrorReport.TYPE_CRASH; + + // Add the crash info to add title of feedback automatically. + ApplicationErrorReport.CrashInfo crash = new + ApplicationErrorReport.CrashInfo(); + crash.exceptionClassName = + "Live TV start up takes long time"; + crash.exceptionMessage = + "The start up time of Live TV is too long"; + report.crashInfo = crash; + + intent.putExtra(Intent.EXTRA_BUG_REPORT, report); + getContext().startActivity(intent); + } + }) + .setNegativeButton(android.R.string.no, null) + .show(); + } + @Override public void onVideoUnavailable(String inputId, int reason) { + if (reason != TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING + && reason != TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING) { + Debug.getTimer(Debug.TAG_START_UP_TIMER).log( + "TunableTvView.onVideoUnAvailable reason = (" + reason + + ") and removes timer"); + Debug.removeTimer(Debug.TAG_START_UP_TIMER); + } else { + Debug.getTimer(Debug.TAG_START_UP_TIMER).log( + "TunableTvView.onVideoUnAvailable reason = (" + reason + ")"); + } hideScreenByVideoAvailability(inputId, reason); if (mOnTuneListener != null) { mOnTuneListener.onStreamInfoChanged(TunableTvView.this); @@ -311,7 +375,8 @@ public class TunableTvView extends FrameLayout implements StreamInfo { @Override public void onContentAllowed(String inputId) { mBlockScreenForTuneView.setVisibility(View.GONE); - unblockScreenByContentRating(); + mBlockedContentRating = null; + checkBlockScreenAndMuteNeeded(); if (mOnTuneListener != null) { mOnTuneListener.onContentAllowed(); } @@ -319,7 +384,8 @@ public class TunableTvView extends FrameLayout implements StreamInfo { @Override public void onContentBlocked(String inputId, TvContentRating rating) { - blockScreenByContentRating(rating); + mBlockedContentRating = rating; + checkBlockScreenAndMuteNeeded(); if (mOnTuneListener != null) { mOnTuneListener.onContentBlocked(); } @@ -327,6 +393,10 @@ public class TunableTvView extends FrameLayout implements StreamInfo { @Override public void onTimeShiftStatusChanged(String inputId, int status) { + if (DEBUG) { + Log.d(TAG, "onTimeShiftStatusChanged: {inputId=" + inputId + ", status=" + status + + "}"); + } boolean available = status == TvInputManager.TIME_SHIFT_STATUS_AVAILABLE; setTimeShiftAvailable(available); } @@ -378,6 +448,8 @@ public class TunableTvView extends FrameLayout implements StreamInfo { mHideScreenView = (BlockScreenView) findViewById(R.id.hide_screen); mHideScreenView.setImageVisibility(false); mBufferingSpinnerView = findViewById(R.id.buffering_spinner); + mHideScreenImageColorFilter = getResources().getColor( + R.color.tvview_block_image_color_filter, null); mBlockScreenForTuneView = findViewById(R.id.block_screen_for_tune); mDimScreenView = findViewById(R.id.dim); mDimScreenView.animate().setListener(new AnimatorListenerAdapter() { @@ -397,27 +469,23 @@ public class TunableTvView extends FrameLayout implements StreamInfo { }); } - public void initialize(AppLayerTvView tvView, boolean isPip, int screenHeight, - int shrunkenTvViewHeight) { + public void initialize(AppLayerTvView tvView, TuningBlockView tuningBlockView, + ProgramDataManager programDataManager, TvInputManagerHelper tvInputManagerHelper) { mTvView = tvView; + mTuningBlockView = tuningBlockView; + mProgramDataManager = programDataManager; + mInputManagerHelper = tvInputManagerHelper; + mContentRatingsManager = tvInputManagerHelper.getContentRatingsManager(); + mParentalControlSettings = tvInputManagerHelper.getParentalControlSettings(); if (mInputSessionManager != null) { mTvViewSession = mInputSessionManager.createTvViewSession(tvView, this, mCallback); } else { mTvView.setCallback(mCallback); } - mIsPip = isPip; - mScreenHeight = screenHeight; - mShrunkenTvViewHeight = shrunkenTvViewHeight; - mTvView.setZOrderOnTop(isPip); copyLayoutParamsToTvView(); } - public void start(TvInputManagerHelper tvInputManagerHelper) { - mInputManagerHelper = tvInputManagerHelper; - mContentRatingsManager = tvInputManagerHelper.getContentRatingsManager(); - if (mStarted) { - return; - } + public void start() { mStarted = true; } @@ -497,6 +565,13 @@ public class TunableTvView extends FrameLayout implements StreamInfo { mWatchedHistoryManager = watchedHistoryManager; } + /** + * Sets if the TunableTvView is under shrunken. + */ + public void setIsUnderShrunken(boolean isUnderShrunken) { + mIsUnderShrunken = isUnderShrunken; + } + public boolean isPlaying() { return mStarted; } @@ -519,6 +594,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo { * if the state is disconnected or channelId doesn't exist, it returns false. */ public boolean tuneTo(Channel channel, Bundle params, OnTuneListener listener) { + Debug.getTimer(Debug.TAG_START_UP_TIMER).log("TunableTvView.tuneTo"); if (!mStarted) { throw new IllegalStateException("TvView isn't started"); } @@ -560,6 +636,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo { mVideoDisplayAspectRatio = 0f; mAudioChannelCount = StreamInfo.AUDIO_CHANNEL_COUNT_UNKNOWN; mHasClosedCaption = false; + mBlockedContentRating = null; mTimeShiftCurrentPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME; // To reduce the IPCs, unregister the callback here and register it when necessary. mTvView.setTimeShiftPositionCallback(null); @@ -571,12 +648,12 @@ public class TunableTvView extends FrameLayout implements StreamInfo { } hideScreenByVideoAvailability(mInputInfo.getId(), TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING); + updateBlockScreenUI(false); if (mTvViewSession != null) { mTvViewSession.tune(channel, params, listener); } else { mTvView.tune(mInputInfo.getId(), mCurrentChannel.getUri(), params); } - unblockScreenByContentRating(); if (channel.isPassthrough()) { mBlockScreenForTuneView.setVisibility(View.GONE); } else if (mParentControlEnabled) { @@ -714,6 +791,11 @@ public class TunableTvView extends FrameLayout implements StreamInfo { return mVideoAvailable; } + @Override + public boolean isVideoOrAudioAvailable() { + return mVideoAvailable || mAudioAvailable; + } + @Override public int getVideoUnavailableReason() { return mVideoUnavailableReason; @@ -747,7 +829,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo { } /** - * Returns if the screen is blocked by {@link #blockScreen()}. + * Returns if the screen is blocked by {@link #blockOrUnblockScreen(boolean)}. */ public boolean isScreenBlocked() { return mScreenBlocked; @@ -766,38 +848,18 @@ public class TunableTvView extends FrameLayout implements StreamInfo { } /** - * Locks current TV screen and mutes. + * Blocks/unblocks current TV screen and mutes. * There would be black screen with lock icon in order to show that * screen block is intended and not an error. * TODO: Accept parameter to show lock icon or not. + * + * @param blockOrUnblock {@code true} to block the screen, or {@code false} to unblock. */ - public void blockScreen() { - mScreenBlocked = true; + public void blockOrUnblockScreen(boolean blockOrUnblock) { + mScreenBlocked = blockOrUnblock; checkBlockScreenAndMuteNeeded(); if (mOnScreenBlockedListener != null) { - mOnScreenBlockedListener.onScreenBlockingChanged(true); - } - } - - private void blockScreenByContentRating(TvContentRating rating) { - mBlockedContentRating = rating; - checkBlockScreenAndMuteNeeded(); - } - - @Override - @SuppressLint("RtlHardcoded") - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - super.onLayout(changed, left, top, right, bottom); - if (mIsPip) { - int height = bottom - top; - float scale; - if (mBlockScreenType == BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW) { - scale = height * PIP_BLOCK_SCREEN_SCALE_FACTOR / mShrunkenTvViewHeight; - } else { - scale = height * PIP_BLOCK_SCREEN_SCALE_FACTOR / mScreenHeight; - } - // TODO: need to get UX confirmation. - mBlockScreenView.scaleContainerView(scale); + mOnScreenBlockedListener.onScreenBlockingChanged(blockOrUnblock); } } @@ -819,17 +881,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo { || tvViewLp.gravity != lp.gravity || tvViewLp.height != lp.height || tvViewLp.width != lp.width) { - if (lp.topMargin == tvViewLp.topMargin && lp.leftMargin == tvViewLp.leftMargin - && !BuildCompat.isAtLeastN()) { - // HACK: If top and left position aren't changed and SurfaceHolder.setFixedSize is - // used, SurfaceView doesn't catch the width and height change. It causes a bug that - // PIP size change isn't shown when PIP is located TOP|LEFT. So we adjust 1 px for - // small size PIP as a workaround. - // Note: This framework issue has been fixed from NYC. - tvViewLp.leftMargin = lp.leftMargin + 1; - } else { - tvViewLp.leftMargin = lp.leftMargin; - } + tvViewLp.leftMargin = lp.leftMargin; tvViewLp.topMargin = lp.topMargin; tvViewLp.bottomMargin = lp.bottomMargin; tvViewLp.rightMargin = lp.rightMargin; @@ -945,96 +997,109 @@ public class TunableTvView extends FrameLayout implements StreamInfo { private void checkBlockScreenAndMuteNeeded() { updateBlockScreenUI(false); - if (mScreenBlocked || mBlockedContentRating != null) { - mute(); - if (mIsPip) { - // If we don't make mTvView invisible, some frames are leaked when a user changes - // PIP layout in options. - // Note: When video is unavailable, we keep the mTvView's visibility, because - // TIS implementation may not send video available with no surface. - mTvView.setVisibility(View.INVISIBLE); - } - } else { - unmuteIfPossible(); - if (mIsPip) { - mTvView.setVisibility(View.VISIBLE); - } - } - } - - public void unblockScreen() { - mScreenBlocked = false; - checkBlockScreenAndMuteNeeded(); - if (mOnScreenBlockedListener != null) { - mOnScreenBlockedListener.onScreenBlockingChanged(false); - } - } - - private void unblockScreenByContentRating() { - mBlockedContentRating = null; - checkBlockScreenAndMuteNeeded(); + updateMuteStatus(); } @UiThread private void hideScreenByVideoAvailability(String inputId, int reason) { mVideoAvailable = false; + mAudioAvailable = false; mVideoUnavailableReason = reason; if (mInternetCheckTask != null) { mInternetCheckTask.cancel(true); mInternetCheckTask = null; } + if (reason != TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING) { + // Tuning block view will apply animation when unhide screen, so let's end the + // animation if it is running. + mTuningBlockView.endFadeOutAnimator(); + } switch (reason) { case TvInputManager.VIDEO_UNAVAILABLE_REASON_AUDIO_ONLY: mHideScreenView.setVisibility(VISIBLE); mHideScreenView.setImageVisibility(false); mHideScreenView.setText(R.string.tvview_msg_audio_only); + mTuningBlockView.setVisibility(GONE); mBufferingSpinnerView.setVisibility(GONE); - unmuteIfPossible(); + mAudioAvailable = true; break; case TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING: mBufferingSpinnerView.setVisibility(VISIBLE); - mute(); break; case TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL: mHideScreenView.setVisibility(VISIBLE); mHideScreenView.setText(R.string.tvview_msg_weak_signal); + mTuningBlockView.setVisibility(GONE); mBufferingSpinnerView.setVisibility(GONE); - mute(); break; case TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING: - mHideScreenView.setVisibility(VISIBLE); - mHideScreenView.setImageVisibility(false); - mHideScreenView.setText(null); mBufferingSpinnerView.setVisibility(VISIBLE); - mute(); + if (shouldShowImageForTuning()) { + mHideScreenView.setVisibility(GONE); + mTuningBlockView.setVisibility(VISIBLE); + mTuningBlockView.setImageVisibility(false); + showImageForTuning(); + } else { + mHideScreenView.setVisibility(VISIBLE); + mHideScreenView.setImageVisibility(false); + mHideScreenView.setText(null); + mTuningBlockView.setVisibility(GONE); + } break; case VIDEO_UNAVAILABLE_REASON_NOT_TUNED: mHideScreenView.setVisibility(VISIBLE); mHideScreenView.setImageVisibility(false); mHideScreenView.setText(null); + mTuningBlockView.setVisibility(GONE); mBufferingSpinnerView.setVisibility(GONE); - mute(); break; case VIDEO_UNAVAILABLE_REASON_NO_RESOURCE: mHideScreenView.setVisibility(VISIBLE); mHideScreenView.setImageVisibility(false); mHideScreenView.setText(getTuneConflictMessage(inputId)); + mTuningBlockView.setVisibility(GONE); mBufferingSpinnerView.setVisibility(GONE); - mute(); break; case TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN: default: mHideScreenView.setVisibility(VISIBLE); mHideScreenView.setImageVisibility(false); mHideScreenView.setText(null); + mTuningBlockView.setVisibility(GONE); mBufferingSpinnerView.setVisibility(GONE); - mute(); if (mCurrentChannel != null && !mCurrentChannel.isPhysicalTunerChannel()) { mInternetCheckTask = new InternetCheckTask(); mInternetCheckTask.execute(); } break; } + updateMuteStatus(); + } + + private boolean shouldShowImageForTuning() { + if (getWidth() == 0 || getWidth() == 0 || mCurrentChannel == null || !isBundledInput() + || mIsUnderShrunken || (mParentControlEnabled && (mCurrentChannel.isLocked()))) { + return false; + } + Program currentProgram = mProgramDataManager.getCurrentProgram(mCurrentChannel.getId()); + if (currentProgram == null) { + return false; + } + TvContentRating rating = + mParentalControlSettings.getBlockedRating(currentProgram.getContentRatings()); + return !(mParentControlEnabled && rating != null); + } + + private void showImageForTuning() { + mTuningBlockView.setImage(null); + if (mCurrentChannel == null) { + return; + } + Program currentProgram = mProgramDataManager.getCurrentProgram(mCurrentChannel.getId()); + if (currentProgram != null) { + currentProgram.loadPosterArt(getContext(), getWidth(), getHeight(), + createProgramPosterArtCallback(mTuningBlockView, mCurrentChannel.getId())); + } } private String getTuneConflictMessage(String inputId) { @@ -1052,25 +1117,43 @@ public class TunableTvView extends FrameLayout implements StreamInfo { private void unhideScreenByVideoAvailability() { mVideoAvailable = true; + mAudioAvailable = true; + mTuningBlockView.hideWithAnimationIfNeeded(); mHideScreenView.setVisibility(GONE); mBufferingSpinnerView.setVisibility(GONE); - unmuteIfPossible(); - } - - private void unmuteIfPossible() { - if (mVideoAvailable && !mScreenBlocked && mBlockedContentRating == null) { - unmute(); + updateMuteStatus(); + } + + private void updateMuteStatus() { + // Workaround: TunerTvInputService uses AC3 pass-through implementation, which disables + // audio tracks to enforce the mute request. We don't want to send mute request if we are + // not going to block the screen to prevent the video jankiness resulted by disabling audio + // track before the playback is started. In other way, we should send unmute request before + // the playback is started, because TunerTvInput will remember the muted state and mute + // itself right way when the playback is going to be started, which results the initial + // jankiness, too. + boolean isBundledInput = isBundledInput(); + if ((isBundledInput || mAudioAvailable) && !mScreenBlocked + && mBlockedContentRating == null) { + if (mIsMuted) { + mIsMuted = false; + mTvView.setStreamVolume(mVolume); + } + } else { + if (!mIsMuted) { + if ((mInputInfo == null || isBundledInput) + && !mScreenBlocked && mBlockedContentRating == null) { + return; + } + mIsMuted = true; + mTvView.setStreamVolume(0); + } } } - private void mute() { - mIsMuted = true; - mTvView.setStreamVolume(0); - } - - private void unmute() { - mIsMuted = false; - mTvView.setStreamVolume(mVolume); + private boolean isBundledInput() { + return mInputInfo != null && mInputInfo.getType() == TvInputInfo.TYPE_TUNER + && Utils.isBundledInput(mInputInfo.getId()); } /** Returns true if this view is faded out. */ @@ -1268,6 +1351,25 @@ public class TunableTvView extends FrameLayout implements StreamInfo { return mTimeShiftCurrentPositionMs; } + private ImageLoader.ImageLoaderCallback createProgramPosterArtCallback( + TuningBlockView view, final long channelId) { + return new ImageLoader.ImageLoaderCallback(view) { + @Override + public void onBitmapLoaded(TuningBlockView view, @Nullable Bitmap posterArt) { + if (posterArt == null || getCurrentChannel() == null + || channelId != getCurrentChannel().getId() + || !shouldShowImageForTuning()) { + return; + } + Drawable drawablePosterArt = new BitmapDrawable(view.getResources(), posterArt); + drawablePosterArt.mutate().setColorFilter( + mHideScreenImageColorFilter, PorterDuff.Mode.SRC_OVER); + view.setImage(drawablePosterArt); + view.setImageVisibility(true); + } + }; + } + /** * Used to receive the time-shift events. */ diff --git a/src/com/android/tv/ui/TuningBlockView.java b/src/com/android/tv/ui/TuningBlockView.java new file mode 100644 index 00000000..2914b461 --- /dev/null +++ b/src/com/android/tv/ui/TuningBlockView.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.ui; + +import android.animation.Animator; +import android.animation.AnimatorInflater; +import android.animation.AnimatorListenerAdapter; +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.widget.FrameLayout; +import android.widget.ImageView; + +import com.android.tv.R; +import com.android.tv.common.SoftPreconditions; + +/** + * A view to block the screen while tuning channels. + */ +public class TuningBlockView extends FrameLayout{ + private final static String TAG = "TuningBlockView"; + + private ImageView mImageView; + private Animator mFadeOut; + + public TuningBlockView(Context context) { + this(context, null, 0); + } + + public TuningBlockView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public TuningBlockView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mImageView = (ImageView) findViewById(R.id.image); + mFadeOut = AnimatorInflater.loadAnimator( + getContext(), R.animator.tuning_block_view_fade_out); + mFadeOut.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + setVisibility(GONE); + } + }); + mFadeOut.setTarget(mImageView); + } + + /** + * Sets image to the image view. This method should be called after finishing inflate the view. + */ + public void setImage(Drawable imageDrawable) { + SoftPreconditions.checkState(mImageView != null, TAG, "imageView is null"); + mImageView.setImageDrawable(imageDrawable); + } + + /** + * Sets the visibility of image view. + * This method should be called after finishing inflate the view. + */ + public void setImageVisibility(boolean visible) { + SoftPreconditions.checkState(mImageView != null, TAG, "imageView is null"); + mImageView.setAlpha(1.0f); + mImageView.setVisibility(visible ? VISIBLE: GONE); + } + + /** + * Returns if the image view is visible. + * This method should be called after finishing inflate the view. + */ + public boolean isImageArtVisible() { + SoftPreconditions.checkState(mImageView != null, TAG, "imageView is null"); + return mImageView.getVisibility() == VISIBLE; + } + + /** + * Hides the view with animation if needed. + */ + public void hideWithAnimationIfNeeded() { + if (getVisibility() == VISIBLE && isImageArtVisible()) { + mFadeOut.start(); + } else { + setVisibility(GONE); + } + } + + /** + * Ends the fade out animator. + */ + public void endFadeOutAnimator() { + if (mFadeOut != null && mFadeOut.isRunning()) { + mFadeOut.end(); + } + } +} diff --git a/src/com/android/tv/ui/TvOverlayManager.java b/src/com/android/tv/ui/TvOverlayManager.java index e14b286b..f86e6e95 100644 --- a/src/com/android/tv/ui/TvOverlayManager.java +++ b/src/com/android/tv/ui/TvOverlayManager.java @@ -39,6 +39,7 @@ import com.android.tv.MainActivity.KeyHandlerResultType; import com.android.tv.R; import com.android.tv.TimeShiftManager; import com.android.tv.TvApplication; +import com.android.tv.TvOptionsManager; import com.android.tv.analytics.Tracker; import com.android.tv.common.WeakHandler; import com.android.tv.common.feature.CommonFeatures; @@ -46,13 +47,14 @@ import com.android.tv.common.ui.setup.OnActionClickListener; import com.android.tv.common.ui.setup.SetupFragment; import com.android.tv.common.ui.setup.SetupMultiPaneFragment; import com.android.tv.data.ChannelDataManager; +import com.android.tv.dialog.DvrHistoryDialogFragment; import com.android.tv.dialog.FullscreenDialogFragment; +import com.android.tv.dialog.HalfSizedDialogFragment; import com.android.tv.dialog.PinDialogFragment; import com.android.tv.dialog.RecentlyWatchedDialogFragment; import com.android.tv.dialog.SafeDismissDialogFragment; import com.android.tv.dvr.DvrDataManager; -import com.android.tv.dvr.ui.DvrActivity; -import com.android.tv.dvr.ui.HalfSizedDialogFragment; +import com.android.tv.dvr.ui.browse.DvrBrowseActivity; import com.android.tv.guide.ProgramGuide; import com.android.tv.menu.Menu; import com.android.tv.menu.Menu.MenuShowReason; @@ -163,6 +165,7 @@ public class TvOverlayManager { private static final Set AVAILABLE_DIALOG_TAGS = new HashSet<>(); static { AVAILABLE_DIALOG_TAGS.add(RecentlyWatchedDialogFragment.DIALOG_TAG); + AVAILABLE_DIALOG_TAGS.add(DvrHistoryDialogFragment.DIALOG_TAG); AVAILABLE_DIALOG_TAGS.add(PinDialogFragment.DIALOG_TAG); AVAILABLE_DIALOG_TAGS.add(FullscreenDialogFragment.DIALOG_TAG); AVAILABLE_DIALOG_TAGS.add(SettingsFragment.LicenseActionItem.DIALOG_TAG); @@ -195,10 +198,10 @@ public class TvOverlayManager { private OnBackStackChangedListener mOnBackStackChangedListener; public TvOverlayManager(MainActivity mainActivity, ChannelTuner channelTuner, - TunableTvView tvView, KeypadChannelSwitchView keypadChannelSwitchView, - ChannelBannerView channelBannerView, InputBannerView inputBannerView, - SelectInputView selectInputView, ViewGroup sceneContainer, - ProgramGuideSearchFragment searchFragment) { + TunableTvView tvView, TvOptionsManager optionsManager, + KeypadChannelSwitchView keypadChannelSwitchView, ChannelBannerView channelBannerView, + InputBannerView inputBannerView, SelectInputView selectInputView, + ViewGroup sceneContainer, ProgramGuideSearchFragment searchFragment) { mMainActivity = mainActivity; mChannelTuner = channelTuner; ApplicationSingletons singletons = TvApplication.getSingletons(mainActivity); @@ -225,7 +228,8 @@ public class TvOverlayManager { }); // Menu MenuView menuView = (MenuView) mainActivity.findViewById(R.id.menu); - mMenu = new Menu(mainActivity, tvView, menuView, new MenuRowFactory(mainActivity, tvView), + mMenu = new Menu(mainActivity, tvView, optionsManager, menuView, + new MenuRowFactory(mainActivity, tvView), new Menu.OnMenuVisibilityChangeListener() { @Override public void onMenuVisibilityChange(boolean visible) { @@ -541,7 +545,7 @@ public class TvOverlayManager { * Shows DVR manager. */ public void showDvrManager() { - Intent intent = new Intent(mMainActivity, DvrActivity.class); + Intent intent = new Intent(mMainActivity, DvrBrowseActivity.class); mMainActivity.startActivity(intent); } @@ -563,6 +567,14 @@ public class TvOverlayManager { new RecentlyWatchedDialogFragment(), false); } + /** + * Shows DVR history dialog. + */ + public void showDvrHistoryDialog() { + showDialogFragment(DvrHistoryDialogFragment.DIALOG_TAG, + new DvrHistoryDialogFragment(), false); + } + /** * Shows banner view. */ @@ -674,7 +686,7 @@ public class TvOverlayManager { } if ((flags & FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS) != 0) { // Keeps side panels. - } else if (mSideFragmentManager.isSidePanelVisible()) { + } else if (mSideFragmentManager.isActive()) { if ((flags & FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANEL_HISTORY) != 0) { mSideFragmentManager.hideSidePanel(withAnimation); } else { diff --git a/src/com/android/tv/ui/TvViewUiManager.java b/src/com/android/tv/ui/TvViewUiManager.java index bf874fc7..8d3b14f7 100644 --- a/src/com/android/tv/ui/TvViewUiManager.java +++ b/src/com/android/tv/ui/TvViewUiManager.java @@ -52,9 +52,8 @@ import com.android.tv.data.DisplayMode; import com.android.tv.util.TvSettings; /** - * The TvViewUiManager is responsible for handling UI layouting and animation of main and PIP - * TvViews. It also control the settings regarding TvView UI such as display mode, PIP layout, - * and PIP size. + * The TvViewUiManager is responsible for handling UI layouting and animation of main TvView. + * It also control the settings regarding TvView UI such as display mode. */ public class TvViewUiManager { private static final String TAG = "TvViewManager"; @@ -69,18 +68,11 @@ public class TvViewUiManager { private final Resources mResources; private final FrameLayout mContentView; private final TunableTvView mTvView; - private final TunableTvView mPipView; private final TvOptionsManager mTvOptionsManager; - private final int mTvViewPapWidth; private final int mTvViewShrunkenStartMargin; private final int mTvViewShrunkenEndMargin; - private final int mTvViewPapStartMargin; - private final int mTvViewPapEndMargin; private int mWindowWidth; private int mWindowHeight; - private final int mPipViewHorizontalMargin; - private final int mPipViewTopMargin; - private final int mPipViewBottomMargin; private final SharedPreferences mSharedPreferences; private final TimeInterpolator mLinearOutSlowIn; private final TimeInterpolator mFastOutLinearIn; @@ -113,9 +105,6 @@ public class TvViewUiManager { private boolean mIsUnderShrunkenTvView; private int mTvViewStartMargin; private int mTvViewEndMargin; - private int mPipLayout; - private int mPipSize; - private boolean mPipStarted; private ObjectAnimator mTvViewAnimator; private FrameLayout.LayoutParams mTvViewLayoutParams; // TV view's position when the display mode is FULL. It is used to compute PIP location relative @@ -130,12 +119,11 @@ public class TvViewUiManager { private int mAppliedTvViewEndMargin; private float mAppliedVideoDisplayAspectRatio; - public TvViewUiManager(Context context, TunableTvView tvView, TunableTvView pipView, + public TvViewUiManager(Context context, TunableTvView tvView, FrameLayout contentView, TvOptionsManager tvOptionManager) { mContext = context; mResources = mContext.getResources(); mTvView = tvView; - mPipView = pipView; mContentView = contentView; mTvOptionsManager = tvOptionManager; @@ -147,18 +135,12 @@ public class TvViewUiManager { mWindowWidth = size.x; mWindowHeight = size.y; - // Have an assumption that PIP and TvView Shrinking happens only in full screen. + // Have an assumption that TvView Shrinking happens only in full screen. mTvViewShrunkenStartMargin = mResources .getDimensionPixelOffset(R.dimen.shrunken_tvview_margin_start); mTvViewShrunkenEndMargin = mResources.getDimensionPixelOffset(R.dimen.shrunken_tvview_margin_end) + mResources.getDimensionPixelSize(R.dimen.side_panel_width); - int papMarginHorizontal = mResources - .getDimensionPixelOffset(R.dimen.papview_margin_horizontal); - int papSpacing = mResources.getDimensionPixelOffset(R.dimen.papview_spacing); - mTvViewPapWidth = (mWindowWidth - papSpacing) / 2 - papMarginHorizontal; - mTvViewPapStartMargin = papMarginHorizontal + mTvViewPapWidth + papSpacing; - mTvViewPapEndMargin = papMarginHorizontal; mTvViewFrame = createMarginLayoutParams(0, 0, 0, 0); mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(mContext); @@ -167,11 +149,6 @@ public class TvViewUiManager { .loadInterpolator(mContext, android.R.interpolator.linear_out_slow_in); mFastOutLinearIn = AnimationUtils .loadInterpolator(mContext, android.R.interpolator.fast_out_linear_in); - - mPipViewHorizontalMargin = mResources - .getDimensionPixelOffset(R.dimen.pipview_margin_horizontal); - mPipViewTopMargin = mResources.getDimensionPixelOffset(R.dimen.pipview_margin_top); - mPipViewBottomMargin = mResources.getDimensionPixelOffset(R.dimen.pipview_margin_bottom); } public void onConfigurationChanged(final int windowWidth, final int windowHeight) { @@ -200,18 +177,11 @@ public class TvViewUiManager { */ public void startShrunkenTvView() { mIsUnderShrunkenTvView = true; + mTvView.setIsUnderShrunken(true); mTvViewStartMarginBeforeShrunken = mTvViewStartMargin; mTvViewEndMarginBeforeShrunken = mTvViewEndMargin; - if (mPipStarted && getPipLayout() == TvSettings.PIP_LAYOUT_SIDE_BY_SIDE) { - float sidePanelWidth = mResources.getDimensionPixelOffset(R.dimen.side_panel_width); - float factor = 1.0f - sidePanelWidth / mWindowWidth; - int startMargin = (int) (mTvViewPapStartMargin * factor); - int endMargin = (int) (mTvViewPapEndMargin * factor + sidePanelWidth); - setTvViewMargin(startMargin, endMargin); - } else { - setTvViewMargin(mTvViewShrunkenStartMargin, mTvViewShrunkenEndMargin); - } + setTvViewMargin(mTvViewShrunkenStartMargin, mTvViewShrunkenEndMargin); mDisplayModeBeforeShrunken = setDisplayMode(DisplayMode.MODE_NORMAL, false, true); } @@ -221,6 +191,7 @@ public class TvViewUiManager { */ public void endShrunkenTvView() { mIsUnderShrunkenTvView = false; + mTvView.setIsUnderShrunken(false); setTvViewMargin(mTvViewStartMarginBeforeShrunken, mTvViewEndMarginBeforeShrunken); setDisplayMode(mDisplayModeBeforeShrunken, false, true); } @@ -326,120 +297,6 @@ public class TvViewUiManager { } } - /** - * Returns the current PIP layout. The layout should be one of - * {@link TvSettings#PIP_LAYOUT_BOTTOM_RIGHT}, {@link TvSettings#PIP_LAYOUT_TOP_RIGHT}, - * {@link TvSettings#PIP_LAYOUT_TOP_LEFT}, {@link TvSettings#PIP_LAYOUT_BOTTOM_LEFT} and - * {@link TvSettings#PIP_LAYOUT_SIDE_BY_SIDE}. - */ - public int getPipLayout() { - return mPipLayout; - } - - /** - * Sets the PIP layout. The layout should be one of - * {@link TvSettings#PIP_LAYOUT_BOTTOM_RIGHT}, {@link TvSettings#PIP_LAYOUT_TOP_RIGHT}, - * {@link TvSettings#PIP_LAYOUT_TOP_LEFT}, {@link TvSettings#PIP_LAYOUT_BOTTOM_LEFT} and - * {@link TvSettings#PIP_LAYOUT_SIDE_BY_SIDE}. - * - * @param storeInPreference if true, the stored value will be restored by - * {@link #restorePipLayout()}. - */ - public void setPipLayout(int pipLayout, boolean storeInPreference) { - mPipLayout = pipLayout; - if (storeInPreference) { - TvSettings.setPipLayout(mContext, pipLayout); - } - updatePipView(mTvViewFrame); - if (mPipLayout == TvSettings.PIP_LAYOUT_SIDE_BY_SIDE) { - setTvViewMargin(mTvViewPapStartMargin, mTvViewPapEndMargin); - setDisplayMode(DisplayMode.MODE_NORMAL, false, false); - } else { - setTvViewMargin(0, 0); - restoreDisplayMode(false); - } - mTvOptionsManager.onPipLayoutChanged(pipLayout); - } - - /** - * Restores the PIP layout which {@link #setPipLayout} lastly stores. - */ - public void restorePipLayout() { - setPipLayout(TvSettings.getPipLayout(mContext), false); - } - - /** - * Called when PIP is started. - */ - public void onPipStart() { - mPipStarted = true; - updatePipView(); - mPipView.setVisibility(View.VISIBLE); - } - - /** - * Called when PIP is stopped. - */ - public void onPipStop() { - setTvViewMargin(0, 0); - mPipView.setVisibility(View.GONE); - mPipStarted = false; - } - - /** - * Called when PIP is resumed. - */ - public void showPipForResume() { - mPipView.setVisibility(View.VISIBLE); - } - - /** - * Called when PIP is paused. - */ - public void hidePipForPause() { - if (mPipLayout != TvSettings.PIP_LAYOUT_SIDE_BY_SIDE) { - mPipView.setVisibility(View.GONE); - } - } - - /** - * Updates PIP view. It is usually called, when video resolution in PIP is updated. - */ - public void updatePipView() { - updatePipView(mTvViewFrame); - } - - /** - * Returns the size of the PIP view. - */ - public int getPipSize() { - return mPipSize; - } - - /** - * Sets PIP size and applies it immediately. - * - * @param pipSize PIP size. The value should be one of {@link TvSettings#PIP_SIZE_BIG} - * and {@link TvSettings#PIP_SIZE_SMALL}. - * @param storeInPreference if true, the stored value will be restored by - * {@link #restorePipSize()}. - */ - public void setPipSize(int pipSize, boolean storeInPreference) { - mPipSize = pipSize; - if (storeInPreference) { - TvSettings.setPipSize(mContext, pipSize); - } - updatePipView(mTvViewFrame); - mTvOptionsManager.onPipSizeChanged(pipSize); - } - - /** - * Restores the PIP size which {@link #setPipSize} lastly stores. - */ - public void restorePipSize() { - setPipSize(TvSettings.getPipSize(mContext), false); - } - /** * This margins will be applied when applyDisplayMode is called. */ @@ -540,113 +397,6 @@ public class TvViewUiManager { } else { mTvView.setLayoutParams(layoutParams); } - updatePipView(mTvViewFrame); - } - } - - /** - * The redlines assume that the ratio of the TV screen is 16:9. If the radio is not 16:9, the - * layout of PAP can be broken. - */ - @SuppressLint("RtlHardcoded") - private void updatePipView(MarginLayoutParams tvViewFrame) { - if (!mPipStarted) { - return; - } - int width; - int height; - int startMargin; - int endMargin; - int topMargin; - int bottomMargin; - int gravity; - - if (mPipLayout == TvSettings.PIP_LAYOUT_SIDE_BY_SIDE) { - gravity = Gravity.CENTER_VERTICAL | Gravity.START; - height = tvViewFrame.height; - float videoDisplayAspectRatio = mPipView.getVideoDisplayAspectRatio(); - if (videoDisplayAspectRatio <= 0f) { - width = tvViewFrame.width; - } else { - width = (int) (height * videoDisplayAspectRatio); - if (width > tvViewFrame.width) { - width = tvViewFrame.width; - } - } - startMargin = mResources.getDimensionPixelOffset(R.dimen.papview_margin_horizontal) - * tvViewFrame.width / mTvViewPapWidth + (tvViewFrame.width - width) / 2; - endMargin = 0; - topMargin = 0; - bottomMargin = 0; - } else { - int tvViewWidth = tvViewFrame.width; - int tvViewHeight = tvViewFrame.height; - int tvStartMargin = tvViewFrame.getMarginStart(); - int tvEndMargin = tvViewFrame.getMarginEnd(); - int tvTopMargin = tvViewFrame.topMargin; - int tvBottomMargin = tvViewFrame.bottomMargin; - float horizontalScaleFactor = (float) tvViewWidth / mWindowWidth; - float verticalScaleFactor = (float) tvViewHeight / mWindowHeight; - - int maxWidth; - if (mPipSize == TvSettings.PIP_SIZE_SMALL) { - maxWidth = (int) (mResources.getDimensionPixelSize(R.dimen.pipview_small_size_width) - * horizontalScaleFactor); - height = (int) (mResources.getDimensionPixelSize(R.dimen.pipview_small_size_height) - * verticalScaleFactor); - } else if (mPipSize == TvSettings.PIP_SIZE_BIG) { - maxWidth = (int) (mResources.getDimensionPixelSize(R.dimen.pipview_large_size_width) - * horizontalScaleFactor); - height = (int) (mResources.getDimensionPixelSize(R.dimen.pipview_large_size_height) - * verticalScaleFactor); - } else { - throw new IllegalArgumentException("Invalid PIP size: " + mPipSize); - } - float videoDisplayAspectRatio = mPipView.getVideoDisplayAspectRatio(); - if (videoDisplayAspectRatio <= 0f) { - width = maxWidth; - } else { - width = (int) (height * videoDisplayAspectRatio); - if (width > maxWidth) { - width = maxWidth; - } - } - - startMargin = tvStartMargin + (int) (mPipViewHorizontalMargin * horizontalScaleFactor); - endMargin = tvEndMargin + (int) (mPipViewHorizontalMargin * horizontalScaleFactor); - topMargin = tvTopMargin + (int) (mPipViewTopMargin * verticalScaleFactor); - bottomMargin = tvBottomMargin + (int) (mPipViewBottomMargin * verticalScaleFactor); - - switch (mPipLayout) { - case TvSettings.PIP_LAYOUT_TOP_LEFT: - gravity = Gravity.TOP | Gravity.LEFT; - break; - case TvSettings.PIP_LAYOUT_TOP_RIGHT: - gravity = Gravity.TOP | Gravity.RIGHT; - break; - case TvSettings.PIP_LAYOUT_BOTTOM_LEFT: - gravity = Gravity.BOTTOM | Gravity.LEFT; - break; - case TvSettings.PIP_LAYOUT_BOTTOM_RIGHT: - gravity = Gravity.BOTTOM | Gravity.RIGHT; - break; - default: - throw new IllegalArgumentException("Invalid PIP location: " + mPipLayout); - } - } - - FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) mPipView.getLayoutParams(); - if (lp.width != width || lp.height != height || lp.getMarginStart() != startMargin - || lp.getMarginEnd() != endMargin || lp.topMargin != topMargin - || lp.bottomMargin != bottomMargin || lp.gravity != gravity) { - lp.width = width; - lp.height = height; - lp.setMarginStart(startMargin); - lp.setMarginEnd(endMargin); - lp.topMargin = topMargin; - lp.bottomMargin = bottomMargin; - lp.gravity = gravity; - mPipView.setLayoutParams(lp); } } @@ -696,7 +446,6 @@ public class TvViewUiManager { mLastAnimatedTvViewFrame = new MarginLayoutParams(0, 0); interpolateMarginsRelative(mLastAnimatedTvViewFrame, mOldTvViewFrame, mTvViewFrame, fraction); - updatePipView(mLastAnimatedTvViewFrame); } }); } diff --git a/src/com/android/tv/ui/sidepanel/ClosedCaptionFragment.java b/src/com/android/tv/ui/sidepanel/ClosedCaptionFragment.java index d6ccdf6b..e5c23372 100644 --- a/src/com/android/tv/ui/sidepanel/ClosedCaptionFragment.java +++ b/src/com/android/tv/ui/sidepanel/ClosedCaptionFragment.java @@ -82,8 +82,9 @@ public class ClosedCaptionFragment extends SideFragment { } mItems.add(item); - for (final TvTrackInfo track : tracks) { - item = new ClosedCaptionOptionItem(getLabel(track), + for (int i = 0; i < tracks.size(); i++) { + final TvTrackInfo track = tracks.get(i); + item = new ClosedCaptionOptionItem(getLabel(track, i), CaptionSettings.OPTION_ON, track.getId(), track.getLanguage()); if (isEnabled && track.getId().equals(trackId)) { item.setChecked(true); @@ -172,11 +173,11 @@ public class ClosedCaptionFragment extends SideFragment { super.onDestroyView(); } - private String getLabel(TvTrackInfo track) { + private String getLabel(TvTrackInfo track, int trackIndex) { if (track.getLanguage() != null) { return new Locale(track.getLanguage()).getDisplayName(); } - return getString(R.string.default_language); + return getString(R.string.closed_caption_unknown_language, trackIndex + 1); } private class ClosedCaptionOptionItem extends RadioButtonItem { diff --git a/src/com/android/tv/ui/sidepanel/DeveloperOptionFragment.java b/src/com/android/tv/ui/sidepanel/DeveloperOptionFragment.java index 0d189cca..fac24696 100644 --- a/src/com/android/tv/ui/sidepanel/DeveloperOptionFragment.java +++ b/src/com/android/tv/ui/sidepanel/DeveloperOptionFragment.java @@ -18,8 +18,6 @@ package com.android.tv.ui.sidepanel; import android.accounts.Account; import android.app.Activity; -import android.app.ApplicationErrorReport; -import android.content.Intent; import android.support.annotation.NonNull; import android.util.Log; import android.widget.Toast; @@ -27,6 +25,7 @@ import android.widget.Toast; import com.android.tv.R; import com.android.tv.TvApplication; import com.android.tv.common.BuildConfig; +import com.android.tv.common.feature.CommonFeatures; import com.android.tv.data.epg.EpgFetcher; import com.android.tv.experiments.Experiments; import com.android.tv.tuner.TunerPreferences; @@ -54,6 +53,14 @@ public class DeveloperOptionFragment extends SideFragment { @Override protected List getItemList() { List items = new ArrayList<>(); + if (CommonFeatures.DVR.isEnabled(getContext())) { + items.add(new ActionItem(getString(R.string.dev_item_dvr_history)) { + @Override + protected void onSelected() { + getMainActivity().getOverlayManager().showDvrHistoryDialog(); + } + }); + } if (BuildConfig.ENG) { items.add(new ActionItem(getString(R.string.dev_item_watch_history)) { @Override @@ -62,18 +69,6 @@ public class DeveloperOptionFragment extends SideFragment { } }); } - items.add(new ActionItem(getString(R.string.dev_item_send_feedback)) { - @Override - protected void onSelected() { - Intent intent = new Intent(Intent.ACTION_APP_ERROR); - ApplicationErrorReport report = new ApplicationErrorReport(); - report.packageName = report.processName = getContext().getPackageName(); - report.time = System.currentTimeMillis(); - report.type = ApplicationErrorReport.TYPE_NONE; - intent.putExtra(Intent.EXTRA_BUG_REPORT, report); - startActivityForResult(intent, 0); - } - }); items.add(new SwitchItem(getString(R.string.dev_item_store_ts_on), getString(R.string.dev_item_store_ts_off), getString(R.string.dev_item_store_ts_description)) { diff --git a/src/com/android/tv/ui/sidepanel/Item.java b/src/com/android/tv/ui/sidepanel/Item.java index 00f16427..4e47e75b 100644 --- a/src/com/android/tv/ui/sidepanel/Item.java +++ b/src/com/android/tv/ui/sidepanel/Item.java @@ -24,6 +24,7 @@ import android.view.ViewGroup; public abstract class Item { private View mItemView; private boolean mEnabled = true; + private boolean mClickable = true; public void setEnabled(boolean enabled) { if (mEnabled != enabled) { @@ -34,6 +35,16 @@ public abstract class Item { } } + /** + * Sets the item to be clickable or not. + */ + public void setClickable(boolean clickable) { + mClickable = clickable; + if (mItemView != null) { + mItemView.setClickable(clickable); + } + } + /** * Returns whether this item is enabled. */ @@ -64,6 +75,7 @@ public abstract class Item { */ protected void onUpdate() { setEnabledInternal(mItemView, mEnabled); + mItemView.setClickable(mClickable); } protected abstract void onSelected(); diff --git a/src/com/android/tv/ui/sidepanel/PipInputSelectorFragment.java b/src/com/android/tv/ui/sidepanel/PipInputSelectorFragment.java deleted file mode 100644 index dec017a8..00000000 --- a/src/com/android/tv/ui/sidepanel/PipInputSelectorFragment.java +++ /dev/null @@ -1,170 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.ui.sidepanel; - -import android.media.tv.TvInputInfo; -import android.os.Bundle; -import android.text.TextUtils; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import com.android.tv.R; -import com.android.tv.util.PipInputManager; -import com.android.tv.util.PipInputManager.PipInput; - -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; - -public class PipInputSelectorFragment extends SideFragment { - private static final String TAG = "PipInputSelector"; - private static final String TRACKER_LABEL = "PIP input source"; - - private final List mInputItems = new ArrayList<>(); - private PipInputManager mPipInputManager; - private PipInput mInitialPipInput; - private boolean mSelected; - - private final PipInputManager.Listener mPipInputListener = new PipInputManager.Listener() { - @Override - public void onPipInputStateUpdated() { - notifyDataSetChanged(); - } - - @Override - public void onPipInputListUpdated() { - refreshInputList(); - setItems(mInputItems); - } - }; - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - mPipInputManager = getMainActivity().getPipInputManager(); - mPipInputManager.addListener(mPipInputListener); - getMainActivity().startShrunkenTvView(false, false); - return super.onCreateView(inflater, container, savedInstanceState); - } - - @Override - public void onStart() { - super.onStart(); - mInitialPipInput = mPipInputManager.getPipInput(getMainActivity().getPipChannel()); - if (mInitialPipInput == null) { - Log.w(TAG, "PIP should be on"); - closeFragment(); - } - int count = 0; - for (Item item : mInputItems) { - InputItem inputItem = (InputItem) item; - if (Objects.equals(inputItem.mPipInput, mInitialPipInput)) { - setSelectedPosition(count); - break; - } - ++count; - } - } - - @Override - public void onDestroyView() { - super.onDestroyView(); - mPipInputManager.removeListener(mPipInputListener); - if (!mSelected) { - getMainActivity().tuneToChannelForPip(mInitialPipInput.getChannel()); - } - getMainActivity().endShrunkenTvView(); - } - - @Override - protected String getTitle() { - return getString(R.string.side_panel_title_pip_input_source); - } - - @Override - public String getTrackerLabel() { - return TRACKER_LABEL; - } - - @Override - protected List getItemList() { - refreshInputList(); - return mInputItems; - } - - private void refreshInputList() { - mInputItems.clear(); - for (PipInput input : mPipInputManager.getPipInputList(false)) { - mInputItems.add(new InputItem(input)); - } - } - - private class InputItem extends RadioButtonItem { - private final PipInput mPipInput; - - private InputItem(PipInput input) { - super(input.getLongLabel()); - mPipInput = input; - setEnabled(isAvailable()); - } - - @Override - protected void onUpdate() { - super.onUpdate(); - setEnabled(mPipInput.isAvailable()); - setChecked(mPipInput == mInitialPipInput); - } - - @Override - protected void onFocused() { - super.onFocused(); - if (isEnabled()) { - getMainActivity().tuneToChannelForPip(mPipInput.getChannel()); - } - } - - @Override - protected void onSelected() { - super.onSelected(); - if (isEnabled()) { - mSelected = true; - closeFragment(); - } - } - - private boolean isAvailable() { - if (!mPipInput.isAvailable()) { - return false; - } - - // If this input shares the same parent with the current main input, you cannot select - // it. (E.g. two HDMI CEC devices that are connected to HDMI port 1 through an A/V - // receiver.) - PipInput pipInput = mPipInputManager.getPipInput(getMainActivity().getCurrentChannel()); - if (pipInput == null) { - return false; - } - TvInputInfo mainInputInfo = pipInput.getInputInfo(); - TvInputInfo pipInputInfo = mPipInput.getInputInfo(); - return mainInputInfo == null || pipInputInfo == null - || !TextUtils.equals(mainInputInfo.getId(), pipInputInfo.getId()) - && !TextUtils.equals(mainInputInfo.getParentId(), pipInputInfo.getParentId()); - } - } -} diff --git a/src/com/android/tv/ui/sidepanel/SettingsFragment.java b/src/com/android/tv/ui/sidepanel/SettingsFragment.java index e8033a22..f6aa4f86 100644 --- a/src/com/android/tv/ui/sidepanel/SettingsFragment.java +++ b/src/com/android/tv/ui/sidepanel/SettingsFragment.java @@ -16,6 +16,8 @@ package com.android.tv.ui.sidepanel; +import android.app.ApplicationErrorReport; +import android.content.Intent; import android.view.View; import android.widget.Toast; @@ -149,12 +151,26 @@ public class SettingsFragment extends SideFragment { // But, we may be able to turn on channel lock feature regardless of the permission. // It's TBD. } + items.add(new ActionItem(getString(R.string.settings_send_feedback)) { + @Override + protected void onSelected() { + Intent intent = new Intent(Intent.ACTION_APP_ERROR); + ApplicationErrorReport report = new ApplicationErrorReport(); + report.packageName = report.processName = getContext().getPackageName(); + report.time = System.currentTimeMillis(); + report.type = ApplicationErrorReport.TYPE_NONE; + intent.putExtra(Intent.EXTRA_BUG_REPORT, report); + startActivityForResult(intent, 0); + } + }); if (LicenseUtils.hasLicenses(activity.getAssets())) { items.add(new LicenseActionItem(activity)); } // Show version. - items.add(new SimpleItem(getString(R.string.settings_menu_version), - ((TvApplication) activity.getApplicationContext()).getVersionName())); + SimpleActionItem version = new SimpleActionItem(getString(R.string.settings_menu_version), + ((TvApplication) activity.getApplicationContext()).getVersionName()); + version.setClickable(false); + items.add(version); return items; } diff --git a/src/com/android/tv/ui/sidepanel/SideFragment.java b/src/com/android/tv/ui/sidepanel/SideFragment.java index 8df56cd2..bb815eb8 100644 --- a/src/com/android/tv/ui/sidepanel/SideFragment.java +++ b/src/com/android/tv/ui/sidepanel/SideFragment.java @@ -26,24 +26,26 @@ import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.LinearLayout; import android.widget.TextView; import com.android.tv.MainActivity; import com.android.tv.R; import com.android.tv.TvApplication; -import com.android.tv.analytics.DurationTimer; +import com.android.tv.util.DurationTimer; import com.android.tv.analytics.HasTrackerLabel; import com.android.tv.analytics.Tracker; import com.android.tv.data.ChannelDataManager; import com.android.tv.data.ProgramDataManager; import com.android.tv.util.SystemProperties; +import com.android.tv.util.ViewCache; import java.util.List; public abstract class SideFragment extends Fragment implements HasTrackerLabel { public static final int INVALID_POSITION = -1; - private static final int RECYCLED_VIEW_POOL_SIZE = 7; + private static final int PRELOADED_VIEW_SIZE = 7; private static final int[] PRELOADED_VIEW_IDS = { R.layout.option_item_radio_button, R.layout.option_item_channel_lock, @@ -51,7 +53,8 @@ public abstract class SideFragment extends Fragment implements HasTrackerLabel { R.layout.option_item_channel_check }; - private static RecyclerView.RecycledViewPool sRecycledViewPool; + private static RecyclerView.RecycledViewPool sRecycledViewPool = + new RecyclerView.RecycledViewPool(); private VerticalGridView mListView; private ItemAdapter mAdapter; @@ -89,13 +92,6 @@ public abstract class SideFragment extends Fragment implements HasTrackerLabel { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - if (sRecycledViewPool == null) { - // sRecycledViewPool should be initialized by calling preloadRecycledViews() - // before the entering animation of this fragment starts, - // because it takes long time and if it is called after the animation starts (e.g. here) - // it can affect the animation. - throw new IllegalStateException("The RecyclerView pool has not been initialized."); - } View view = inflater.inflate(getFragmentLayoutResourceId(), container, false); TextView textView = (TextView) view.findViewById(R.id.side_panel_title); @@ -236,30 +232,27 @@ public abstract class SideFragment extends Fragment implements HasTrackerLabel { } /** - * Preloads the view holders. + * Preloads the item views. */ - public static void preloadRecycledViews(Context context) { - if (sRecycledViewPool != null) { - return; - } - sRecycledViewPool = new RecyclerView.RecycledViewPool(); + public static void preloadItemViews(Context context) { LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + // Use a fake parent to make the layoutParams set correctly. + ViewGroup fakeParent = new LinearLayout(context); for (int id : PRELOADED_VIEW_IDS) { - sRecycledViewPool.setMaxRecycledViews(id, RECYCLED_VIEW_POOL_SIZE); - for (int j = 0; j < RECYCLED_VIEW_POOL_SIZE; ++j) { - ItemAdapter.ViewHolder viewHolder = new ItemAdapter.ViewHolder( - inflater.inflate(id, null, false)); - sRecycledViewPool.putRecycledView(viewHolder); + sRecycledViewPool.setMaxRecycledViews(id, PRELOADED_VIEW_SIZE); + for (int j = 0; j < PRELOADED_VIEW_SIZE; ++j) { + View view = inflater.inflate(id, fakeParent, false); + ViewCache.getInstance().putView(id, view); } } } /** - * Releases the pre-loaded view holders. + * Releases the recycled view pool. */ - public static void releasePreloadedRecycledViews() { - sRecycledViewPool = null; + public static void releaseRecycledViewPool() { + sRecycledViewPool.clear(); } private static class ItemAdapter extends RecyclerView.Adapter { @@ -278,7 +271,11 @@ public abstract class SideFragment extends Fragment implements HasTrackerLabel { @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - return new ViewHolder(mLayoutInflater.inflate(viewType, parent, false)); + View view = ViewCache.getInstance().getView(viewType); + if (view == null) { + view = mLayoutInflater.inflate(viewType, parent, false); + } + return new ViewHolder(view); } @Override diff --git a/src/com/android/tv/ui/sidepanel/SideFragmentManager.java b/src/com/android/tv/ui/sidepanel/SideFragmentManager.java index 553cd9d7..4398b3f3 100644 --- a/src/com/android/tv/ui/sidepanel/SideFragmentManager.java +++ b/src/com/android/tv/ui/sidepanel/SideFragmentManager.java @@ -99,7 +99,6 @@ public class SideFragmentManager { * Shows the given {@link SideFragment}. */ public void show(SideFragment sideFragment, boolean showEnterAnimation) { - SideFragment.preloadRecycledViews(mActivity); if (isHiding()) { mHideAnimator.end(); } @@ -178,7 +177,6 @@ public class SideFragmentManager { * @param withAnimation specifies if animation should be shown. */ public void showSidePanel(boolean withAnimation) { - SideFragment.preloadRecycledViews(mActivity); if (mFragmentCount == 0) { return; } diff --git a/src/com/android/tv/ui/sidepanel/SimpleActionItem.java b/src/com/android/tv/ui/sidepanel/SimpleActionItem.java new file mode 100644 index 00000000..42553b66 --- /dev/null +++ b/src/com/android/tv/ui/sidepanel/SimpleActionItem.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.ui.sidepanel; + +/** + * A simple item which shows title and description. + */ +public class SimpleActionItem extends ActionItem { + public SimpleActionItem(String title) { + super(title); + } + + public SimpleActionItem(String title, String description) { + super(title, description); + } + + @Override + protected void onSelected() { + } +} diff --git a/src/com/android/tv/ui/sidepanel/SimpleItem.java b/src/com/android/tv/ui/sidepanel/SimpleItem.java deleted file mode 100644 index 52a5f13f..00000000 --- a/src/com/android/tv/ui/sidepanel/SimpleItem.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.ui.sidepanel; - -/** - * A simple item which shows title and description. - */ -public class SimpleItem extends ActionItem { - public SimpleItem(String title) { - super(title); - } - - public SimpleItem(String title, String description) { - super(title, description); - } - - @Override - protected void onSelected() { - } -} diff --git a/src/com/android/tv/util/AsyncDbTask.java b/src/com/android/tv/util/AsyncDbTask.java index 78243642..3cc91e40 100644 --- a/src/com/android/tv/util/AsyncDbTask.java +++ b/src/com/android/tv/util/AsyncDbTask.java @@ -31,7 +31,7 @@ import android.util.Range; import com.android.tv.common.SoftPreconditions; import com.android.tv.data.Channel; import com.android.tv.data.Program; -import com.android.tv.dvr.RecordedProgram; +import com.android.tv.dvr.data.RecordedProgram; import java.util.ArrayList; import java.util.List; diff --git a/src/com/android/tv/util/Debug.java b/src/com/android/tv/util/Debug.java new file mode 100644 index 00000000..67a2683d --- /dev/null +++ b/src/com/android/tv/util/Debug.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.util; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * A class only for help developers. + */ +public class Debug { + /** + * A threshold of start up time, when the start up time of Live TV is more than it, + * a warning will show to the developer. + */ + public static final long TIME_START_UP_DURATION_THRESHOLD = TimeUnit.SECONDS.toMillis(6); + /** + * Tag for measuring start up time of Live TV. + */ + public static final String TAG_START_UP_TIMER = "start_up_timer"; + + /** + * A global map for duration timers. + */ + private final static Map sTimerMap = new HashMap<>(); + + /** + * Returns the global duration timer by tag. + */ + public static DurationTimer getTimer(String tag) { + if (sTimerMap.get(tag) != null) { + return sTimerMap.get(tag); + } + DurationTimer timer = new DurationTimer(tag, true); + sTimerMap.put(tag, timer); + return timer; + } + + /** + * Removes the global duration timer by tag. + */ + public static DurationTimer removeTimer(String tag) { + return sTimerMap.remove(tag); + } +} diff --git a/src/com/android/tv/util/DurationTimer.java b/src/com/android/tv/util/DurationTimer.java new file mode 100644 index 00000000..b6221496 --- /dev/null +++ b/src/com/android/tv/util/DurationTimer.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.util; + +import android.os.SystemClock; +import android.util.Log; + +import com.android.tv.common.BuildConfig; + +/** + * Times a duration. + */ +public final class DurationTimer { + private static final String TAG = "DurationTimer"; + public static final long TIME_NOT_SET = -1; + + private long startTimeMs = TIME_NOT_SET; + private String mTag = TAG; + private boolean mLogEngOnly; + + public DurationTimer() { } + + public DurationTimer(String tag, boolean logEngOnly) { + mTag = tag; + mLogEngOnly = logEngOnly; + } + + /** + * Returns true if the timer is running. + */ + public boolean isRunning() { + return startTimeMs != TIME_NOT_SET; + } + + /** + * Start the timer. + */ + public void start() { + startTimeMs = SystemClock.elapsedRealtime(); + } + + /** + * Returns the current duration in milliseconds or {@link #TIME_NOT_SET} if the timer is not + * running. + */ + public long getDuration() { + return isRunning() ? SystemClock.elapsedRealtime() - startTimeMs : TIME_NOT_SET; + } + + /** + * Stops the timer and resets its value to {@link #TIME_NOT_SET}. + * + * @return the current duration in milliseconds or {@link #TIME_NOT_SET} if the timer is not + * running. + */ + public long reset() { + long duration = getDuration(); + startTimeMs = TIME_NOT_SET; + return duration; + } + + /** + * Adds information and duration time to the log. + */ + public void log(String message) { + if (isRunning() && (!mLogEngOnly || BuildConfig.ENG)) { + Log.i(mTag, message + " : " + getDuration() + "ms"); + } + } +} diff --git a/src/com/android/tv/util/LocationUtils.java b/src/com/android/tv/util/LocationUtils.java index 8e3b59e9..2ae6db18 100644 --- a/src/com/android/tv/util/LocationUtils.java +++ b/src/com/android/tv/util/LocationUtils.java @@ -16,7 +16,9 @@ package com.android.tv.util; +import android.Manifest; import android.content.Context; +import android.content.pm.PackageManager; import android.location.Address; import android.location.Geocoder; import android.location.Location; @@ -25,6 +27,7 @@ import android.location.LocationManager; import android.os.Bundle; import android.util.Log; +import com.android.tv.tuner.util.PostalCodeUtils; import java.io.IOException; import java.util.List; @@ -39,6 +42,7 @@ public class LocationUtils { private static Context sApplicationContext; private static Address sAddress; + private static String sCountry; private static IOException sError; /** @@ -59,6 +63,19 @@ public class LocationUtils { return null; } + /** + * Returns the current country. + */ + public static synchronized String getCurrentCountry(Context context) { + if (sCountry != null) { + return sCountry; + } + if (sCountry == null) { + sCountry = context.getResources().getConfiguration().locale.getCountry(); + } + return sCountry; + } + private static void updateAddress(Location location) { if (DEBUG) Log.d(TAG, "Updating address with " + location); if (location == null) { @@ -68,9 +85,14 @@ public class LocationUtils { try { List

addresses = geocoder.getFromLocation( location.getLatitude(), location.getLongitude(), 1); - if (addresses != null) { + if (addresses != null && !addresses.isEmpty()) { sAddress = addresses.get(0); if (DEBUG) Log.d(TAG, "Got " + sAddress); + try { + PostalCodeUtils.updatePostalCode(sApplicationContext); + } catch (Exception e) { + // Do nothing + } } else { if (DEBUG) Log.d(TAG, "No address returned"); } diff --git a/src/com/android/tv/util/Partner.java b/src/com/android/tv/util/Partner.java new file mode 100644 index 00000000..e3688392 --- /dev/null +++ b/src/com/android/tv/util/Partner.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.util; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.res.Resources; +import android.media.tv.TvInputInfo; +import android.text.TextUtils; +import android.util.Log; + +import java.util.HashMap; +import java.util.Map; + +/** + * This file refers to Partner.java in LeanbackLauncher. Interact with partner customizations. There + * can only be one set of customizations on a device, and it must be bundled with the system. + */ +public class Partner { + private static final String TAG = "Partner"; + /** Marker action used to discover partner */ + private static final String ACTION_PARTNER_CUSTOMIZATION = + "com.google.android.leanbacklauncher.action.PARTNER_CUSTOMIZATION"; + + /** ID tags for device input types */ + public static final String INPUT_TYPE_BUNDLED_TUNER = "input_type_combined_tuners"; + public static final String INPUT_TYPE_TUNER = "input_type_tuner"; + public static final String INPUT_TYPE_CEC_LOGICAL = "input_type_cec_logical"; + public static final String INPUT_TYPE_CEC_RECORDER = "input_type_cec_recorder"; + public static final String INPUT_TYPE_CEC_PLAYBACK = "input_type_cec_playback"; + public static final String INPUT_TYPE_MHL_MOBILE = "input_type_mhl_mobile"; + public static final String INPUT_TYPE_HDMI = "input_type_hdmi"; + public static final String INPUT_TYPE_DVI = "input_type_dvi"; + public static final String INPUT_TYPE_COMPONENT = "input_type_component"; + public static final String INPUT_TYPE_SVIDEO = "input_type_svideo"; + public static final String INPUT_TYPE_COMPOSITE = "input_type_composite"; + public static final String INPUT_TYPE_DISPLAY_PORT = "input_type_displayport"; + public static final String INPUT_TYPE_VGA = "input_type_vga"; + public static final String INPUT_TYPE_SCART = "input_type_scart"; + public static final String INPUT_TYPE_OTHER = "input_type_other"; + + private static final String INPUTS_ORDER = "home_screen_inputs_ordering"; + private static final String TYPE_ARRAY = "array"; + + private static Partner sPartner; + private static final Object sLock = new Object(); + + private final String mPackageName; + private final String mReceiverName; + private final Resources mResources; + + private static final Map INPUT_TYPE_MAP = new HashMap<>(); + static { + INPUT_TYPE_MAP.put(INPUT_TYPE_BUNDLED_TUNER, TvInputManagerHelper.TYPE_BUNDLED_TUNER); + INPUT_TYPE_MAP.put(INPUT_TYPE_TUNER, TvInputInfo.TYPE_TUNER); + INPUT_TYPE_MAP.put(INPUT_TYPE_CEC_LOGICAL, TvInputManagerHelper.TYPE_CEC_DEVICE); + INPUT_TYPE_MAP.put(INPUT_TYPE_CEC_RECORDER, TvInputManagerHelper.TYPE_CEC_DEVICE_RECORDER); + INPUT_TYPE_MAP.put(INPUT_TYPE_CEC_PLAYBACK, TvInputManagerHelper.TYPE_CEC_DEVICE_PLAYBACK); + INPUT_TYPE_MAP.put(INPUT_TYPE_MHL_MOBILE, TvInputManagerHelper.TYPE_MHL_MOBILE); + INPUT_TYPE_MAP.put(INPUT_TYPE_HDMI, TvInputInfo.TYPE_HDMI); + INPUT_TYPE_MAP.put(INPUT_TYPE_DVI, TvInputInfo.TYPE_DVI); + INPUT_TYPE_MAP.put(INPUT_TYPE_COMPONENT, TvInputInfo.TYPE_COMPONENT); + INPUT_TYPE_MAP.put(INPUT_TYPE_SVIDEO, TvInputInfo.TYPE_SVIDEO); + INPUT_TYPE_MAP.put(INPUT_TYPE_COMPOSITE, TvInputInfo.TYPE_COMPOSITE); + INPUT_TYPE_MAP.put(INPUT_TYPE_DISPLAY_PORT, TvInputInfo.TYPE_DISPLAY_PORT); + INPUT_TYPE_MAP.put(INPUT_TYPE_VGA, TvInputInfo.TYPE_VGA); + INPUT_TYPE_MAP.put(INPUT_TYPE_SCART, TvInputInfo.TYPE_SCART); + INPUT_TYPE_MAP.put(INPUT_TYPE_OTHER, TvInputInfo.TYPE_OTHER); + } + + private Partner(String packageName, String receiverName, Resources res) { + mPackageName = packageName; + mReceiverName = receiverName; + mResources = res; + } + + /** Returns partner instance. */ + public static Partner getInstance(Context context) { + PackageManager pm = context.getPackageManager(); + synchronized (sLock) { + ResolveInfo info = getPartnerResolveInfo(pm); + if (info != null) { + final String packageName = info.activityInfo.packageName; + final String receiverName = info.activityInfo.name; + try { + final Resources res = pm.getResourcesForApplication(packageName); + sPartner = new Partner(packageName, receiverName, res); + sPartner.sendInitBroadcast(context); + } catch (PackageManager.NameNotFoundException e) { + Log.w(TAG, "Failed to find resources for " + packageName); + } + } + if (sPartner == null) { + sPartner = new Partner(null, null, null); + } + } + return sPartner; + } + + /** Resets the Partner instance to handle the partner package has changed. */ + public static void reset(Context context, String packageName) { + synchronized (sLock) { + if (sPartner != null && !TextUtils.isEmpty(packageName)) { + if (packageName.equals(sPartner.mPackageName)) { + // Force a refresh, so we send an Init to the updated package + sPartner = null; + getInstance(context); + } + } + } + } + + /** This method is used to send init broadcast to the new/changed partner package. */ + private void sendInitBroadcast(Context context) { + if (!TextUtils.isEmpty(mPackageName) && !TextUtils.isEmpty(mReceiverName)) { + Intent intent = new Intent(ACTION_PARTNER_CUSTOMIZATION); + final ComponentName componentName = new ComponentName(mPackageName, mReceiverName); + intent.setComponent(componentName); + intent.setFlags(Intent.FLAG_RECEIVER_FOREGROUND); + context.sendBroadcast(intent); + } + } + + /** Returns the order of inputs. */ + public Map getInputsOrderMap() { + HashMap map = new HashMap<>(); + if (mResources != null && !TextUtils.isEmpty(mPackageName)) { + String[] inputsArray = null; + final int resId = mResources.getIdentifier(INPUTS_ORDER, TYPE_ARRAY, mPackageName); + if (resId != 0) { + inputsArray = mResources.getStringArray(resId); + } + if (inputsArray != null) { + int priority = 0; + for (String input : inputsArray) { + Integer type = INPUT_TYPE_MAP.get(input); + if (type != null) { + map.put(type, priority++); + } + } + } + } + return map; + } + + private static ResolveInfo getPartnerResolveInfo(PackageManager pm) { + final Intent intent = new Intent(ACTION_PARTNER_CUSTOMIZATION); + ResolveInfo partnerInfo = null; + for (ResolveInfo info : pm.queryBroadcastReceivers(intent, 0)) { + if (isSystemApp(info)) { + partnerInfo = info; + break; + } + } + return partnerInfo; + } + + protected static boolean isSystemApp(ResolveInfo info) { + return (info.activityInfo != null + && info.activityInfo.applicationInfo != null + && (info.activityInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0); + } +} diff --git a/src/com/android/tv/util/PipInputManager.java b/src/com/android/tv/util/PipInputManager.java deleted file mode 100644 index 2c51d5a0..00000000 --- a/src/com/android/tv/util/PipInputManager.java +++ /dev/null @@ -1,432 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.util; - -import android.content.Context; -import android.media.tv.TvInputInfo; -import android.media.tv.TvInputManager; -import android.media.tv.TvInputManager.TvInputCallback; -import android.util.ArraySet; -import android.util.Log; - -import com.android.tv.ChannelTuner; -import com.android.tv.R; -import com.android.tv.data.Channel; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** - * A class that manages inputs for PIP. All tuner inputs are represented to one tuner input for PIP. - * Hidden inputs should not be visible to the users. - */ -public class PipInputManager { - private static final String TAG = "PipInputManager"; - - // Tuner inputs aren't distinguished each other in PipInput. They are handled as one input. - // Therefore, we define a fake input id for the unified input. - private static final String TUNER_INPUT_ID = "tuner_input_id"; - - private final Context mContext; - private final TvInputManagerHelper mInputManager; - private final ChannelTuner mChannelTuner; - private boolean mStarted; - private final Map mPipInputMap = new HashMap<>(); // inputId -> PipInput - private final Set mListeners = new ArraySet<>(); - - private final TvInputCallback mTvInputCallback = new TvInputCallback() { - @Override - public void onInputAdded(String inputId) { - TvInputInfo input = mInputManager.getTvInputInfo(inputId); - if (input.isPassthroughInput()) { - boolean available = mInputManager.getInputState(input) - == TvInputManager.INPUT_STATE_CONNECTED; - mPipInputMap.put(inputId, new PipInput(inputId, available)); - } else if (!mPipInputMap.containsKey(TUNER_INPUT_ID)) { - boolean available = mChannelTuner.getBrowsableChannelCount() != 0; - mPipInputMap.put(TUNER_INPUT_ID, new PipInput(TUNER_INPUT_ID, available)); - } else { - return; - } - for (Listener l : mListeners) { - l.onPipInputListUpdated(); - } - } - - @Override - public void onInputRemoved(String inputId) { - PipInput pipInput = mPipInputMap.remove(inputId); - if (pipInput == null) { - if (!mPipInputMap.containsKey(TUNER_INPUT_ID)) { - Log.w(TAG, "A TV input (" + inputId + ") isn't tracked in PipInputManager"); - return; - } - if (mInputManager.getTunerTvInputSize() > 0) { - return; - } - mPipInputMap.remove(TUNER_INPUT_ID); - } - for (Listener l : mListeners) { - l.onPipInputListUpdated(); - } - } - - @Override - public void onInputStateChanged(String inputId, int state) { - PipInput pipInput = mPipInputMap.get(inputId); - if (pipInput == null) { - // For tuner input, state change is handled in mChannelTunerListener. - return; - } - pipInput.updateAvailability(); - } - }; - - private final ChannelTuner.Listener mChannelTunerListener = new ChannelTuner.Listener() { - @Override - public void onLoadFinished() { } - - @Override - public void onCurrentChannelUnavailable(Channel channel) { } - - @Override - public void onBrowsableChannelListChanged() { - PipInput tunerInput = mPipInputMap.get(TUNER_INPUT_ID); - if (tunerInput == null) { - return; - } - tunerInput.updateAvailability(); - } - - @Override - public void onChannelChanged(Channel previousChannel, Channel currentChannel) { - if (previousChannel != null && currentChannel != null - && !previousChannel.isPassthrough() && !currentChannel.isPassthrough()) { - // Channel change between channels for tuner inputs. - return; - } - PipInput previousMainInput = getPipInput(previousChannel); - if (previousMainInput != null) { - previousMainInput.updateAvailability(); - } - PipInput currentMainInput = getPipInput(currentChannel); - if (currentMainInput != null) { - currentMainInput.updateAvailability(); - } - } - }; - - public PipInputManager(Context context, TvInputManagerHelper inputManager, - ChannelTuner channelTuner) { - mContext = context; - mInputManager = inputManager; - mChannelTuner = channelTuner; - } - - /** - * Starts {@link PipInputManager}. - */ - public void start() { - if (mStarted) { - return; - } - mStarted = true; - mInputManager.addCallback(mTvInputCallback); - mChannelTuner.addListener(mChannelTunerListener); - initializePipInputList(); - } - - /** - * Stops {@link PipInputManager}. - */ - public void stop() { - if (!mStarted) { - return; - } - mStarted = false; - mInputManager.removeCallback(mTvInputCallback); - mChannelTuner.removeListener(mChannelTunerListener); - mPipInputMap.clear(); - } - - /** - * Adds a {@link PipInputManager.Listener}. - */ - public void addListener(Listener listener) { - mListeners.add(listener); - } - - /** - * Removes a {@link PipInputManager.Listener}. - */ - public void removeListener(Listener listener) { - mListeners.remove(listener); - } - - /** - * Gets the size of inputs for PIP. - * - *

The hidden inputs are not counted. - * - * @param availableOnly If {@code true}, it counts only available PIP inputs. Please see {@link - * PipInput#isAvailable()} for the details of availability. - */ - public int getPipInputSize(boolean availableOnly) { - int count = 0; - for (PipInput pipInput : mPipInputMap.values()) { - if (!pipInput.isHidden() && (!availableOnly || pipInput.mAvailable)) { - ++count; - } - if (pipInput.isPassthrough()) { - TvInputInfo info = pipInput.getInputInfo(); - // Do not count HDMI ports if a CEC device is directly connected to the port. - if (info.getParentId() != null && !info.isConnectedToHdmiSwitch()) { - --count; - } - } - } - return count; - } - - /** - * Gets the list of inputs for PIP.. - * - *

The hidden inputs are excluded. - * - * @param availableOnly If true, it returns only available PIP inputs. Please see {@link - * PipInput#isAvailable()} for the details of availability. - */ - public List getPipInputList(boolean availableOnly) { - List pipInputs = new ArrayList<>(); - List removeInputs = new ArrayList<>(); - for (PipInput pipInput : mPipInputMap.values()) { - if (!pipInput.isHidden() && (!availableOnly || pipInput.mAvailable)) { - pipInputs.add(pipInput); - } - if (pipInput.isPassthrough()) { - TvInputInfo info = pipInput.getInputInfo(); - // Do not show HDMI ports if a CEC device is directly connected to the port. - if (info.getParentId() != null && !info.isConnectedToHdmiSwitch()) { - removeInputs.add(mPipInputMap.get(info.getParentId())); - } - } - } - if (!removeInputs.isEmpty()) { - pipInputs.removeAll(removeInputs); - } - Collections.sort(pipInputs, new Comparator() { - @Override - public int compare(PipInput lhs, PipInput rhs) { - if (!lhs.mIsPassthrough) { - return -1; - } - if (!rhs.mIsPassthrough) { - return 1; - } - String a = lhs.getLabel(); - String b = rhs.getLabel(); - return a.compareTo(b); - } - }); - return pipInputs; - } - - /** - * Returns an PIP input corresponding to {@code channel}. - */ - public PipInput getPipInput(Channel channel) { - if (channel == null) { - return null; - } - if (channel.isPassthrough()) { - return mPipInputMap.get(channel.getInputId()); - } else { - return mPipInputMap.get(TUNER_INPUT_ID); - } - } - - /** - * Returns true, if {@code channel1} and {@code channel2} belong to the same input. For example, - * two channels from different tuner inputs are also in the same input "Tuner" from PIP - * point of view. - */ - public boolean areInSamePipInput(Channel channel1, Channel channel2) { - PipInput input1 = getPipInput(channel1); - PipInput input2 = getPipInput(channel2); - return input1 != null && input2 != null - && getPipInput(channel1).equals(getPipInput(channel2)); - } - - private void initializePipInputList() { - boolean hasTunerInput = false; - for (TvInputInfo input : mInputManager.getTvInputInfos(false, false)) { - if (input.isPassthroughInput()) { - boolean available = mInputManager.getInputState(input) - == TvInputManager.INPUT_STATE_CONNECTED; - mPipInputMap.put(input.getId(), new PipInput(input.getId(), available)); - } else if (!hasTunerInput) { - hasTunerInput = true; - boolean available = mChannelTuner.getBrowsableChannelCount() != 0; - mPipInputMap.put(TUNER_INPUT_ID, new PipInput(TUNER_INPUT_ID, available)); - } - } - PipInput input = getPipInput(mChannelTuner.getCurrentChannel()); - if (input != null) { - input.updateAvailability(); - } - for (Listener l : mListeners) { - l.onPipInputListUpdated(); - } - } - - /** - * Listeners to notify PIP input state changes. - */ - public interface Listener { - /** - * Called when the state (availability) of PIP inputs is changed. - */ - void onPipInputStateUpdated(); - - /** - * Called when the list of PIP inputs is changed. - */ - void onPipInputListUpdated(); - } - - /** - * Input class for PIP. It has useful methods for PIP handling. - */ - public class PipInput { - private final String mInputId; - private final boolean mIsPassthrough; - private final TvInputInfo mInputInfo; - private boolean mAvailable; - - private PipInput(String inputId, boolean available) { - mInputId = inputId; - mIsPassthrough = !mInputId.equals(TUNER_INPUT_ID); - if (mIsPassthrough) { - mInputInfo = mInputManager.getTvInputInfo(mInputId); - } else { - mInputInfo = null; - } - mAvailable = available; - } - - /** - * Returns the {@link TvInputInfo} object that matches to this PIP input. - */ - public TvInputInfo getInputInfo() { - return mInputInfo; - } - - /** - * Returns {@code true}, if the input is available for PIP. If a channel of an input is - * already played or an input is not connected state or there is no browsable channel, the - * input is unavailable. - */ - public boolean isAvailable() { - return mAvailable; - } - - /** - * Returns true, if the input is a passthrough TV input. - */ - public boolean isPassthrough() { - return mIsPassthrough; - } - - /** - * Gets a channel to play in a PIP view. - */ - public Channel getChannel() { - if (mIsPassthrough) { - return Channel.createPassthroughChannel(mInputId); - } else { - return mChannelTuner.findNearestBrowsableChannel( - Utils.getLastWatchedChannelId(mContext)); - } - } - - /** - * Gets a label of the input. - */ - public String getLabel() { - if (mIsPassthrough) { - return mInputInfo.loadLabel(mContext).toString(); - } else { - return mContext.getString(R.string.input_selector_tuner_label); - } - } - - /** - * Gets a long label including a customized label. - */ - public String getLongLabel() { - if (mIsPassthrough) { - String customizedLabel = Utils.loadLabel(mContext, mInputInfo); - String label = getLabel(); - if (label.equals(customizedLabel)) { - return customizedLabel; - } - return customizedLabel + " (" + label + ")"; - } else { - return mContext.getString(R.string.input_long_label_for_tuner); - } - } - - /** - * Updates availability. It returns true, if availability is changed. - */ - private void updateAvailability() { - boolean available; - // current playing input cannot be available for PIP. - Channel currentChannel = mChannelTuner.getCurrentChannel(); - if (mIsPassthrough) { - if (currentChannel != null && currentChannel.getInputId().equals(mInputId)) { - available = false; - } else { - available = mInputManager.getInputState(mInputId) - == TvInputManager.INPUT_STATE_CONNECTED; - } - } else { - if (currentChannel != null && !currentChannel.isPassthrough()) { - available = false; - } else { - available = mChannelTuner.getBrowsableChannelCount() > 0; - } - } - if (mAvailable != available) { - mAvailable = available; - for (Listener l : mListeners) { - l.onPipInputStateUpdated(); - } - } - } - - private boolean isHidden() { - // mInputInfo is null for the tuner input and it's always visible. - return mInputInfo != null && mInputInfo.isHidden(mContext); - } - } -} diff --git a/src/com/android/tv/util/RecurringRunner.java b/src/com/android/tv/util/RecurringRunner.java index 4135bd4e..324afe73 100644 --- a/src/com/android/tv/util/RecurringRunner.java +++ b/src/com/android/tv/util/RecurringRunner.java @@ -57,12 +57,15 @@ public final class RecurringRunner { mHandler = new Handler(mContext.getMainLooper()); } - public void start() { + public void start(boolean restNextRunTime) { SoftPreconditions.checkState(!mRunning, TAG, mName + " start is called twice."); if (mRunning) { return; } mRunning = true; + if (restNextRunTime) { + resetNextRunTime(); + } new AsyncTask() { @Override protected Long doInBackground(Void... params) { @@ -76,6 +79,10 @@ public final class RecurringRunner { }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } + public void start() { + start(false); + } + public void stop() { mRunning = false; mHandler.removeCallbacksAndMessages(null); diff --git a/src/com/android/tv/util/SearchManagerHelper.java b/src/com/android/tv/util/SearchManagerHelper.java deleted file mode 100644 index b6e34d7a..00000000 --- a/src/com/android/tv/util/SearchManagerHelper.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.util; - -import android.app.SearchManager; -import android.content.Context; -import android.os.Bundle; -import android.os.UserHandle; -import android.util.Log; - -import java.lang.reflect.InvocationTargetException; - -/** - * A convenience class for calling methods in android.app.SearchManager. - */ -public final class SearchManagerHelper { - private static final String TAG = "SearchManagerHelper"; - - private static final Object sLock = new Object(); - private static SearchManagerHelper sInstance; - - private final SearchManager mSearchManager; - - private SearchManagerHelper(Context context) { - mSearchManager = ((android.app.SearchManager) context.getSystemService( - Context.SEARCH_SERVICE)); - } - - public static SearchManagerHelper getInstance(Context context) { - synchronized (sLock) { - if (sInstance == null) { - sInstance = new SearchManagerHelper(context.getApplicationContext()); - } - return sInstance; - } - } - - public void launchAssistAction() { - try { - SearchManager.class.getDeclaredMethod("launchLegacyAssist", String.class, Integer.TYPE, - Bundle.class).invoke(mSearchManager, null, UserHandle.myUserId(), null); - } catch (NoSuchMethodException | IllegalArgumentException | IllegalAccessException - | InvocationTargetException e) { - Log.e(TAG, "Fail to call SearchManager.launchAssistAction", e); - } - } -} diff --git a/src/com/android/tv/util/SetupUtils.java b/src/com/android/tv/util/SetupUtils.java index 8223a81c..0dabe7c0 100644 --- a/src/com/android/tv/util/SetupUtils.java +++ b/src/com/android/tv/util/SetupUtils.java @@ -114,7 +114,7 @@ public class SetupUtils { @Override public void onLoadFinished() { manager.removeListener(this); - updateChannelBrowsable(mTvApplication, inputId, postRunnable); + updateChannelsAfterSetup(mTvApplication, inputId, postRunnable); } @Override @@ -124,17 +124,18 @@ public class SetupUtils { public void onChannelBrowsableChanged() { } }); } else { - updateChannelBrowsable(mTvApplication, inputId, postRunnable); + updateChannelsAfterSetup(mTvApplication, inputId, postRunnable); } } - private static void updateChannelBrowsable(Context context, final String inputId, + private static void updateChannelsAfterSetup(Context context, final String inputId, final Runnable postRunnable) { ApplicationSingletons appSingletons = TvApplication.getSingletons(context); final ChannelDataManager manager = appSingletons.getChannelDataManager(); manager.updateChannels(new Runnable() { @Override public void run() { + Channel firstChannelForInput = null; boolean browsableChanged = false; for (Channel channel : manager.getChannelList()) { if (channel.getInputId().equals(inputId)) { @@ -142,8 +143,14 @@ public class SetupUtils { manager.updateBrowsable(channel.getId(), true, true); browsableChanged = true; } + if (firstChannelForInput == null) { + firstChannelForInput = channel; + } } } + if (firstChannelForInput != null) { + Utils.setLastWatchedChannel(context, firstChannelForInput); + } if (browsableChanged) { manager.notifyChannelBrowsableChanged(); manager.applyUpdatedValuesToDb(); @@ -385,10 +392,7 @@ public class SetupUtils { // Start fetching program guide data for internal tuners. Context context = mTvApplication.getApplicationContext(); if (Utils.isInternalTvInput(context, inputId)) { - if (context.checkSelfPermission(android.Manifest.permission.ACCESS_COARSE_LOCATION) - == PackageManager.PERMISSION_GRANTED && Experiments.CLOUD_EPG.get()) { - EpgFetcher.getInstance(context).startImmediately(); - } + EpgFetcher.getInstance(context).startImmediately(true); } } -} +} \ No newline at end of file diff --git a/src/com/android/tv/util/StringUtils.java b/src/com/android/tv/util/StringUtils.java new file mode 100644 index 00000000..659807e2 --- /dev/null +++ b/src/com/android/tv/util/StringUtils.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.util; + +/** + * Utility class for handling {@link String}. + */ +public final class StringUtils { + + private StringUtils() { } + + /** + * Returns compares two strings lexicographically and handles null values quietly. + */ + public static int compare(String a, String b) { + if (a == null) { + return b == null ? 0 : -1; + } + if (b == null) { + return 1; + } + return a.compareTo(b); + } +} diff --git a/src/com/android/tv/util/TvInputManagerHelper.java b/src/com/android/tv/util/TvInputManagerHelper.java index 121f56ed..67970ed6 100644 --- a/src/com/android/tv/util/TvInputManagerHelper.java +++ b/src/com/android/tv/util/TvInputManagerHelper.java @@ -18,20 +18,26 @@ package com.android.tv.util; import android.content.Context; import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.graphics.drawable.Drawable; +import android.hardware.hdmi.HdmiDeviceInfo; import android.media.tv.TvInputInfo; import android.media.tv.TvInputManager; import android.media.tv.TvInputManager.TvInputCallback; import android.os.Handler; import android.support.annotation.VisibleForTesting; import android.text.TextUtils; +import android.util.ArrayMap; import android.util.Log; import com.android.tv.Features; import com.android.tv.common.SoftPreconditions; +import com.android.tv.common.TvCommonUtils; import com.android.tv.parental.ContentRatingsManager; import com.android.tv.parental.ParentalControlSettings; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; @@ -42,14 +48,64 @@ import java.util.Map; public class TvInputManagerHelper { private static final String TAG = "TvInputManagerHelper"; private static final boolean DEBUG = false; + + /** + * Types of HDMI device and bundled tuner. + */ + public static final int TYPE_CEC_DEVICE = -2; + public static final int TYPE_BUNDLED_TUNER = -3; + public static final int TYPE_CEC_DEVICE_RECORDER = -4; + public static final int TYPE_CEC_DEVICE_PLAYBACK = -5; + public static final int TYPE_MHL_MOBILE = -6; + + private static final String PERMISSION_ACCESS_ALL_EPG_DATA = + "com.android.providers.tv.permission.ACCESS_ALL_EPG_DATA"; + private static final String [] mPhysicalTunerBlackList = { + }; + private static final String META_LABEL_SORT_KEY = "input_sort_key"; + + /** + * The default tv input priority to show. + */ + private static final ArrayList DEFAULT_TV_INPUT_PRIORITY = new ArrayList<>(); + static { + DEFAULT_TV_INPUT_PRIORITY.add(TYPE_BUNDLED_TUNER); + DEFAULT_TV_INPUT_PRIORITY.add(TvInputInfo.TYPE_TUNER); + DEFAULT_TV_INPUT_PRIORITY.add(TYPE_CEC_DEVICE); + DEFAULT_TV_INPUT_PRIORITY.add(TYPE_CEC_DEVICE_RECORDER); + DEFAULT_TV_INPUT_PRIORITY.add(TYPE_CEC_DEVICE_PLAYBACK); + DEFAULT_TV_INPUT_PRIORITY.add(TYPE_MHL_MOBILE); + DEFAULT_TV_INPUT_PRIORITY.add(TvInputInfo.TYPE_HDMI); + DEFAULT_TV_INPUT_PRIORITY.add(TvInputInfo.TYPE_DVI); + DEFAULT_TV_INPUT_PRIORITY.add(TvInputInfo.TYPE_COMPONENT); + DEFAULT_TV_INPUT_PRIORITY.add(TvInputInfo.TYPE_SVIDEO); + DEFAULT_TV_INPUT_PRIORITY.add(TvInputInfo.TYPE_COMPOSITE); + DEFAULT_TV_INPUT_PRIORITY.add(TvInputInfo.TYPE_DISPLAY_PORT); + DEFAULT_TV_INPUT_PRIORITY.add(TvInputInfo.TYPE_VGA); + DEFAULT_TV_INPUT_PRIORITY.add(TvInputInfo.TYPE_SCART); + DEFAULT_TV_INPUT_PRIORITY.add(TvInputInfo.TYPE_OTHER); + } + private static final String[] PARTNER_TUNER_INPUT_PREFIX_BLACKLIST = { }; + private static final String[] TESTABLE_INPUTS = { + "com.android.tv.testinput/.TestTvInputService" + }; + private final Context mContext; + private final PackageManager mPackageManager; private final TvInputManager mTvInputManager; private final Map mInputStateMap = new HashMap<>(); private final Map mInputMap = new HashMap<>(); + private final Map mTvInputLabels = new ArrayMap<>(); + private final Map mTvInputCustomLabels = new ArrayMap<>(); private final Map mInputIdToPartnerInputMap = new HashMap<>(); + + private final Map mTvInputApplicationLabels = new ArrayMap<>(); + private final Map mTvInputApplicationIcons = new ArrayMap<>(); + private final Map mTvInputAppliactionBanners = new ArrayMap<>(); + private final TvInputCallback mInternalCallback = new TvInputCallback() { @Override public void onInputStateChanged(String inputId, int state) { @@ -72,6 +128,11 @@ public class TvInputManagerHelper { TvInputInfo info = mTvInputManager.getTvInputInfo(inputId); if (info != null) { mInputMap.put(inputId, info); + mTvInputLabels.put(inputId, info.loadLabel(mContext).toString()); + CharSequence inputCustomLabel = info.loadCustomLabel(mContext); + if (inputCustomLabel != null) { + mTvInputCustomLabels.put(inputId, inputCustomLabel.toString()); + } mInputStateMap.put(inputId, mTvInputManager.getInputState(inputId)); mInputIdToPartnerInputMap.put(inputId, isPartnerInput(info)); } @@ -85,6 +146,11 @@ public class TvInputManagerHelper { public void onInputRemoved(String inputId) { if (DEBUG) Log.d(TAG, "onInputRemoved " + inputId); mInputMap.remove(inputId); + mTvInputLabels.remove(inputId); + mTvInputCustomLabels.remove(inputId); + mTvInputApplicationLabels.remove(inputId); + mTvInputApplicationIcons.remove(inputId); + mTvInputAppliactionBanners.remove(inputId); mInputStateMap.remove(inputId); mInputIdToPartnerInputMap.remove(inputId); mContentRatingsManager.update(); @@ -103,6 +169,14 @@ public class TvInputManagerHelper { } TvInputInfo info = mTvInputManager.getTvInputInfo(inputId); mInputMap.put(inputId, info); + mTvInputLabels.put(inputId, info.loadLabel(mContext).toString()); + CharSequence inputCustomLabel = info.loadCustomLabel(mContext); + if (inputCustomLabel != null) { + mTvInputCustomLabels.put(inputId, inputCustomLabel.toString()); + } + mTvInputApplicationLabels.remove(inputId); + mTvInputApplicationIcons.remove(inputId); + mTvInputAppliactionBanners.remove(inputId); for (TvInputCallback callback : mCallbacks) { callback.onInputUpdated(inputId); } @@ -114,6 +188,11 @@ public class TvInputManagerHelper { public void onTvInputInfoUpdated(TvInputInfo inputInfo) { if (DEBUG) Log.d(TAG, "onTvInputInfoUpdated " + inputInfo); mInputMap.put(inputInfo.getId(), inputInfo); + mTvInputLabels.put(inputInfo.getId(), inputInfo.loadLabel(mContext).toString()); + CharSequence inputCustomLabel = inputInfo.loadCustomLabel(mContext); + if (inputCustomLabel != null) { + mTvInputCustomLabels.put(inputInfo.getId(), inputCustomLabel.toString()); + } for (TvInputCallback callback : mCallbacks) { callback.onTvInputInfoUpdated(inputInfo); } @@ -131,6 +210,7 @@ public class TvInputManagerHelper { public TvInputManagerHelper(Context context) { mContext = context.getApplicationContext(); + mPackageManager = context.getPackageManager(); mTvInputManager = (TvInputManager) context.getSystemService(Context.TV_INPUT_SERVICE); mContentRatingsManager = new ContentRatingsManager(context); mParentalControlSettings = new ParentalControlSettings(context); @@ -145,6 +225,11 @@ public class TvInputManagerHelper { mStarted = true; mTvInputManager.registerCallback(mInternalCallback, mHandler); mInputMap.clear(); + mTvInputLabels.clear(); + mTvInputCustomLabels.clear(); + mTvInputApplicationLabels.clear(); + mTvInputApplicationIcons.clear(); + mTvInputAppliactionBanners.clear(); mInputStateMap.clear(); mInputIdToPartnerInputMap.clear(); for (TvInputInfo input : mTvInputManager.getTvInputList()) { @@ -171,9 +256,23 @@ public class TvInputManagerHelper { mStarted = false; mInputStateMap.clear(); mInputMap.clear(); + mTvInputLabels.clear(); + mTvInputCustomLabels.clear(); + mTvInputApplicationLabels.clear(); + mTvInputApplicationIcons.clear(); + mTvInputAppliactionBanners.clear();; mInputIdToPartnerInputMap.clear(); } + /** + * Clears the TvInput labels map. + */ + public void clearTvInputLabels() { + mTvInputLabels.clear(); + mTvInputCustomLabels.clear(); + mTvInputApplicationLabels.clear(); + } + public List getTvInputInfos(boolean availableOnly, boolean tunerOnly) { ArrayList list = new ArrayList<>(); for (Map.Entry pair : mInputStateMap.entrySet()) { @@ -245,7 +344,69 @@ public class TvInputManagerHelper { */ @VisibleForTesting public String loadLabel(TvInputInfo info) { - return info.loadLabel(mContext).toString(); + String label = mTvInputLabels.get(info.getId()); + if (label == null) { + label = info.loadLabel(mContext).toString(); + mTvInputLabels.put(info.getId(), label); + } + return label; + } + + /** + * Loads custom label of {@code info} + */ + public String loadCustomLabel(TvInputInfo info) { + String customLabel = mTvInputCustomLabels.get(info.getId()); + if (customLabel == null) { + CharSequence customLabelCharSequence = info.loadCustomLabel(mContext); + if (customLabelCharSequence != null) { + customLabel = customLabelCharSequence.toString(); + mTvInputCustomLabels.put(info.getId(), customLabel); + } + } + return customLabel; + } + + /** + * Gets the tv input application's label. + */ + public CharSequence getTvInputApplicationLabel(CharSequence inputId) { + return mTvInputApplicationLabels.get(inputId); + } + + /** + * Stores the tv input application's label. + */ + public void setTvInputApplicationLabel(String inputId, CharSequence label) { + mTvInputApplicationLabels.put(inputId, label); + } + + /** + * Gets the tv input application's icon. + */ + public Drawable getTvInputApplicationIcon(String inputId) { + return mTvInputApplicationIcons.get(inputId); + } + + /** + * Stores the tv input application's icon. + */ + public void setTvInputApplicationIcon(String inputId, Drawable icon) { + mTvInputApplicationIcons.put(inputId, icon); + } + + /** + * Gets the tv input application's banner. + */ + public Drawable getTvInputApplicationBanner(String inputId) { + return mTvInputAppliactionBanners.get(inputId); + } + + /** + * Stores the tv input application's banner. + */ + public void setTvInputApplicationBanner(String inputId, Drawable banner) { + mTvInputAppliactionBanners.put(inputId, banner); } /** @@ -321,15 +482,55 @@ public class TvInputManagerHelper { return mContentRatingsManager; } - private boolean isInBlackList(String inputId) { - if (!Features.USE_PARTNER_INPUT_BLACKLIST.isEnabled(mContext)) { + private int getInputSortKey(TvInputInfo input) { + return input.getServiceInfo().metaData.getInt(META_LABEL_SORT_KEY, + Integer.MAX_VALUE); + } + + private boolean isInputPhysicalTuner(TvInputInfo input) { + String packageName = input.getServiceInfo().packageName; + if (Arrays.asList(mPhysicalTunerBlackList).contains(packageName)) { return false; } - for (String disabledTunerInputPrefix : PARTNER_TUNER_INPUT_PREFIX_BLACKLIST) { - if (inputId.contains(disabledTunerInputPrefix)) { - return true; + + if (input.createSetupIntent() == null) { + return false; + } else { + boolean mayBeTunerInput = mPackageManager.checkPermission( + PERMISSION_ACCESS_ALL_EPG_DATA, input.getServiceInfo().packageName) + == PackageManager.PERMISSION_GRANTED; + if (!mayBeTunerInput) { + try { + ApplicationInfo ai = mPackageManager.getApplicationInfo( + input.getServiceInfo().packageName, 0); + if ((ai.flags & (ApplicationInfo.FLAG_SYSTEM + | ApplicationInfo.FLAG_UPDATED_SYSTEM_APP)) == 0) { + return false; + } + } catch (PackageManager.NameNotFoundException e) { + return false; + } } } + return true; + } + + private boolean isInBlackList(String inputId) { + if (Features.USE_PARTNER_INPUT_BLACKLIST.isEnabled(mContext)) { + for (String disabledTunerInputPrefix : PARTNER_TUNER_INPUT_PREFIX_BLACKLIST) { + if (inputId.contains(disabledTunerInputPrefix)) { + return true; + } + } + } + if (TvCommonUtils.isRunningInTest()) { + for (String testableInput : TESTABLE_INPUTS) { + if (testableInput.equals(inputId)) { + return false; + } + } + return true; + } return false; } @@ -357,4 +558,123 @@ public class TvInputManagerHelper { return mInputManager.loadLabel(lhs).compareTo(mInputManager.loadLabel(rhs)); } } + + /** + * A comparator used for {@link com.android.tv.ui.SelectInputView} to show the list of + * TV inputs. + */ + public static class InputComparator implements Comparator { + private Map mTypePriorities = new HashMap<>(); + private final TvInputManagerHelper mTvInputManagerHelper; + private final Context mContext; + + public InputComparator(Context context, TvInputManagerHelper tvInputManagerHelper) { + mContext = context; + mTvInputManagerHelper = tvInputManagerHelper; + setupDeviceTypePriorities(); + } + + @Override + public int compare(TvInputInfo lhs, TvInputInfo rhs) { + if (lhs == null) { + return (rhs == null) ? 0 : 1; + } + if (rhs == null) { + return -1; + } + + boolean enabledL = (mTvInputManagerHelper.getInputState(lhs) + != TvInputManager.INPUT_STATE_DISCONNECTED); + boolean enabledR = (mTvInputManagerHelper.getInputState(rhs) + != TvInputManager.INPUT_STATE_DISCONNECTED); + if (enabledL != enabledR) { + return enabledL ? -1 : 1; + } + + int priorityL = getPriority(lhs); + int priorityR = getPriority(rhs); + if (priorityL != priorityR) { + return priorityL - priorityR; + } + + if (lhs.getType() == TvInputInfo.TYPE_TUNER + && rhs.getType() == TvInputInfo.TYPE_TUNER) { + boolean isPhysicalL = mTvInputManagerHelper.isInputPhysicalTuner(lhs); + boolean isPhysicalR = mTvInputManagerHelper.isInputPhysicalTuner(rhs); + if (isPhysicalL != isPhysicalR) { + return isPhysicalL ? -1 : 1; + } + } + + int sortKeyL = mTvInputManagerHelper.getInputSortKey(lhs); + int sortKeyR = mTvInputManagerHelper.getInputSortKey(rhs); + if (sortKeyL != sortKeyR) { + return sortKeyR - sortKeyL; + } + + String parentLabelL = lhs.getParentId() != null + ? getLabel(mTvInputManagerHelper.getTvInputInfo(lhs.getParentId())) + : getLabel(mTvInputManagerHelper.getTvInputInfo(lhs.getId())); + String parentLabelR = rhs.getParentId() != null + ? getLabel(mTvInputManagerHelper.getTvInputInfo(rhs.getParentId())) + : getLabel(mTvInputManagerHelper.getTvInputInfo(rhs.getId())); + + if (!TextUtils.equals(parentLabelL, parentLabelR)) { + return parentLabelL.compareToIgnoreCase(parentLabelR); + } + return getLabel(lhs).compareToIgnoreCase(getLabel(rhs)); + } + + private String getLabel(TvInputInfo input) { + if (input == null) { + return ""; + } + String label = mTvInputManagerHelper.loadCustomLabel(input); + if (TextUtils.isEmpty(label)) { + label = mTvInputManagerHelper.loadLabel(input); + } + return label; + } + + private int getPriority(TvInputInfo info) { + Integer priority = null; + if (mTypePriorities != null) { + priority = mTypePriorities.get(getTvInputTypeForPriority(info)); + } + if (priority != null) { + return priority; + } + return Integer.MAX_VALUE; + } + + private void setupDeviceTypePriorities() { + mTypePriorities = Partner.getInstance(mContext).getInputsOrderMap(); + + // Fill in any missing priorities in the map we got from the OEM + int priority = mTypePriorities.size(); + for (int type : DEFAULT_TV_INPUT_PRIORITY) { + if (!mTypePriorities.containsKey(type)) { + mTypePriorities.put(type, priority++); + } + } + } + + private int getTvInputTypeForPriority(TvInputInfo info) { + if (info.getHdmiDeviceInfo() != null) { + if (info.getHdmiDeviceInfo().isCecDevice()) { + switch (info.getHdmiDeviceInfo().getDeviceType()) { + case HdmiDeviceInfo.DEVICE_RECORDER: + return TYPE_CEC_DEVICE_RECORDER; + case HdmiDeviceInfo.DEVICE_PLAYBACK: + return TYPE_CEC_DEVICE_PLAYBACK; + default: + return TYPE_CEC_DEVICE; + } + } else if (info.getHdmiDeviceInfo().isMhlDevice()) { + return TYPE_MHL_MOBILE; + } + } + return info.getType(); + } + } } diff --git a/src/com/android/tv/util/TvSettings.java b/src/com/android/tv/util/TvSettings.java index 97ff59d6..970cd055 100644 --- a/src/com/android/tv/util/TvSettings.java +++ b/src/com/android/tv/util/TvSettings.java @@ -17,6 +17,8 @@ package com.android.tv.util; import android.content.Context; +import android.content.SharedPreferences; +import android.media.tv.TvTrackInfo; import android.preference.PreferenceManager; import android.support.annotation.IntDef; @@ -26,7 +28,6 @@ import java.util.Collections; import java.util.HashSet; import java.util.Set; - /** * A class about the constants for TV settings. * Objects that are returned from the various {@code get} methods must be treated as immutable. @@ -35,44 +36,21 @@ public final class TvSettings { private TvSettings() {} public static final String PREF_DISPLAY_MODE = "display_mode"; // int value - public static final String PREF_PIP_LAYOUT = "pip_layout"; // int value - public static final String PREF_PIP_SIZE = "pip_size"; // int value public static final String PREF_PIN = "pin"; // 4-digit string value. Otherwise, it's not set. - // PIP sounds - @Retention(RetentionPolicy.SOURCE) - @IntDef({ - PIP_SOUND_MAIN, PIP_SOUND_PIP_WINDOW }) - public @interface PipSound {} - public static final int PIP_SOUND_MAIN = 0; - public static final int PIP_SOUND_PIP_WINDOW = PIP_SOUND_MAIN + 1; - - // PIP layouts - @Retention(RetentionPolicy.SOURCE) - @IntDef({ - PIP_LAYOUT_BOTTOM_RIGHT, PIP_LAYOUT_TOP_RIGHT, PIP_LAYOUT_TOP_LEFT, - PIP_LAYOUT_BOTTOM_LEFT, PIP_LAYOUT_SIDE_BY_SIDE }) - public @interface PipLayout {} - public static final int PIP_LAYOUT_BOTTOM_RIGHT = 0; - public static final int PIP_LAYOUT_TOP_RIGHT = PIP_LAYOUT_BOTTOM_RIGHT + 1; - public static final int PIP_LAYOUT_TOP_LEFT = PIP_LAYOUT_TOP_RIGHT + 1; - public static final int PIP_LAYOUT_BOTTOM_LEFT = PIP_LAYOUT_TOP_LEFT + 1; - public static final int PIP_LAYOUT_SIDE_BY_SIDE = PIP_LAYOUT_BOTTOM_LEFT + 1; - public static final int PIP_LAYOUT_LAST = PIP_LAYOUT_SIDE_BY_SIDE; - - // PIP sizes - @Retention(RetentionPolicy.SOURCE) - @IntDef({ PIP_SIZE_SMALL, PIP_SIZE_BIG }) - public @interface PipSize {} - public static final int PIP_SIZE_SMALL = 0; - public static final int PIP_SIZE_BIG = PIP_SIZE_SMALL + 1; - public static final int PIP_SIZE_LAST = PIP_SIZE_BIG; - // Multi-track audio settings private static final String PREF_MULTI_AUDIO_ID = "pref.multi_audio_id"; private static final String PREF_MULTI_AUDIO_LANGUAGE = "pref.multi_audio_language"; private static final String PREF_MULTI_AUDIO_CHANNEL_COUNT = "pref.multi_audio_channel_count"; + // DVR Multi-audio and subtitle settings + private static final String PREF_DVR_MULTI_AUDIO_ID = "pref.dvr_multi_audio_id"; + private static final String PREF_DVR_MULTI_AUDIO_LANGUAGE = "pref.dvr_multi_audio_language"; + private static final String PREF_DVR_MULTI_AUDIO_CHANNEL_COUNT = + "pref.dvr_multi_audio_channel_count"; + private static final String PREF_DVR_SUBTITLE_ID = "pref.dvr_subtitle_id"; + private static final String PREF_DVR_SUBTITLE_LANGUAGE = "pref.dvr_subtitle_language"; + // Parental Control settings private static final String PREF_CONTENT_RATING_SYSTEMS = "pref.content_rating_systems"; private static final String PREF_CONTENT_RATING_LEVEL = "pref.content_rating_level"; @@ -89,59 +67,6 @@ public final class TvSettings { public static final int CONTENT_RATING_LEVEL_LOW = 3; public static final int CONTENT_RATING_LEVEL_CUSTOM = 4; - // PIP settings - /** - * Returns the layout of the PIP window stored in the shared preferences. - * - * @return the saved layout of the PIP window. This value is one of - * {@link #PIP_LAYOUT_TOP_LEFT}, {@link #PIP_LAYOUT_TOP_RIGHT}, - * {@link #PIP_LAYOUT_BOTTOM_LEFT}, {@link #PIP_LAYOUT_BOTTOM_RIGHT} and - * {@link #PIP_LAYOUT_SIDE_BY_SIDE}. If the preference value does not exist, - * {@link #PIP_LAYOUT_BOTTOM_RIGHT} is returned. - */ - @SuppressWarnings("ResourceType") - @PipLayout - public static int getPipLayout(Context context) { - return PreferenceManager.getDefaultSharedPreferences(context).getInt( - PREF_PIP_LAYOUT, PIP_LAYOUT_BOTTOM_RIGHT); - } - - /** - * Stores the layout of PIP window to the shared preferences. - * - * @param pipLayout This value should be one of {@link #PIP_LAYOUT_TOP_LEFT}, - * {@link #PIP_LAYOUT_TOP_RIGHT}, {@link #PIP_LAYOUT_BOTTOM_LEFT}, - * {@link #PIP_LAYOUT_BOTTOM_RIGHT} and {@link #PIP_LAYOUT_SIDE_BY_SIDE}. - */ - public static void setPipLayout(Context context, @PipLayout int pipLayout) { - PreferenceManager.getDefaultSharedPreferences(context).edit().putInt( - PREF_PIP_LAYOUT, pipLayout).apply(); - } - - /** - * Returns the size of the PIP view stored in the shared preferences. - * - * @return the saved size of the PIP view. This value is one of - * {@link #PIP_SIZE_SMALL} and {@link #PIP_SIZE_BIG}. If the preference value does not - * exist, {@link #PIP_SIZE_SMALL} is returned. - */ - @SuppressWarnings("ResourceType") - @PipSize - public static int getPipSize(Context context) { - return PreferenceManager.getDefaultSharedPreferences(context).getInt( - PREF_PIP_SIZE, PIP_SIZE_SMALL); - } - - /** - * Stores the size of PIP view to the shared preferences. - * - * @param pipSize This value should be one of {@link #PIP_SIZE_SMALL} and {@link #PIP_SIZE_BIG}. - */ - public static void setPipSize(Context context, @PipSize int pipSize) { - PreferenceManager.getDefaultSharedPreferences(context).edit().putInt( - PREF_PIP_SIZE, pipSize).apply(); - } - // Multi-track audio settings public static String getMultiAudioId(Context context) { return PreferenceManager.getDefaultSharedPreferences(context).getString( @@ -173,6 +98,57 @@ public final class TvSettings { PREF_MULTI_AUDIO_CHANNEL_COUNT, channelCount).apply(); } + public static void setDvrPlaybackTrackSettings(Context context, int trackType, + TvTrackInfo info) { + if (trackType == TvTrackInfo.TYPE_AUDIO) { + if (info == null) { + PreferenceManager.getDefaultSharedPreferences(context).edit() + .remove(PREF_DVR_MULTI_AUDIO_ID).apply(); + } else { + PreferenceManager.getDefaultSharedPreferences(context).edit() + .putString(PREF_DVR_MULTI_AUDIO_LANGUAGE, info.getLanguage()) + .putInt(PREF_DVR_MULTI_AUDIO_CHANNEL_COUNT, info.getAudioChannelCount()) + .putString(PREF_DVR_MULTI_AUDIO_ID, info.getId()).apply(); + } + } else if (trackType == TvTrackInfo.TYPE_SUBTITLE) { + if (info == null) { + PreferenceManager.getDefaultSharedPreferences(context).edit() + .remove(PREF_DVR_SUBTITLE_ID).apply(); + } else { + PreferenceManager.getDefaultSharedPreferences(context).edit() + .putString(PREF_DVR_SUBTITLE_LANGUAGE, info.getLanguage()) + .putString(PREF_DVR_SUBTITLE_ID, info.getId()).apply(); + } + } + } + + public static TvTrackInfo getDvrPlaybackTrackSettings(Context context, + int trackType) { + String language; + String trackId; + int channelCount; + SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(context); + if (trackType == TvTrackInfo.TYPE_AUDIO) { + trackId = pref.getString(PREF_DVR_MULTI_AUDIO_ID, null); + if (trackId == null) { + return null; + } + language = pref.getString(PREF_DVR_MULTI_AUDIO_LANGUAGE, null); + channelCount = pref.getInt(PREF_DVR_MULTI_AUDIO_CHANNEL_COUNT, 0); + return new TvTrackInfo.Builder(trackType, trackId) + .setLanguage(language).setAudioChannelCount(channelCount).build(); + } else if (trackType == TvTrackInfo.TYPE_SUBTITLE) { + trackId = pref.getString(PREF_DVR_SUBTITLE_ID, null); + if (trackId == null) { + return null; + } + language = pref.getString(PREF_DVR_SUBTITLE_LANGUAGE, null); + return new TvTrackInfo.Builder(trackType, trackId).setLanguage(language).build(); + } else { + return null; + } + } + // Parental Control settings public static void addContentRatingSystems(Context context, Set ids) { Set contentRatingSystemSet = getContentRatingSystemSet(context); @@ -254,4 +230,4 @@ public final class TvSettings { PreferenceManager.getDefaultSharedPreferences(context).edit().putLong( PREF_DISABLE_PIN_UNTIL, timeMillis).apply(); } -} +} \ No newline at end of file diff --git a/src/com/android/tv/util/TvTrackInfoUtils.java b/src/com/android/tv/util/TvTrackInfoUtils.java index c004f001..667cc9bf 100644 --- a/src/com/android/tv/util/TvTrackInfoUtils.java +++ b/src/com/android/tv/util/TvTrackInfoUtils.java @@ -52,35 +52,22 @@ public class TvTrackInfoUtils { } // Assumes {@code null} language matches to any language since it means user hasn't // selected any track before or selected a track without language information. - boolean rhsLangMatch = language == null || Utils.isEqualLanguage(rhs.getLanguage(), - language); boolean lhsLangMatch = language == null || Utils.isEqualLanguage(lhs.getLanguage(), language); - if (rhsLangMatch) { - if (lhsLangMatch) { - boolean rhsCountMatch = rhs.getAudioChannelCount() == channelCount; - boolean lhsCountMatch = lhs.getAudioChannelCount() == channelCount; - if (rhsCountMatch) { - if (lhsCountMatch) { - boolean rhsIdMatch = rhs.getId().equals(id); - boolean lhsIdMatch = lhs.getId().equals(id); - if (rhsIdMatch) { - return lhsIdMatch ? 0 : -1; - } else { - return lhsIdMatch ? 1 : 0; - } - - } else { - return -1; - } - } else { - return lhsCountMatch ? 1 : 0; - } + boolean rhsLangMatch = language == null || Utils.isEqualLanguage(rhs.getLanguage(), + language); + if (lhsLangMatch && rhsLangMatch) { + boolean lhsCountMatch = lhs.getType() != TvTrackInfo.TYPE_AUDIO + || lhs.getAudioChannelCount() == channelCount; + boolean rhsCountMatch = rhs.getType() != TvTrackInfo.TYPE_AUDIO + || rhs.getAudioChannelCount() == channelCount; + if (lhsCountMatch && rhsCountMatch) { + return Boolean.compare(lhs.getId().equals(id), rhs.getId().equals(id)); } else { - return -1; + return Boolean.compare(lhsCountMatch, rhsCountMatch); } } else { - return lhsLangMatch ? 1 : 0; + return Boolean.compare(lhsLangMatch, rhsLangMatch); } } }; @@ -112,4 +99,4 @@ public class TvTrackInfoUtils { private TvTrackInfoUtils() { } -} +} \ No newline at end of file diff --git a/src/com/android/tv/util/Utils.java b/src/com/android/tv/util/Utils.java index 99d34431..3fe2ec3d 100644 --- a/src/com/android/tv/util/Utils.java +++ b/src/com/android/tv/util/Utils.java @@ -44,11 +44,13 @@ import android.view.View; import com.android.tv.ApplicationSingletons; import com.android.tv.R; import com.android.tv.TvApplication; +import com.android.tv.common.BuildConfig; import com.android.tv.common.SoftPreconditions; import com.android.tv.data.Channel; import com.android.tv.data.GenreItems; import com.android.tv.data.Program; import com.android.tv.data.StreamInfo; +import com.android.tv.experiments.Experiments; import java.io.File; import java.text.SimpleDateFormat; @@ -83,9 +85,6 @@ public class Utils { public static final String EXTRA_KEY_RECORDED_PROGRAM_PIN_CHECKED = "recorded_program_pin_checked"; - // Query parameter in the intent of starting MainActivity. - public static final String PARAM_SOURCE = "source"; - private static final String PATH_CHANNEL = "channel"; private static final String PATH_PROGRAM = "program"; @@ -97,6 +96,8 @@ public class Utils { "last_watched_tuner_input_id"; private static final String PREF_KEY_RECORDING_FAILED_REASONS = "recording_failed_reasons"; + private static final String PREF_KEY_FAILED_SCHEDULED_RECORDING_INFO_SET = + "failed_scheduled_recording_info_set"; private static final int VIDEO_SD_WIDTH = 704; private static final int VIDEO_SD_HEIGHT = 480; @@ -114,6 +115,7 @@ public class Utils { private static final int AUDIO_CHANNEL_SURROUND_8 = 8; private static final long RECORDING_FAILED_REASON_NONE = 0; + private static final long HALF_MINUTE_MS = TimeUnit.SECONDS.toMillis(30); private static final long ONE_DAY_MS = TimeUnit.DAYS.toMillis(1); // Hardcoded list for known bundled inputs not written by OEM/SOCs. @@ -206,6 +208,28 @@ public class Utils { .apply(); } + /** + * Adds the info of failed scheduled recording. + */ + public static void addFailedScheduledRecordingInfo(Context context, + String scheduledRecordingInfo) { + Set failedScheduledRecordingInfoSet = getFailedScheduledRecordingInfoSet(context); + failedScheduledRecordingInfoSet.add(scheduledRecordingInfo); + PreferenceManager.getDefaultSharedPreferences(context).edit() + .putStringSet(PREF_KEY_FAILED_SCHEDULED_RECORDING_INFO_SET, + failedScheduledRecordingInfoSet) + .apply(); + } + + /** + * Clears the failed scheduled recording info set. + */ + public static void clearFailedScheduledRecordingInfoSet(Context context) { + PreferenceManager.getDefaultSharedPreferences(context).edit() + .remove(PREF_KEY_FAILED_SCHEDULED_RECORDING_INFO_SET) + .apply(); + } + /** * Clears recording failed reason. */ @@ -245,6 +269,14 @@ public class Utils { RECORDING_FAILED_REASON_NONE); } + /** + * Returns the failed scheduled recordings info set. + */ + public static Set getFailedScheduledRecordingInfoSet(Context context) { + return PreferenceManager.getDefaultSharedPreferences(context) + .getStringSet(PREF_KEY_FAILED_SCHEDULED_RECORDING_INFO_SET, new HashSet<>()); + } + /** * Checks do recording failed reason exist. */ @@ -332,6 +364,14 @@ public class Utils { return getProgramAt(context, channelId, System.currentTimeMillis()); } + /** + * Returns the round off minutes when convert milliseconds to minutes. + */ + public static int getRoundOffMinsFromMs(long millis) { + // Round off the result by adding half minute to the original ms. + return (int) TimeUnit.MILLISECONDS.toMinutes(millis + HALF_MINUTE_MS); + } + /** * Returns duration string according to the date & time format. * If {@code startUtcMillis} and {@code endUtcMills} are equal, @@ -392,16 +432,18 @@ public class Utils { : DateUtils.formatDateRange(context, startUtcMillis, endUtcMillis + 1, flag); } - @VisibleForTesting + /** + * Checks if two given time (in milliseconds) are in the same day with regard to the + * locale timezone. + */ public static boolean isInGivenDay(long dayToMatchInMillis, long subjectTimeInMillis) { - final long DAY_IN_MS = TimeUnit.DAYS.toMillis(1); TimeZone timeZone = Calendar.getInstance().getTimeZone(); long offset = timeZone.getRawOffset(); if (timeZone.inDaylightTime(new Date(dayToMatchInMillis))) { offset += timeZone.getDSTSavings(); } - return Utils.floorTime(dayToMatchInMillis + offset, DAY_IN_MS) - == Utils.floorTime(subjectTimeInMillis + offset, DAY_IN_MS); + return Utils.floorTime(dayToMatchInMillis + offset, ONE_DAY_MS) + == Utils.floorTime(subjectTimeInMillis + offset, ONE_DAY_MS); } /** @@ -523,7 +565,7 @@ public class Utils { if (track.getType() != TvTrackInfo.TYPE_AUDIO) { throw new IllegalArgumentException("Not an audio track: " + track); } - String language = context.getString(R.string.default_language); + String language = context.getString(R.string.multi_audio_unknown_language); if (!TextUtils.isEmpty(track.getLanguage())) { language = new Locale(track.getLanguage()).getDisplayName(); } else { @@ -860,4 +902,11 @@ public class Utils { } return Genres.encode(genres); } + + /** + * Returns true if the current user is a developer. + */ + public static boolean isDeveloper() { + return BuildConfig.ENG || Experiments.ENABLE_DEVELOPER_FEATURES.get(); + } } diff --git a/src/com/android/tv/util/ViewCache.java b/src/com/android/tv/util/ViewCache.java new file mode 100644 index 00000000..113bda27 --- /dev/null +++ b/src/com/android/tv/util/ViewCache.java @@ -0,0 +1,70 @@ +package com.android.tv.util; + +import android.util.SparseArray; +import android.view.View; + +import java.util.ArrayList; + +/** + * A cache for the views. + */ +public class ViewCache { + private final static SparseArray> mViews = new SparseArray(); + + private static ViewCache sViewCache; + + private ViewCache() { } + + /** + * Returns an instance of the view cache. + */ + public static ViewCache getInstance() { + if (sViewCache == null) { + return new ViewCache(); + } else { + return sViewCache; + } + } + + /** + * Returns if the view cache is empty. + */ + public boolean isEmpty() { + return mViews.size() == 0; + } + + /** + * Stores a view into this view cache. + */ + public void putView(int resId, View view) { + ArrayList views = mViews.get(resId); + if (views == null) { + views = new ArrayList(); + mViews.put(resId, views); + } + views.add(view); + } + + /** + * Returns the view for specific resource id. + */ + public View getView(int resId) { + ArrayList views = mViews.get(resId); + if (views != null && !views.isEmpty()) { + View view = views.remove(views.size() - 1); + if (views.isEmpty()) { + mViews.remove(resId); + } + return view; + } else { + return null; + } + } + + /** + * Clears the view cache. + */ + public void clear() { + mViews.clear(); + } +} diff --git a/tests/common/src/com/android/tv/testing/dvr/RecordingTestUtils.java b/tests/common/src/com/android/tv/testing/dvr/RecordingTestUtils.java index b9def95e..a9bfa97a 100644 --- a/tests/common/src/com/android/tv/testing/dvr/RecordingTestUtils.java +++ b/tests/common/src/com/android/tv/testing/dvr/RecordingTestUtils.java @@ -16,7 +16,7 @@ package com.android.tv.testing.dvr; -import com.android.tv.dvr.ScheduledRecording; +import com.android.tv.dvr.data.ScheduledRecording; import junit.framework.Assert; diff --git a/tests/func/src/com/android/tv/tests/ui/LiveChannelsTestCase.java b/tests/func/src/com/android/tv/tests/ui/LiveChannelsTestCase.java index 25c7909b..e306e6c6 100644 --- a/tests/func/src/com/android/tv/tests/ui/LiveChannelsTestCase.java +++ b/tests/func/src/com/android/tv/tests/ui/LiveChannelsTestCase.java @@ -73,6 +73,8 @@ public abstract class LiveChannelsTestCase extends InstrumentationTestCase { .hasObject(Constants.PROGRAM_GUIDE)) { mDevice.pressBack(); } + // To destroy the activity to make sure next test case's activity launch check works well. + mDevice.pressBack(); super.tearDown(); } diff --git a/tests/func/src/com/android/tv/tests/ui/PlayControlsRowViewTest.java b/tests/func/src/com/android/tv/tests/ui/PlayControlsRowViewTest.java index bbc7aa81..82c6a810 100644 --- a/tests/func/src/com/android/tv/tests/ui/PlayControlsRowViewTest.java +++ b/tests/func/src/com/android/tv/tests/ui/PlayControlsRowViewTest.java @@ -16,11 +16,11 @@ package com.android.tv.tests.ui; +import static com.android.tv.testing.uihelper.Constants.CHANNEL_BANNER; import static com.android.tv.testing.uihelper.Constants.FOCUSED_VIEW; import static com.android.tv.testing.uihelper.Constants.MENU; import static com.android.tv.testing.uihelper.UiDeviceAsserts.assertWaitForCondition; -import android.support.test.filters.SdkSuppress; import android.support.test.filters.SmallTest; import android.support.test.uiautomator.BySelector; import android.support.test.uiautomator.UiObject2; @@ -32,9 +32,8 @@ import com.android.tv.testing.testinput.TvTestInputConstants; import com.android.tv.testing.uihelper.DialogHelper; @SmallTest -@SdkSuppress(minSdkVersion = 23) public class PlayControlsRowViewTest extends LiveChannelsTestCase { - private static final int BUTTON_INDEX_PLAY_PAUSE = 2; + private static final String BUTTON_ID_PLAY_PAUSE = "com.android.tv:id/play_pause"; private BySelector mBySettingsSidePanel; @@ -42,7 +41,9 @@ public class PlayControlsRowViewTest extends LiveChannelsTestCase { protected void setUp() throws Exception { super.setUp(); mLiveChannelsHelper.assertAppStarted(); - pressKeysForChannel(TvTestInputConstants.CH_1_DEFAULT_DONT_MODIFY); + pressKeysForChannel(TvTestInputConstants.CH_2); + // Wait until KeypadChannelSwitchView closes. + assertWaitForCondition(mDevice, Until.hasObject(CHANNEL_BANNER)); // Tune to a new channel to ensure that the channel is changed. mDevice.pressDPadUp(); getInstrumentation().waitForIdleSync(); @@ -56,7 +57,7 @@ public class PlayControlsRowViewTest extends LiveChannelsTestCase { public void testFocusedViewInNormalCase() { mMenuHelper.showMenu(); mMenuHelper.assertNavigateToPlayControlsRow(); - assertButtonHasFocus(BUTTON_INDEX_PLAY_PAUSE); + assertButtonHasFocus(BUTTON_ID_PLAY_PAUSE); mDevice.pressBack(); } @@ -69,49 +70,30 @@ public class PlayControlsRowViewTest extends LiveChannelsTestCase { // Fast forward button mDevice.pressKeyCode(KeyEvent.KEYCODE_MEDIA_FAST_FORWARD); mMenuHelper.assertWaitForMenu(); - assertButtonHasFocus(BUTTON_INDEX_PLAY_PAUSE); + assertButtonHasFocus(BUTTON_ID_PLAY_PAUSE); mDevice.pressBack(); // Next button mDevice.pressKeyCode(KeyEvent.KEYCODE_MEDIA_NEXT); mMenuHelper.assertWaitForMenu(); - assertButtonHasFocus(BUTTON_INDEX_PLAY_PAUSE); - mDevice.pressBack(); - } - - /** - * Tests the case when the rewinding action is disabled. - * In this case, the button corresponding to the action is disabled, so play/pause button should - * have the focus. - */ - public void testFocusedViewWithDisabledActionBackward() { - // Previous button - mDevice.pressKeyCode(KeyEvent.KEYCODE_MEDIA_PREVIOUS); - mMenuHelper.assertWaitForMenu(); - assertButtonHasFocus(BUTTON_INDEX_PLAY_PAUSE); - mDevice.pressBack(); - - // Rewind button - mDevice.pressKeyCode(KeyEvent.KEYCODE_MEDIA_REWIND); - mMenuHelper.assertWaitForMenu(); - assertButtonHasFocus(BUTTON_INDEX_PLAY_PAUSE); + assertButtonHasFocus(BUTTON_ID_PLAY_PAUSE); mDevice.pressBack(); } public void testFocusedViewInMenu() { mMenuHelper.showMenu(); mDevice.pressKeyCode(KeyEvent.KEYCODE_MEDIA_PLAY); - assertButtonHasFocus(BUTTON_INDEX_PLAY_PAUSE); + assertButtonHasFocus(BUTTON_ID_PLAY_PAUSE); mMenuHelper.assertNavigateToRow(R.string.menu_title_channels); mDevice.pressKeyCode(KeyEvent.KEYCODE_MEDIA_NEXT); - assertButtonHasFocus(BUTTON_INDEX_PLAY_PAUSE); + assertButtonHasFocus(BUTTON_ID_PLAY_PAUSE); } public void testKeepPausedWhileParentalControlChange() { // Pause the playback. mDevice.pressKeyCode(KeyEvent.KEYCODE_MEDIA_PAUSE); mMenuHelper.assertWaitForMenu(); - assertButtonHasFocus(BUTTON_INDEX_PLAY_PAUSE); + assertButtonHasFocus(BUTTON_ID_PLAY_PAUSE); // Show parental controls fragment. mMenuHelper.assertPressOptionsSettings(); assertWaitForCondition(mDevice, Until.hasObject(mBySettingsSidePanel)); @@ -130,14 +112,14 @@ public class PlayControlsRowViewTest extends LiveChannelsTestCase { mDevice.pressBack(); // Return to the main menu. mMenuHelper.assertWaitForMenu(); - assertButtonHasFocus(BUTTON_INDEX_PLAY_PAUSE); + assertButtonHasFocus(BUTTON_ID_PLAY_PAUSE); } public void testKeepPausedAfterVisitingHome() { // Pause the playback. mDevice.pressKeyCode(KeyEvent.KEYCODE_MEDIA_PAUSE); mMenuHelper.assertWaitForMenu(); - assertButtonHasFocus(BUTTON_INDEX_PLAY_PAUSE); + assertButtonHasFocus(BUTTON_ID_PLAY_PAUSE); // Press HOME twice to visit the home screen and return to Live TV. mDevice.pressHome(); // Wait until home screen is shown. @@ -147,19 +129,15 @@ public class PlayControlsRowViewTest extends LiveChannelsTestCase { mDevice.waitForIdle(); // Return to the main menu. mMenuHelper.assertWaitForMenu(); - assertButtonHasFocus(BUTTON_INDEX_PLAY_PAUSE); + assertButtonHasFocus(BUTTON_ID_PLAY_PAUSE); } - private void assertButtonHasFocus(int expectedButtonIndex) { + private void assertButtonHasFocus(String buttonId) { UiObject2 menu = mDevice.findObject(MENU); UiObject2 focusedView = menu.findObject(FOCUSED_VIEW); assertNotNull("Play controls row doesn't have a focused child.", focusedView); UiObject2 focusedButtonGroup = focusedView.getParent(); assertNotNull("The focused item should have parent", focusedButtonGroup); - UiObject2 controlBar = focusedButtonGroup.getParent(); - assertNotNull("The focused item should have grandparent", controlBar); - assertTrue("The grandparent should have more than five children", - controlBar.getChildCount() >= 5); - assertEquals(controlBar.getChildren().get(expectedButtonIndex), focusedButtonGroup); + assertEquals(buttonId, focusedButtonGroup.getResourceName()); } } diff --git a/tests/input/res/values/strings.xml b/tests/input/res/values/strings.xml index 3f2ab3f7..4ef43955 100644 --- a/tests/input/res/values/strings.xml +++ b/tests/input/res/values/strings.xml @@ -15,7 +15,6 @@ --> Test TV Inputs - About TV Test Inputs Version: %1$s Test TV Input Test Input diff --git a/tests/jank/src/com/android/tv/tests/jank/ProgramGuideJankTest.java b/tests/jank/src/com/android/tv/tests/jank/ProgramGuideJankTest.java index 7d751c4c..03796cfa 100644 --- a/tests/jank/src/com/android/tv/tests/jank/ProgramGuideJankTest.java +++ b/tests/jank/src/com/android/tv/tests/jank/ProgramGuideJankTest.java @@ -30,7 +30,6 @@ import com.android.tv.testing.uihelper.ByResource; import com.android.tv.testing.uihelper.Constants; import com.android.tv.testing.uihelper.LiveChannelsUiDeviceHelper; import com.android.tv.testing.uihelper.MenuHelper; -import com.android.tv.testing.uihelper.UiDeviceUtils; /** * Jank tests for the program guide. @@ -83,7 +82,7 @@ public class ProgramGuideJankTest extends JankTestBase { } @JankTest(expectedFrames = EXPECTED_FRAMES, - beforeLoop = "showProgramGuide", + beforeLoop = "showAndFocusProgramGuide", afterLoop = "clearProgramGuide") @GfxMonitor(processName = Utils.LIVE_CHANNELS_PROCESS_NAME) public void testScrollDown() { @@ -95,7 +94,7 @@ public class ProgramGuideJankTest extends JankTestBase { } @JankTest(expectedFrames = EXPECTED_FRAMES, - beforeLoop = "showProgramGuide", + beforeLoop = "showAndFocusProgramGuide", afterLoop = "clearProgramGuide") @GfxMonitor(processName = Utils.LIVE_CHANNELS_PROCESS_NAME) public void testScrollRight() { @@ -128,11 +127,17 @@ public class ProgramGuideJankTest extends JankTestBase { assertWaitForCondition(mDevice, Until.gone(Constants.PROGRAM_GUIDE)); } - // It's public to be used with @JankTest annotation. public void showProgramGuide() { selectProgramGuideMenuItem(); mDevice.pressDPadCenter(); assertWaitForCondition(mDevice, Until.hasObject(Constants.PROGRAM_GUIDE)); + } + + // It's public to be used with @JankTest annotation. + public void showAndFocusProgramGuide() { + selectProgramGuideMenuItem(); + mDevice.pressDPadCenter(); + assertWaitForCondition(mDevice, Until.hasObject(Constants.PROGRAM_GUIDE)); // If the side panel grid is visible (and thus has focus), move right to clear it. if (mDevice.hasObject( ByResource.id(mTargetResources, R.id.program_guide_side_panel_grid_view))) { diff --git a/tests/unit/src/com/android/tv/MainActivityTest.java b/tests/unit/src/com/android/tv/MainActivityTest.java index b2fe6745..8425597f 100644 --- a/tests/unit/src/com/android/tv/MainActivityTest.java +++ b/tests/unit/src/com/android/tv/MainActivityTest.java @@ -39,7 +39,6 @@ public class MainActivityTest extends BaseMainActivityTestCase { waitUntilChannelLoadingFinish(); List channelList = mActivity.getChannelDataManager().getChannelList(); assertTrue("Expected at least one channel", channelList.size() > 0); - assertFalse("PIP disabled", mActivity.isPipEnabled()); } public void testTuneToChannel() throws Throwable { diff --git a/tests/unit/src/com/android/tv/data/ChannelNumberTest.java b/tests/unit/src/com/android/tv/data/ChannelNumberTest.java index 4e6e9f3c..d074baae 100644 --- a/tests/unit/src/com/android/tv/data/ChannelNumberTest.java +++ b/tests/unit/src/com/android/tv/data/ChannelNumberTest.java @@ -42,14 +42,14 @@ public class ChannelNumberTest extends TestCase { */ public void testParseChannelNumber() { assertNull(parseChannelNumber("")); - assertNull(parseChannelNumber(" ")); + assertNull(parseChannelNumber("-")); assertNull(parseChannelNumber("abcd12")); assertNull(parseChannelNumber("12abcd")); assertNull(parseChannelNumber("-12")); assertChannelEquals(parseChannelNumber("1"), "1", false, ""); - assertChannelEquals(parseChannelNumber("1234 4321"), "1234", true, "4321"); + assertChannelEquals(parseChannelNumber("1234-4321"), "1234", true, "4321"); assertChannelEquals(parseChannelNumber("3-4"), "3", true, "4"); - assertChannelEquals(parseChannelNumber("5.6"), "5", true, "6"); + assertChannelEquals(parseChannelNumber("5-6"), "5", true, "6"); } /** @@ -59,13 +59,11 @@ public class ChannelNumberTest extends TestCase { new ComparableTester() .addEquivalentGroup(parseChannelNumber("1"), parseChannelNumber("1")) .addEquivalentGroup(parseChannelNumber("2")) - .addEquivalentGroup(parseChannelNumber("2 1"), parseChannelNumber("2.1"), - parseChannelNumber("2-1")) + .addEquivalentGroup(parseChannelNumber("2-1")) .addEquivalentGroup(parseChannelNumber("2-2")) .addEquivalentGroup(parseChannelNumber("2-10")) .addEquivalentGroup(parseChannelNumber("3")) - .addEquivalentGroup(parseChannelNumber("4"), parseChannelNumber("4 0"), - parseChannelNumber("4.0"), parseChannelNumber("4-0")) + .addEquivalentGroup(parseChannelNumber("4"), parseChannelNumber("4-0")) .addEquivalentGroup(parseChannelNumber("10")) .addEquivalentGroup(parseChannelNumber("100")) .test(); diff --git a/tests/unit/src/com/android/tv/data/ChannelTest.java b/tests/unit/src/com/android/tv/data/ChannelTest.java index 95e3ee90..f3d80cbe 100644 --- a/tests/unit/src/com/android/tv/data/ChannelTest.java +++ b/tests/unit/src/com/android/tv/data/ChannelTest.java @@ -226,7 +226,6 @@ public class ChannelTest extends AndroidTestCase { * See b/23031603. */ public void testComparatorLabel() { - TvInputManagerHelper manager = Mockito.mock(TvInputManagerHelper.class); Mockito.when(manager.isPartnerInput(Matchers.anyString())).thenAnswer( new Answer() { @@ -254,6 +253,29 @@ public class ChannelTest extends AndroidTestCase { comparatorTester.test(); } + public void testNormalizeChannelNumber() { + assertNormalizedDisplayNumber(null, null); + assertNormalizedDisplayNumber("", ""); + assertNormalizedDisplayNumber("1", "1"); + assertNormalizedDisplayNumber("abcde", "abcde"); + assertNormalizedDisplayNumber("1-1", "1-1"); + assertNormalizedDisplayNumber("1.1", "1-1"); + assertNormalizedDisplayNumber("1 1", "1-1"); + assertNormalizedDisplayNumber("1\u058a1", "1-1"); + assertNormalizedDisplayNumber("1\u05be1", "1-1"); + assertNormalizedDisplayNumber("1\u14001", "1-1"); + assertNormalizedDisplayNumber("1\u18061", "1-1"); + assertNormalizedDisplayNumber("1\u20101", "1-1"); + assertNormalizedDisplayNumber("1\u20111", "1-1"); + assertNormalizedDisplayNumber("1\u20121", "1-1"); + assertNormalizedDisplayNumber("1\u20131", "1-1"); + assertNormalizedDisplayNumber("1\u20141", "1-1"); + } + + private void assertNormalizedDisplayNumber(String displayNumber, String normalized) { + assertEquals(normalized, Channel.normalizeDisplayNumber(displayNumber)); + } + private class TestChannelComparator extends Channel.DefaultComparator { public TestChannelComparator(TvInputManagerHelper manager) { super(null, manager); diff --git a/tests/unit/src/com/android/tv/data/ProgramTest.java b/tests/unit/src/com/android/tv/data/ProgramTest.java index 7e474cd6..08dd17f3 100644 --- a/tests/unit/src/com/android/tv/data/ProgramTest.java +++ b/tests/unit/src/com/android/tv/data/ProgramTest.java @@ -19,13 +19,11 @@ import static android.media.tv.TvContract.Programs.Genres.COMEDY; import static android.media.tv.TvContract.Programs.Genres.FAMILY_KIDS; import com.android.tv.data.Program.CriticScore; -import com.android.tv.dvr.SeriesRecording; import android.media.tv.TvContentRating; import android.media.tv.TvContract.Programs.Genres; import android.os.Parcel; import android.support.test.filters.SmallTest; -import android.util.Log; import junit.framework.TestCase; diff --git a/tests/unit/src/com/android/tv/dvr/BaseDvrDataManagerTest.java b/tests/unit/src/com/android/tv/dvr/BaseDvrDataManagerTest.java index 1292759e..6c1b1976 100644 --- a/tests/unit/src/com/android/tv/dvr/BaseDvrDataManagerTest.java +++ b/tests/unit/src/com/android/tv/dvr/BaseDvrDataManagerTest.java @@ -21,6 +21,7 @@ import android.support.test.filters.SmallTest; import android.test.AndroidTestCase; import android.test.MoreAsserts; +import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.testing.FakeClock; import com.android.tv.testing.dvr.RecordingTestUtils; diff --git a/tests/unit/src/com/android/tv/dvr/DvrDataManagerImplTest.java b/tests/unit/src/com/android/tv/dvr/DvrDataManagerImplTest.java index b822f164..d142f432 100644 --- a/tests/unit/src/com/android/tv/dvr/DvrDataManagerImplTest.java +++ b/tests/unit/src/com/android/tv/dvr/DvrDataManagerImplTest.java @@ -18,6 +18,7 @@ package com.android.tv.dvr; import android.support.test.filters.SmallTest; +import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.testing.dvr.RecordingTestUtils; import junit.framework.TestCase; diff --git a/tests/unit/src/com/android/tv/dvr/DvrDataManagerInMemoryImpl.java b/tests/unit/src/com/android/tv/dvr/DvrDataManagerInMemoryImpl.java index 85e35c4d..b2164dec 100644 --- a/tests/unit/src/com/android/tv/dvr/DvrDataManagerInMemoryImpl.java +++ b/tests/unit/src/com/android/tv/dvr/DvrDataManagerInMemoryImpl.java @@ -24,7 +24,10 @@ import android.util.Log; import android.util.Range; import com.android.tv.common.SoftPreconditions; -import com.android.tv.dvr.ScheduledRecording.RecordingState; +import com.android.tv.dvr.data.RecordedProgram; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.data.ScheduledRecording.RecordingState; +import com.android.tv.dvr.data.SeriesRecording; import com.android.tv.util.Clock; import java.util.ArrayList; @@ -37,7 +40,7 @@ import java.util.concurrent.atomic.AtomicLong; /** * A DVR Data manager that stores values in memory suitable for testing. */ -final class DvrDataManagerInMemoryImpl extends BaseDvrDataManager { +public final class DvrDataManagerInMemoryImpl extends BaseDvrDataManager { private final static String TAG = "DvrDataManagerInMemory"; private final AtomicLong mNextId = new AtomicLong(1); private final Map mScheduledRecordings = new HashMap<>(); diff --git a/tests/unit/src/com/android/tv/dvr/DvrDbSyncTest.java b/tests/unit/src/com/android/tv/dvr/DvrDbSyncTest.java deleted file mode 100644 index 7cb3721c..00000000 --- a/tests/unit/src/com/android/tv/dvr/DvrDbSyncTest.java +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.tv.dvr; - -import static org.mockito.Matchers.anyObject; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import android.os.Build; -import android.support.test.filters.SdkSuppress; -import android.support.test.filters.SmallTest; -import android.test.AndroidTestCase; - -import com.android.tv.data.ChannelDataManager; -import com.android.tv.data.Program; - -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -/** - * Tests for {@link DvrScheduleManager} - */ -@SmallTest -@SdkSuppress(minSdkVersion = Build.VERSION_CODES.N) -public class DvrDbSyncTest extends AndroidTestCase { - private static final String INPUT_ID = "input_id"; - private static final long BASE_PROGRAM_ID = 1; - private static final long BASE_START_TIME_MS = 0; - private static final long BASE_END_TIME_MS = 1; - private static final String BASE_SEASON_NUMBER = "2"; - private static final String BASE_EPISODE_NUMBER = "3"; - private static final Program BASE_PROGRAM = new Program.Builder().setId(BASE_PROGRAM_ID) - .setStartTimeUtcMillis(BASE_START_TIME_MS).setEndTimeUtcMillis(BASE_END_TIME_MS) - .setSeasonNumber(BASE_SEASON_NUMBER).setEpisodeNumber(BASE_EPISODE_NUMBER).build(); - private static final ScheduledRecording BASE_SCHEDULE = - ScheduledRecording.builder(INPUT_ID, BASE_PROGRAM).build(); - - private DvrDbSync mDbSync; - @Mock private DvrDataManagerImpl mDataManager; - @Mock private ChannelDataManager mChannelDataManager; - - @Override - protected void setUp() throws Exception { - super.setUp(); - MockitoAnnotations.initMocks(this); - when(mChannelDataManager.isDbLoadFinished()).thenReturn(true); - mDbSync = new DvrDbSync(getContext(), mDataManager, mChannelDataManager); - } - - public void testHandleUpdateProgram_null() { - addSchedule(BASE_PROGRAM_ID, BASE_SCHEDULE); - mDbSync.handleUpdateProgram(null, BASE_PROGRAM_ID); - verify(mDataManager).removeScheduledRecording(BASE_SCHEDULE); - } - - public void testHandleUpdateProgram_changeTimeNotStarted() { - addSchedule(BASE_PROGRAM_ID, BASE_SCHEDULE); - long startTimeMs = BASE_START_TIME_MS + 1; - long endTimeMs = BASE_END_TIME_MS + 1; - Program program = new Program.Builder(BASE_PROGRAM).setStartTimeUtcMillis(startTimeMs) - .setEndTimeUtcMillis(endTimeMs).build(); - mDbSync.handleUpdateProgram(program, BASE_PROGRAM_ID); - assertUpdateScheduleCalled(program); - } - - public void testHandleUpdateProgram_changeTimeInProgressNotCalled() { - addSchedule(BASE_PROGRAM_ID, ScheduledRecording.buildFrom(BASE_SCHEDULE) - .setState(ScheduledRecording.STATE_RECORDING_IN_PROGRESS).build()); - long startTimeMs = BASE_START_TIME_MS + 1; - Program program = new Program.Builder(BASE_PROGRAM).setStartTimeUtcMillis(startTimeMs) - .build(); - mDbSync.handleUpdateProgram(program, BASE_PROGRAM_ID); - verify(mDataManager, never()).updateScheduledRecording(anyObject()); - } - - public void testHandleUpdateProgram_changeSeason() { - addSchedule(BASE_PROGRAM_ID, BASE_SCHEDULE); - String seasonNumber = BASE_SEASON_NUMBER + "1"; - String episodeNumber = BASE_EPISODE_NUMBER + "1"; - Program program = new Program.Builder(BASE_PROGRAM).setSeasonNumber(seasonNumber) - .setEpisodeNumber(episodeNumber).build(); - mDbSync.handleUpdateProgram(program, BASE_PROGRAM_ID); - assertUpdateScheduleCalled(program); - } - - public void testHandleUpdateProgram_finished() { - addSchedule(BASE_PROGRAM_ID, ScheduledRecording.buildFrom(BASE_SCHEDULE) - .setState(ScheduledRecording.STATE_RECORDING_FINISHED).build()); - String seasonNumber = BASE_SEASON_NUMBER + "1"; - String episodeNumber = BASE_EPISODE_NUMBER + "1"; - Program program = new Program.Builder(BASE_PROGRAM).setSeasonNumber(seasonNumber) - .setEpisodeNumber(episodeNumber).build(); - mDbSync.handleUpdateProgram(program, BASE_PROGRAM_ID); - verify(mDataManager, never()).updateScheduledRecording(anyObject()); - } - - private void addSchedule(long programId, ScheduledRecording schedule) { - when(mDataManager.getScheduledRecordingForProgramId(programId)).thenReturn(schedule); - } - - private void assertUpdateScheduleCalled(Program program) { - verify(mDataManager).updateScheduledRecording( - eq(ScheduledRecording.builder(INPUT_ID, program).build())); - } -} diff --git a/tests/unit/src/com/android/tv/dvr/DvrRecordingServiceTest.java b/tests/unit/src/com/android/tv/dvr/DvrRecordingServiceTest.java deleted file mode 100644 index 0a203ede..00000000 --- a/tests/unit/src/com/android/tv/dvr/DvrRecordingServiceTest.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.tv.dvr; - -import static org.mockito.Mockito.verify; - -import android.os.Build; -import android.support.test.filters.SdkSuppress; -import android.support.test.filters.SmallTest; -import android.test.ServiceTestCase; - -import com.android.tv.common.feature.CommonFeatures; -import com.android.tv.common.feature.TestableFeature; -import com.android.tv.testing.FakeClock; - -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; - -/** - * Tests for {@link DvrRecordingService}. - */ -@SmallTest -@SdkSuppress(minSdkVersion = Build.VERSION_CODES.N) -public class DvrRecordingServiceTest extends ServiceTestCase { - @Mock Scheduler mMockScheduler; - private final TestableFeature mDvrFeature = CommonFeatures.DVR; - private final FakeClock mFakeClock = FakeClock.createWithCurrentTime(); - - @Override - protected void setUp() throws Exception { - super.setUp(); - mDvrFeature.enableForTest(); - MockitoAnnotations.initMocks(this); - setupService(); - DvrRecordingService service = getService(); - service.setScheduler(mMockScheduler); - } - - @Override - protected void tearDown() throws Exception { - mDvrFeature.resetForTests(); - super.tearDown(); - } - - public DvrRecordingServiceTest() { - super(DvrRecordingService.class); - } - - public void testStartService_null() throws Exception { - startService(null); - verify(mMockScheduler, Mockito.only()).update(); - } -} \ No newline at end of file diff --git a/tests/unit/src/com/android/tv/dvr/DvrScheduleManagerTest.java b/tests/unit/src/com/android/tv/dvr/DvrScheduleManagerTest.java index 2850a5f7..cfb27211 100644 --- a/tests/unit/src/com/android/tv/dvr/DvrScheduleManagerTest.java +++ b/tests/unit/src/com/android/tv/dvr/DvrScheduleManagerTest.java @@ -20,6 +20,8 @@ import android.support.test.filters.SmallTest; import android.test.MoreAsserts; import android.util.Range; +import com.android.tv.dvr.DvrScheduleManager.ConflictInfo; +import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.testing.dvr.RecordingTestUtils; import junit.framework.TestCase; @@ -28,7 +30,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.Map; /** * Tests for {@link DvrScheduleManager} @@ -586,49 +587,80 @@ public class DvrScheduleManagerTest extends TestCase { RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(++channelId, --priority, 50L, 900L) )); - Map conflictsInfo = DvrScheduleManager - .getConflictingSchedulesInfo(schedules, 1); - - assertNull(conflictsInfo.get(schedules.get(0))); - assertFalse(conflictsInfo.get(schedules.get(1))); - assertTrue(conflictsInfo.get(schedules.get(2))); - assertTrue(conflictsInfo.get(schedules.get(3))); - assertNull(conflictsInfo.get(schedules.get(4))); - assertTrue(conflictsInfo.get(schedules.get(5))); - assertNull(conflictsInfo.get(schedules.get(6))); - assertFalse(conflictsInfo.get(schedules.get(7))); - assertFalse(conflictsInfo.get(schedules.get(8))); - assertFalse(conflictsInfo.get(schedules.get(9))); - assertFalse(conflictsInfo.get(schedules.get(10))); - - conflictsInfo = DvrScheduleManager - .getConflictingSchedulesInfo(schedules, 2); - - assertNull(conflictsInfo.get(schedules.get(0))); - assertNull(conflictsInfo.get(schedules.get(1))); - assertNull(conflictsInfo.get(schedules.get(2))); - assertNull(conflictsInfo.get(schedules.get(3))); - assertNull(conflictsInfo.get(schedules.get(4))); - assertNull(conflictsInfo.get(schedules.get(5))); - assertNull(conflictsInfo.get(schedules.get(6))); - assertFalse(conflictsInfo.get(schedules.get(7))); - assertFalse(conflictsInfo.get(schedules.get(8))); - assertFalse(conflictsInfo.get(schedules.get(9))); - assertTrue(conflictsInfo.get(schedules.get(10))); - - conflictsInfo = DvrScheduleManager - .getConflictingSchedulesInfo(schedules, 3); - - assertNull(conflictsInfo.get(schedules.get(0))); - assertNull(conflictsInfo.get(schedules.get(1))); - assertNull(conflictsInfo.get(schedules.get(2))); - assertNull(conflictsInfo.get(schedules.get(3))); - assertNull(conflictsInfo.get(schedules.get(4))); - assertNull(conflictsInfo.get(schedules.get(5))); - assertNull(conflictsInfo.get(schedules.get(6))); - assertNull(conflictsInfo.get(schedules.get(7))); - assertTrue(conflictsInfo.get(schedules.get(8))); - assertNull(conflictsInfo.get(schedules.get(9))); - assertTrue(conflictsInfo.get(schedules.get(10))); + List conflicts = DvrScheduleManager.getConflictingSchedulesInfo(schedules, 1); + + assertNotInList(schedules.get(0), conflicts); + assertFullConflict(schedules.get(1), conflicts); + assertPartialConflict(schedules.get(2), conflicts); + assertPartialConflict(schedules.get(3), conflicts); + assertNotInList(schedules.get(4), conflicts); + assertPartialConflict(schedules.get(5), conflicts); + assertNotInList(schedules.get(6), conflicts); + assertFullConflict(schedules.get(7), conflicts); + assertFullConflict(schedules.get(8), conflicts); + assertFullConflict(schedules.get(9), conflicts); + assertFullConflict(schedules.get(10), conflicts); + + conflicts = DvrScheduleManager.getConflictingSchedulesInfo(schedules, 2); + + assertNotInList(schedules.get(0), conflicts); + assertNotInList(schedules.get(1), conflicts); + assertNotInList(schedules.get(2), conflicts); + assertNotInList(schedules.get(3), conflicts); + assertNotInList(schedules.get(4), conflicts); + assertNotInList(schedules.get(5), conflicts); + assertNotInList(schedules.get(6), conflicts); + assertFullConflict(schedules.get(7), conflicts); + assertFullConflict(schedules.get(8), conflicts); + assertFullConflict(schedules.get(9), conflicts); + assertPartialConflict(schedules.get(10), conflicts); + + conflicts = DvrScheduleManager.getConflictingSchedulesInfo(schedules, 3); + + assertNotInList(schedules.get(0), conflicts); + assertNotInList(schedules.get(1), conflicts); + assertNotInList(schedules.get(2), conflicts); + assertNotInList(schedules.get(3), conflicts); + assertNotInList(schedules.get(4), conflicts); + assertNotInList(schedules.get(5), conflicts); + assertNotInList(schedules.get(6), conflicts); + assertNotInList(schedules.get(7), conflicts); + assertPartialConflict(schedules.get(8), conflicts); + assertNotInList(schedules.get(9), conflicts); + assertPartialConflict(schedules.get(10), conflicts); + } + + private void assertNotInList(ScheduledRecording schedule, List conflicts) { + for (ConflictInfo conflictInfo : conflicts) { + if (conflictInfo.schedule.equals(schedule)) { + fail(schedule + " conflicts with others."); + } + } + } + + private void assertPartialConflict(ScheduledRecording schedule, List conflicts) { + for (ConflictInfo conflictInfo : conflicts) { + if (conflictInfo.schedule.equals(schedule)) { + if (conflictInfo.partialConflict) { + return; + } else { + fail(schedule + " fully conflicts with others."); + } + } + } + fail(schedule + " doesn't conflict"); + } + + private void assertFullConflict(ScheduledRecording schedule, List conflicts) { + for (ConflictInfo conflictInfo : conflicts) { + if (conflictInfo.schedule.equals(schedule)) { + if (!conflictInfo.partialConflict) { + return; + } else { + fail(schedule + " partially conflicts with others."); + } + } + } + fail(schedule + " doesn't conflict"); } } \ No newline at end of file diff --git a/tests/unit/src/com/android/tv/dvr/EpisodicProgramLoadTaskTest.java b/tests/unit/src/com/android/tv/dvr/EpisodicProgramLoadTaskTest.java deleted file mode 100644 index 2172d488..00000000 --- a/tests/unit/src/com/android/tv/dvr/EpisodicProgramLoadTaskTest.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.dvr; - -import android.os.Build; -import android.support.test.filters.SdkSuppress; -import android.support.test.filters.SmallTest; -import android.test.AndroidTestCase; - -import com.android.tv.dvr.EpisodicProgramLoadTask.ScheduledEpisode; - -import java.util.ArrayList; -import java.util.List; - -/** - * Tests for {@link EpisodicProgramLoadTask} - */ -@SmallTest -@SdkSuppress(minSdkVersion = Build.VERSION_CODES.N) -public class EpisodicProgramLoadTaskTest extends AndroidTestCase { - private static final long SERIES_RECORDING_ID1 = 1; - private static final long SERIES_RECORDING_ID2 = 2; - private static final String SEASON_NUMBER1 = "SEASON NUMBER1"; - private static final String SEASON_NUMBER2 = "SEASON NUMBER2"; - private static final String EPISODE_NUMBER1 = "EPISODE NUMBER1"; - private static final String EPISODE_NUMBER2 = "EPISODE NUMBER2"; - - public void testEpisodeAlreadyScheduled_true() { - List episodes = new ArrayList<>(); - ScheduledEpisode episode = new ScheduledEpisode(SERIES_RECORDING_ID1, SEASON_NUMBER1, - EPISODE_NUMBER1); - episodes.add(episode); - assertTrue(EpisodicProgramLoadTask.isEpisodeScheduled(episodes, - new ScheduledEpisode(SERIES_RECORDING_ID1, SEASON_NUMBER1, EPISODE_NUMBER1))); - } - - public void testEpisodeAlreadyScheduled_false() { - List episodes = new ArrayList<>(); - ScheduledEpisode episode = new ScheduledEpisode(SERIES_RECORDING_ID1, SEASON_NUMBER1, - EPISODE_NUMBER1); - episodes.add(episode); - assertFalse(EpisodicProgramLoadTask.isEpisodeScheduled(episodes, - new ScheduledEpisode(SERIES_RECORDING_ID2, SEASON_NUMBER1, EPISODE_NUMBER1))); - assertFalse(EpisodicProgramLoadTask.isEpisodeScheduled(episodes, - new ScheduledEpisode(SERIES_RECORDING_ID1, SEASON_NUMBER2, EPISODE_NUMBER1))); - assertFalse(EpisodicProgramLoadTask.isEpisodeScheduled(episodes, - new ScheduledEpisode(SERIES_RECORDING_ID1, SEASON_NUMBER1, EPISODE_NUMBER2))); - } - - public void testEpisodeAlreadyScheduled_null() { - List episodes = new ArrayList<>(); - ScheduledEpisode episode = new ScheduledEpisode(SERIES_RECORDING_ID1, SEASON_NUMBER1, - EPISODE_NUMBER1); - episodes.add(episode); - assertFalse(EpisodicProgramLoadTask.isEpisodeScheduled(episodes, - new ScheduledEpisode(SERIES_RECORDING_ID1, null, EPISODE_NUMBER1))); - assertFalse(EpisodicProgramLoadTask.isEpisodeScheduled(episodes, - new ScheduledEpisode(SERIES_RECORDING_ID1, SEASON_NUMBER1, null))); - assertFalse(EpisodicProgramLoadTask.isEpisodeScheduled(episodes, - new ScheduledEpisode(SERIES_RECORDING_ID1, null, null))); - } -} diff --git a/tests/unit/src/com/android/tv/dvr/InputTaskSchedulerTest.java b/tests/unit/src/com/android/tv/dvr/InputTaskSchedulerTest.java deleted file mode 100644 index 85c78ce2..00000000 --- a/tests/unit/src/com/android/tv/dvr/InputTaskSchedulerTest.java +++ /dev/null @@ -1,221 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.tv.dvr; - -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyLong; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.after; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.timeout; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import android.app.AlarmManager; -import android.media.tv.TvInputInfo; -import android.os.Build; -import android.os.Handler; -import android.os.Looper; -import android.support.test.filters.SdkSuppress; -import android.support.test.filters.SmallTest; -import android.test.AndroidTestCase; - -import com.android.tv.InputSessionManager; -import com.android.tv.data.Channel; -import com.android.tv.data.ChannelDataManager; -import com.android.tv.dvr.InputTaskScheduler.RecordingTaskFactory; -import com.android.tv.testing.FakeClock; -import com.android.tv.testing.dvr.RecordingTestUtils; -import com.android.tv.util.Clock; -import com.android.tv.util.TestUtils; - -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.TimeUnit; - -/** - * Tests for {@link InputTaskScheduler}. - */ -@SmallTest -@SdkSuppress(minSdkVersion = Build.VERSION_CODES.N) -public class InputTaskSchedulerTest extends AndroidTestCase { - private static final String INPUT_ID = "input_id"; - private static final int CHANNEL_ID = 1; - private static final long LISTENER_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(1); - private static final int TUNER_COUNT_ONE = 1; - private static final int TUNER_COUNT_TWO = 2; - private static final long LOW_PRIORITY = 1; - private static final long HIGH_PRIORITY = 2; - - private FakeClock mFakeClock; - private InputTaskScheduler mScheduler; - @Mock private DvrManager mDvrManager; - @Mock private WritableDvrDataManager mDataManager; - @Mock private InputSessionManager mSessionManager; - @Mock private AlarmManager mMockAlarmManager; - @Mock private ChannelDataManager mChannelDataManager; - private List mRecordingTasks; - - @Override - protected void setUp() throws Exception { - super.setUp(); - if (Looper.myLooper() == null) { - Looper.prepare(); - } - Handler fakeMainHandler = new Handler(); - Handler workerThreadHandler = new Handler(); - mRecordingTasks = new ArrayList(); - MockitoAnnotations.initMocks(this); - mFakeClock = FakeClock.createWithCurrentTime(); - TvInputInfo input = createTvInputInfo(TUNER_COUNT_ONE); - mScheduler = new InputTaskScheduler(getContext(), input, Looper.myLooper(), - mChannelDataManager, mDvrManager, mDataManager, mSessionManager, mFakeClock, - fakeMainHandler, workerThreadHandler, new RecordingTaskFactory() { - @Override - public RecordingTask createRecordingTask(ScheduledRecording scheduledRecording, - Channel channel, DvrManager dvrManager, - InputSessionManager sessionManager, WritableDvrDataManager dataManager, - Clock clock) { - RecordingTask task = mock(RecordingTask.class); - when(task.getPriority()).thenReturn(scheduledRecording.getPriority()); - when(task.getEndTimeMs()).thenReturn(scheduledRecording.getEndTimeMs()); - mRecordingTasks.add(task); - return task; - } - }); - } - - @Override - protected void tearDown() throws Exception { - super.tearDown(); - } - - public void testAddSchedule_past() throws Exception { - ScheduledRecording r = RecordingTestUtils.createTestRecordingWithPeriod(INPUT_ID, - CHANNEL_ID, 0L, 1L); - when(mDataManager.getScheduledRecording(anyLong())).thenReturn(r); - mScheduler.handleAddSchedule(r); - mScheduler.handleBuildSchedule(); - verify(mDataManager, timeout((int) LISTENER_TIMEOUT_MS).times(1)) - .changeState(any(ScheduledRecording.class), - eq(ScheduledRecording.STATE_RECORDING_FAILED)); - } - - public void testAddSchedule_start() throws Exception { - mScheduler.handleAddSchedule(RecordingTestUtils.createTestRecordingWithPeriod(INPUT_ID, - CHANNEL_ID, mFakeClock.currentTimeMillis(), - mFakeClock.currentTimeMillis() + TimeUnit.HOURS.toMillis(1))); - mScheduler.handleBuildSchedule(); - verify(mRecordingTasks.get(0), timeout((int) LISTENER_TIMEOUT_MS).times(1)).start(); - } - - public void testAddSchedule_consecutiveNoStop() throws Exception { - long startTimeMs = mFakeClock.currentTimeMillis(); - long endTimeMs = startTimeMs + TimeUnit.SECONDS.toMillis(1); - long id = 0; - mScheduler.handleAddSchedule( - RecordingTestUtils.createTestRecordingWithIdAndPriorityAndPeriod(++id, CHANNEL_ID, - LOW_PRIORITY, startTimeMs, endTimeMs)); - mScheduler.handleBuildSchedule(); - startTimeMs = endTimeMs; - endTimeMs = startTimeMs + TimeUnit.SECONDS.toMillis(1); - mScheduler.handleAddSchedule( - RecordingTestUtils.createTestRecordingWithIdAndPriorityAndPeriod(++id, CHANNEL_ID, - HIGH_PRIORITY, startTimeMs, endTimeMs)); - mScheduler.handleBuildSchedule(); - verify(mRecordingTasks.get(0), timeout((int) LISTENER_TIMEOUT_MS).times(1)).start(); - // The first schedule should not be stopped because the second one should wait for the end - // of the first schedule. - verify(mRecordingTasks.get(0), after((int) LISTENER_TIMEOUT_MS).never()).stop(); - } - - public void testAddSchedule_consecutiveNoFail() throws Exception { - long startTimeMs = mFakeClock.currentTimeMillis(); - long endTimeMs = startTimeMs + TimeUnit.SECONDS.toMillis(1); - long id = 0; - when(mDataManager.getScheduledRecording(anyLong())).thenReturn(ScheduledRecording - .builder(INPUT_ID, CHANNEL_ID, 0L, 0L).build()); - mScheduler.handleAddSchedule( - RecordingTestUtils.createTestRecordingWithIdAndPriorityAndPeriod(++id, CHANNEL_ID, - HIGH_PRIORITY, startTimeMs, endTimeMs)); - mScheduler.handleBuildSchedule(); - startTimeMs = endTimeMs; - endTimeMs = startTimeMs + TimeUnit.SECONDS.toMillis(1); - mScheduler.handleAddSchedule( - RecordingTestUtils.createTestRecordingWithIdAndPriorityAndPeriod(++id, CHANNEL_ID, - LOW_PRIORITY, startTimeMs, endTimeMs)); - mScheduler.handleBuildSchedule(); - verify(mRecordingTasks.get(0), timeout((int) LISTENER_TIMEOUT_MS).times(1)).start(); - verify(mRecordingTasks.get(0), after((int) LISTENER_TIMEOUT_MS).never()).stop(); - // The second schedule should not fail because it can starts after the first one finishes. - verify(mDataManager, after((int) LISTENER_TIMEOUT_MS).never()) - .changeState(any(ScheduledRecording.class), - eq(ScheduledRecording.STATE_RECORDING_FAILED)); - } - - public void testAddSchedule_consecutiveUseLessSession() throws Exception { - TvInputInfo input = createTvInputInfo(TUNER_COUNT_TWO); - mScheduler.updateTvInputInfo(input); - long startTimeMs = mFakeClock.currentTimeMillis(); - long endTimeMs = startTimeMs + TimeUnit.SECONDS.toMillis(1); - long id = 0; - mScheduler.handleAddSchedule( - RecordingTestUtils.createTestRecordingWithIdAndPriorityAndPeriod(++id, CHANNEL_ID, - LOW_PRIORITY, startTimeMs, endTimeMs)); - mScheduler.handleBuildSchedule(); - startTimeMs = endTimeMs; - endTimeMs = startTimeMs + TimeUnit.SECONDS.toMillis(1); - mScheduler.handleAddSchedule( - RecordingTestUtils.createTestRecordingWithIdAndPriorityAndPeriod(++id, CHANNEL_ID, - HIGH_PRIORITY, startTimeMs, endTimeMs)); - mScheduler.handleBuildSchedule(); - verify(mRecordingTasks.get(0), timeout((int) LISTENER_TIMEOUT_MS).times(1)).start(); - verify(mRecordingTasks.get(0), after((int) LISTENER_TIMEOUT_MS).never()).stop(); - // The second schedule should wait until the first one finishes rather than creating a new - // session even though there are available tuners. - assertTrue(mRecordingTasks.size() == 1); - } - - public void testUpdateSchedule_noCancel() throws Exception { - ScheduledRecording r = RecordingTestUtils.createTestRecordingWithPeriod(INPUT_ID, - CHANNEL_ID, mFakeClock.currentTimeMillis(), - mFakeClock.currentTimeMillis() + TimeUnit.HOURS.toMillis(1)); - mScheduler.handleAddSchedule(r); - mScheduler.handleBuildSchedule(); - mScheduler.handleUpdateSchedule(r); - verify(mRecordingTasks.get(0), after((int) LISTENER_TIMEOUT_MS).never()).cancel(); - } - - public void testUpdateSchedule_cancel() throws Exception { - ScheduledRecording r = RecordingTestUtils.createTestRecordingWithPeriod(INPUT_ID, - CHANNEL_ID, mFakeClock.currentTimeMillis(), - mFakeClock.currentTimeMillis() + TimeUnit.HOURS.toMillis(2)); - mScheduler.handleAddSchedule(r); - mScheduler.handleBuildSchedule(); - mScheduler.handleUpdateSchedule(ScheduledRecording.buildFrom(r) - .setStartTimeMs(mFakeClock.currentTimeMillis() + TimeUnit.HOURS.toMillis(1)) - .build()); - verify(mRecordingTasks.get(0), timeout((int) LISTENER_TIMEOUT_MS).times(1)).cancel(); - } - - private TvInputInfo createTvInputInfo(int tunerCount) throws Exception { - return TestUtils.createTvInputInfo(null, null, null, 0, false, true, tunerCount); - } -} diff --git a/tests/unit/src/com/android/tv/dvr/RecordingTaskTest.java b/tests/unit/src/com/android/tv/dvr/RecordingTaskTest.java deleted file mode 100644 index 7404a554..00000000 --- a/tests/unit/src/com/android/tv/dvr/RecordingTaskTest.java +++ /dev/null @@ -1,166 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.tv.dvr; - -import static org.mockito.Matchers.anyLong; -import static org.mockito.Matchers.anyObject; -import static org.mockito.Matchers.anyString; -import static org.mockito.Matchers.argThat; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.when; -import static org.mockito.hamcrest.MockitoHamcrest.longThat; - -import android.os.Build; -import android.os.Handler; -import android.os.Looper; -import android.os.Message; -import android.support.test.filters.SdkSuppress; -import android.support.test.filters.SmallTest; -import android.test.AndroidTestCase; - -import com.android.tv.InputSessionManager; -import com.android.tv.InputSessionManager.RecordingSession; -import com.android.tv.data.Channel; -import com.android.tv.dvr.RecordingTask.State; -import com.android.tv.testing.FakeClock; -import com.android.tv.testing.dvr.RecordingTestUtils; - -import org.hamcrest.BaseMatcher; -import org.hamcrest.Description; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.mockito.compat.ArgumentMatcher; - -import java.util.concurrent.TimeUnit; - -/** - * Tests for {@link RecordingTask}. - */ -@SmallTest -@SdkSuppress(minSdkVersion = Build.VERSION_CODES.N) -public class RecordingTaskTest extends AndroidTestCase { - private static final long DURATION = TimeUnit.MINUTES.toMillis(30); - private static final long START_OFFSET_MS = Scheduler.MS_TO_WAKE_BEFORE_START; - private static final String INPUT_ID = "input_id"; - private static final int CHANNEL_ID = 273; - - private FakeClock mFakeClock; - private DvrDataManagerInMemoryImpl mDataManager; - @Mock Handler mMockHandler; - @Mock DvrManager mDvrManager; - @Mock InputSessionManager mMockSessionManager; - @Mock RecordingSession mMockRecordingSession; - - @Override - protected void setUp() throws Exception { - super.setUp(); - if (Looper.myLooper() == null) { - Looper.prepare(); - } - MockitoAnnotations.initMocks(this); - mFakeClock = FakeClock.createWithCurrentTime(); - mDataManager = new DvrDataManagerInMemoryImpl(getContext(), mFakeClock); - } - - public void testHandle_init() { - Channel channel = createTestChannel(); - ScheduledRecording r = createRecording(channel); - RecordingTask task = createRecordingTask(r, channel); - String inputId = channel.getInputId(); - when(mMockSessionManager.createRecordingSession(eq(inputId), anyString(), eq(task), - eq(mMockHandler), anyLong())).thenReturn(mMockRecordingSession); - when(mMockHandler.sendMessageAtTime(anyObject(), anyLong())).thenReturn(true); - assertTrue(task.handleMessage(createMessage(RecordingTask.MSG_INITIALIZE))); - assertEquals(State.CONNECTION_PENDING, task.getState()); - verify(mMockSessionManager).createRecordingSession(eq(inputId), anyString(), eq(task), - eq(mMockHandler), anyLong()); - verify(mMockRecordingSession).tune(eq(inputId), eq(channel.getUri())); - verifyNoMoreInteractions(mMockHandler, mMockRecordingSession, mMockSessionManager); - } - - private static Channel createTestChannel() { - return new Channel.Builder().setInputId(INPUT_ID).setId(CHANNEL_ID) - .setDisplayName("Test Ch " + CHANNEL_ID).build(); - } - - public void testOnConnected() { - Channel channel = createTestChannel(); - ScheduledRecording r = createRecording(channel); - mDataManager.addScheduledRecording(r); - RecordingTask task = createRecordingTask(r, channel); - String inputId = channel.getInputId(); - when(mMockSessionManager.createRecordingSession(eq(inputId), anyString(), eq(task), - eq(mMockHandler), anyLong())).thenReturn(mMockRecordingSession); - when(mMockHandler.sendMessageAtTime(anyObject(), anyLong())).thenReturn(true); - task.handleMessage(createMessage(RecordingTask.MSG_INITIALIZE)); - task.onTuned(channel.getUri()); - assertEquals(State.CONNECTED, task.getState()); - } - - private ScheduledRecording createRecording(Channel c) { - long startTime = mFakeClock.currentTimeMillis() + START_OFFSET_MS; - long endTime = startTime + DURATION; - return RecordingTestUtils.createTestRecordingWithPeriod(c.getInputId(), c.getId(), - startTime, endTime); - } - - private RecordingTask createRecordingTask(ScheduledRecording r, Channel channel) { - RecordingTask recordingTask = new RecordingTask(getContext(), r, channel, mDvrManager, - mMockSessionManager, mDataManager, mFakeClock); - recordingTask.setHandler(mMockHandler); - return recordingTask; - } - - private void verifySendMessageAt(int what, long when) { - verify(mMockHandler).sendMessageAtTime(argThat(messageMatchesWhat(what)), delta(when, 100)); - } - - private static long delta(final long value, final long delta) { - return longThat(new BaseMatcher() { - @Override - public boolean matches(Object item) { - Long other = (Long) item; - return other >= value - delta && other <= value + delta; - } - - @Override - public void describeTo(Description description) { - description.appendText("eq " + value + "±" + delta); - - } - }); - } - - private Message createMessage(int what) { - Message msg = new Message(); - msg.setTarget(mMockHandler); - msg.what = what; - return msg; - } - - private static ArgumentMatcher messageMatchesWhat(final int what) { - return new ArgumentMatcher() { - @Override - public boolean matchesObject(Object argument) { - Message message = (Message) argument; - return message.what == what; - } - }; - } -} diff --git a/tests/unit/src/com/android/tv/dvr/ScheduledProgramReaperTest.java b/tests/unit/src/com/android/tv/dvr/ScheduledProgramReaperTest.java deleted file mode 100644 index 847540c2..00000000 --- a/tests/unit/src/com/android/tv/dvr/ScheduledProgramReaperTest.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.dvr; - -import android.test.MoreAsserts; - -import com.android.tv.testing.FakeClock; -import com.android.tv.testing.dvr.RecordingTestUtils; - -import junit.framework.TestCase; - -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import java.util.concurrent.TimeUnit; - -/** - * Tests for {@link ScheduledProgramReaper}. - */ -public class ScheduledProgramReaperTest extends TestCase { - private static final String INPUT_ID = "input_id"; - private static final int CHANNEL_ID = 273; - private static final long DURATION = TimeUnit.HOURS.toMillis(1); - - private ScheduledProgramReaper mReaper; - private FakeClock mFakeClock; - private DvrDataManagerInMemoryImpl mDvrDataManager; - @Mock private DvrManager mDvrManager; - - - @Override - protected void setUp() throws Exception { - super.setUp(); - MockitoAnnotations.initMocks(this); - mFakeClock = FakeClock.createWithTimeOne(); - mDvrDataManager = new DvrDataManagerInMemoryImpl(null, mFakeClock); - mReaper = new ScheduledProgramReaper(mDvrDataManager, mFakeClock); - } - - public void testRun_noRecordings() { - MoreAsserts.assertContentsInAnyOrder(mDvrDataManager.getAllScheduledRecordings()); - mReaper.run(); - MoreAsserts.assertContentsInAnyOrder(mDvrDataManager.getAllScheduledRecordings()); - } - - public void testRun_oneRecordingsTomorrow() { - ScheduledRecording recording = addNewScheduledRecordingForTomorrow(); - MoreAsserts - .assertContentsInAnyOrder(mDvrDataManager.getAllScheduledRecordings(), recording); - mReaper.run(); - MoreAsserts - .assertContentsInAnyOrder(mDvrDataManager.getAllScheduledRecordings(), recording); - } - - public void testRun_oneRecordingsStarted() { - ScheduledRecording recording = addNewScheduledRecordingForTomorrow(); - MoreAsserts - .assertContentsInAnyOrder(mDvrDataManager.getAllScheduledRecordings(), recording); - mFakeClock.increment(TimeUnit.DAYS); - mReaper.run(); - MoreAsserts - .assertContentsInAnyOrder(mDvrDataManager.getAllScheduledRecordings(), recording); - } - - public void testRun_oneRecordingsFinished() { - ScheduledRecording recording = addNewScheduledRecordingForTomorrow(); - MoreAsserts - .assertContentsInAnyOrder(mDvrDataManager.getAllScheduledRecordings(), recording); - mFakeClock.increment(TimeUnit.DAYS); - mFakeClock.increment(TimeUnit.MINUTES, 2); - mReaper.run(); - MoreAsserts - .assertContentsInAnyOrder(mDvrDataManager.getAllScheduledRecordings(), recording); - } - - public void testRun_oneRecordingsExpired() { - ScheduledRecording recording = addNewScheduledRecordingForTomorrow(); - MoreAsserts - .assertContentsInAnyOrder(mDvrDataManager.getAllScheduledRecordings(), recording); - mFakeClock.increment(TimeUnit.DAYS, 1 + ScheduledProgramReaper.DAYS); - mFakeClock.increment(TimeUnit.MILLISECONDS, DURATION); - // After the cutoff and enough so we can see on the clock - mFakeClock.increment(TimeUnit.SECONDS, 1); - - mReaper.run(); - MoreAsserts.assertContentsInAnyOrder( - "Recordings after reaper at " + com.android.tv.util.Utils - .toIsoDateTimeString(mFakeClock.currentTimeMillis()), - mDvrDataManager.getAllScheduledRecordings()); - } - - private ScheduledRecording addNewScheduledRecordingForTomorrow() { - long startTime = mFakeClock.currentTimeMillis() + TimeUnit.DAYS.toMillis(1); - ScheduledRecording recording = RecordingTestUtils.createTestRecordingWithPeriod(INPUT_ID, - CHANNEL_ID, startTime, startTime + DURATION); - return mDvrDataManager.addScheduledRecordingInternal( - ScheduledRecording.buildFrom(recording) - .setState(ScheduledRecording.STATE_RECORDING_FINISHED).build()); - } -} diff --git a/tests/unit/src/com/android/tv/dvr/ScheduledRecordingTest.java b/tests/unit/src/com/android/tv/dvr/ScheduledRecordingTest.java index 96036418..426e60ba 100644 --- a/tests/unit/src/com/android/tv/dvr/ScheduledRecordingTest.java +++ b/tests/unit/src/com/android/tv/dvr/ScheduledRecordingTest.java @@ -26,6 +26,7 @@ import android.util.Range; import com.android.tv.data.Channel; import com.android.tv.data.Program; +import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.testing.dvr.RecordingTestUtils; import junit.framework.TestCase; diff --git a/tests/unit/src/com/android/tv/dvr/SchedulerTest.java b/tests/unit/src/com/android/tv/dvr/SchedulerTest.java deleted file mode 100644 index 30ac1ff1..00000000 --- a/tests/unit/src/com/android/tv/dvr/SchedulerTest.java +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.tv.dvr; - -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyZeroInteractions; - -import android.app.AlarmManager; -import android.app.PendingIntent; -import android.os.Build; -import android.os.Looper; -import android.support.test.filters.SdkSuppress; -import android.support.test.filters.SmallTest; -import android.test.AndroidTestCase; - -import com.android.tv.InputSessionManager; -import com.android.tv.data.ChannelDataManager; -import com.android.tv.testing.FakeClock; -import com.android.tv.testing.dvr.RecordingTestUtils; -import com.android.tv.util.TvInputManagerHelper; - -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; - -import java.util.concurrent.TimeUnit; - -/** - * Tests for {@link Scheduler}. - */ -@SmallTest -@SdkSuppress(minSdkVersion = Build.VERSION_CODES.N) -public class SchedulerTest extends AndroidTestCase { - private static final String INPUT_ID = "input_id"; - private static final int CHANNEL_ID = 273; - - private FakeClock mFakeClock; - private DvrDataManagerInMemoryImpl mDataManager; - private Scheduler mScheduler; - @Mock DvrManager mDvrManager; - @Mock InputSessionManager mSessionManager; - @Mock AlarmManager mMockAlarmManager; - @Mock ChannelDataManager mChannelDataManager; - @Mock TvInputManagerHelper mInputManager; - - @Override - protected void setUp() throws Exception { - super.setUp(); - MockitoAnnotations.initMocks(this); - mFakeClock = FakeClock.createWithCurrentTime(); - mDataManager = new DvrDataManagerInMemoryImpl(getContext(), mFakeClock); - Mockito.when(mChannelDataManager.isDbLoadFinished()).thenReturn(true); - mScheduler = new Scheduler(Looper.myLooper(), mDvrManager, mSessionManager, mDataManager, - mChannelDataManager, mInputManager, getContext(), mFakeClock, mMockAlarmManager); - } - - public void testUpdate_none() throws Exception { - mScheduler.start(); - mScheduler.update(); - verifyZeroInteractions(mMockAlarmManager); - } - - public void testUpdate_nextIn12Hours() throws Exception { - long now = mFakeClock.currentTimeMillis(); - long startTime = now + TimeUnit.HOURS.toMillis(12); - ScheduledRecording r = RecordingTestUtils - .createTestRecordingWithPeriod(INPUT_ID, CHANNEL_ID, startTime, - startTime + TimeUnit.HOURS.toMillis(1)); - mDataManager.addScheduledRecording(r); - mScheduler.start(); - verify(mMockAlarmManager).set( - eq(AlarmManager.RTC_WAKEUP), - eq(startTime - Scheduler.MS_TO_WAKE_BEFORE_START), - any(PendingIntent.class)); - Mockito.reset(mMockAlarmManager); - mScheduler.update(); - verify(mMockAlarmManager).set( - eq(AlarmManager.RTC_WAKEUP), - eq(startTime - Scheduler.MS_TO_WAKE_BEFORE_START), - any(PendingIntent.class)); - } - - public void testStartsWithin() throws Exception { - long now = mFakeClock.currentTimeMillis(); - long startTime = now + 3; - ScheduledRecording r = RecordingTestUtils - .createTestRecordingWithPeriod(INPUT_ID, CHANNEL_ID, startTime, startTime + 100); - assertFalse(mScheduler.startsWithin(r, 2)); - assertTrue(mScheduler.startsWithin(r, 3)); - } -} \ No newline at end of file diff --git a/tests/unit/src/com/android/tv/dvr/SeriesRecordingSchedulerTest.java b/tests/unit/src/com/android/tv/dvr/SeriesRecordingSchedulerTest.java deleted file mode 100644 index efefb93c..00000000 --- a/tests/unit/src/com/android/tv/dvr/SeriesRecordingSchedulerTest.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.dvr; - -import android.os.Build; -import android.support.test.filters.SdkSuppress; -import android.support.test.filters.SmallTest; -import android.test.AndroidTestCase; -import android.test.MoreAsserts; -import android.util.LongSparseArray; - -import com.android.tv.data.Program; -import com.android.tv.testing.FakeClock; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -/** - * Tests for {@link SeriesRecordingScheduler} - */ -@SmallTest -@SdkSuppress(minSdkVersion = Build.VERSION_CODES.N) -public class SeriesRecordingSchedulerTest extends AndroidTestCase { - private static final String PROGRAM_TITLE = "MyProgram"; - private static final long CHANNEL_ID = 123; - private static final long SERIES_RECORDING_ID1 = 1; - private static final String SERIES_ID = "SERIES_ID"; - private static final String SEASON_NUMBER1 = "SEASON NUMBER1"; - private static final String SEASON_NUMBER2 = "SEASON NUMBER2"; - private static final String EPISODE_NUMBER1 = "EPISODE NUMBER1"; - private static final String EPISODE_NUMBER2 = "EPISODE NUMBER2"; - - private final SeriesRecording mBaseSeriesRecording = new SeriesRecording.Builder() - .setTitle(PROGRAM_TITLE).setChannelId(CHANNEL_ID).setSeriesId(SERIES_ID).build(); - private final Program mBaseProgram = new Program.Builder().setTitle(PROGRAM_TITLE) - .setChannelId(CHANNEL_ID).setSeriesId(SERIES_ID).build(); - - private DvrDataManagerInMemoryImpl mDataManager; - - @Override - protected void setUp() throws Exception { - super.setUp(); - FakeClock fakeClock = FakeClock.createWithCurrentTime(); - mDataManager = new DvrDataManagerInMemoryImpl(getContext(), fakeClock); - } - - public void testPickOneProgramPerEpisode_onePerEpisode() { - SeriesRecording seriesRecording = SeriesRecording.buildFrom(mBaseSeriesRecording) - .setId(SERIES_RECORDING_ID1).build(); - mDataManager.addSeriesRecording(seriesRecording); - List programs = new ArrayList<>(); - Program program1 = new Program.Builder(mBaseProgram).setSeasonNumber(SEASON_NUMBER1) - .setEpisodeNumber(EPISODE_NUMBER1).build(); - programs.add(program1); - Program program2 = new Program.Builder(mBaseProgram).setSeasonNumber(SEASON_NUMBER2) - .setEpisodeNumber(EPISODE_NUMBER2).build(); - programs.add(program2); - LongSparseArray> result = SeriesRecordingScheduler.pickOneProgramPerEpisode( - mDataManager, Collections.singletonList(seriesRecording), programs); - MoreAsserts.assertContentsInAnyOrder(result.get(SERIES_RECORDING_ID1), program1, program2); - } - - public void testPickOneProgramPerEpisode_manyPerEpisode() { - SeriesRecording seriesRecording = SeriesRecording.buildFrom(mBaseSeriesRecording) - .setId(SERIES_RECORDING_ID1).build(); - mDataManager.addSeriesRecording(seriesRecording); - List programs = new ArrayList<>(); - Program program1 = new Program.Builder(mBaseProgram).setSeasonNumber(SEASON_NUMBER1) - .setEpisodeNumber(EPISODE_NUMBER1).setStartTimeUtcMillis(0).build(); - programs.add(program1); - Program program2 = new Program.Builder(program1).setStartTimeUtcMillis(1).build(); - programs.add(program2); - Program program3 = new Program.Builder(mBaseProgram).setSeasonNumber(SEASON_NUMBER2) - .setEpisodeNumber(EPISODE_NUMBER2).build(); - programs.add(program3); - Program program4 = new Program.Builder(program1).setStartTimeUtcMillis(1).build(); - programs.add(program4); - LongSparseArray> result = SeriesRecordingScheduler.pickOneProgramPerEpisode( - mDataManager, Collections.singletonList(seriesRecording), programs); - MoreAsserts.assertContentsInAnyOrder(result.get(SERIES_RECORDING_ID1), program1, program3); - } - - public void testPickOneProgramPerEpisode_nullEpisode() { - SeriesRecording seriesRecording = SeriesRecording.buildFrom(mBaseSeriesRecording) - .setId(SERIES_RECORDING_ID1).build(); - mDataManager.addSeriesRecording(seriesRecording); - List programs = new ArrayList<>(); - Program program1 = new Program.Builder(mBaseProgram).setStartTimeUtcMillis(0).build(); - programs.add(program1); - Program program2 = new Program.Builder(mBaseProgram).setStartTimeUtcMillis(1).build(); - programs.add(program2); - LongSparseArray> result = SeriesRecordingScheduler.pickOneProgramPerEpisode( - mDataManager, Collections.singletonList(seriesRecording), programs); - MoreAsserts.assertContentsInAnyOrder(result.get(SERIES_RECORDING_ID1), program1, program2); - } -} diff --git a/tests/unit/src/com/android/tv/dvr/SeriesRecordingTest.java b/tests/unit/src/com/android/tv/dvr/SeriesRecordingTest.java deleted file mode 100644 index c48fec02..00000000 --- a/tests/unit/src/com/android/tv/dvr/SeriesRecordingTest.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.tv.dvr; - -import android.os.Build; -import android.os.Parcel; -import android.support.test.filters.SdkSuppress; -import android.support.test.filters.SmallTest; - -import com.android.tv.data.Program; - -import junit.framework.TestCase; - -/** - * Tests for {@link SeriesRecording}. - */ -@SmallTest -@SdkSuppress(minSdkVersion = Build.VERSION_CODES.N) -public class SeriesRecordingTest extends TestCase { - private static final String PROGRAM_TITLE = "MyProgram"; - private static final long CHANNEL_ID = 123; - private static final long OTHER_CHANNEL_ID = 321; - private static final String SERIES_ID = "SERIES_ID"; - private static final String OTHER_SERIES_ID = "OTHER_SERIES_ID"; - - private final SeriesRecording mBaseSeriesRecording = new SeriesRecording.Builder() - .setTitle(PROGRAM_TITLE).setChannelId(CHANNEL_ID).setSeriesId(SERIES_ID).build(); - private final SeriesRecording mSeriesRecordingSeason2 = SeriesRecording - .buildFrom(mBaseSeriesRecording).setStartFromSeason(2).build(); - private final SeriesRecording mSeriesRecordingSeason2Episode5 = SeriesRecording - .buildFrom(mSeriesRecordingSeason2).setStartFromEpisode(5).build(); - private final Program mBaseProgram = new Program.Builder().setTitle(PROGRAM_TITLE) - .setChannelId(CHANNEL_ID).setSeriesId(SERIES_ID).build(); - - public void testParcelable() throws Exception { - SeriesRecording r1 = new SeriesRecording.Builder() - .setId(1) - .setChannelId(2) - .setPriority(3) - .setTitle("4") - .setDescription("5") - .setLongDescription("5-long") - .setSeriesId("6") - .setStartFromEpisode(7) - .setStartFromSeason(8) - .setChannelOption(SeriesRecording.OPTION_CHANNEL_ALL) - .setCanonicalGenreIds(new int[] {9, 10}) - .setPosterUri("11") - .setPhotoUri("12") - .build(); - Parcel p1 = Parcel.obtain(); - Parcel p2 = Parcel.obtain(); - try { - r1.writeToParcel(p1, 0); - byte[] bytes = p1.marshall(); - p2.unmarshall(bytes, 0, bytes.length); - p2.setDataPosition(0); - SeriesRecording r2 = SeriesRecording.fromParcel(p2); - assertEquals(r1, r2); - } finally { - p1.recycle(); - p2.recycle(); - } - } - - public void testDoesProgramMatch_simpleMatch() { - assertDoesProgramMatch(mBaseProgram, mBaseSeriesRecording, true); - } - - public void testDoesProgramMatch_differentSeriesId() { - Program program = new Program.Builder(mBaseProgram).setSeriesId(OTHER_SERIES_ID).build(); - assertDoesProgramMatch(program, mBaseSeriesRecording, false); - } - - public void testDoesProgramMatch_differentChannel() { - Program program = new Program.Builder(mBaseProgram).setChannelId(OTHER_CHANNEL_ID).build(); - assertDoesProgramMatch(program, mBaseSeriesRecording, false); - } - - public void testDoesProgramMatch_startFromSeason2() { - Program program = mBaseProgram; - assertDoesProgramMatch(program, mSeriesRecordingSeason2, true); - program = new Program.Builder(program).setSeasonNumber("1").build(); - assertDoesProgramMatch(program, mSeriesRecordingSeason2, false); - program = new Program.Builder(program).setSeasonNumber("2").build(); - assertDoesProgramMatch(program, mSeriesRecordingSeason2, true); - program = new Program.Builder(program).setSeasonNumber("3").build(); - assertDoesProgramMatch(program, mSeriesRecordingSeason2, true); - } - - public void testDoesProgramMatch_startFromSeason2episode5() { - Program program = mBaseProgram; - assertDoesProgramMatch(program, mSeriesRecordingSeason2Episode5, true); - program = new Program.Builder(program).setSeasonNumber("2").build(); - assertDoesProgramMatch(program, mSeriesRecordingSeason2Episode5, true); - program = new Program.Builder(program).setEpisodeNumber("4").build(); - assertDoesProgramMatch(program, mSeriesRecordingSeason2Episode5, false); - program = new Program.Builder(program).setEpisodeNumber("5").build(); - assertDoesProgramMatch(program, mSeriesRecordingSeason2Episode5, true); - program = new Program.Builder(program).setEpisodeNumber("6").build(); - assertDoesProgramMatch(program, mSeriesRecordingSeason2Episode5, true); - program = new Program.Builder(program).setSeasonNumber("3").setEpisodeNumber("1").build(); - assertDoesProgramMatch(program, mSeriesRecordingSeason2Episode5, true); - } - - private void assertDoesProgramMatch(Program p, SeriesRecording seriesRecording, - boolean expected) { - assertEquals(seriesRecording + " doesProgramMatch " + p, expected, - seriesRecording.matchProgram(p)); - } -} diff --git a/tests/unit/src/com/android/tv/dvr/data/SeriesRecordingTest.java b/tests/unit/src/com/android/tv/dvr/data/SeriesRecordingTest.java new file mode 100644 index 00000000..7512ed0e --- /dev/null +++ b/tests/unit/src/com/android/tv/dvr/data/SeriesRecordingTest.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tv.dvr.data; + +import android.os.Build; +import android.os.Parcel; +import android.support.test.filters.SdkSuppress; +import android.support.test.filters.SmallTest; + +import com.android.tv.data.Program; + +import junit.framework.TestCase; + +/** + * Tests for {@link SeriesRecording}. + */ +@SmallTest +@SdkSuppress(minSdkVersion = Build.VERSION_CODES.N) +public class SeriesRecordingTest extends TestCase { + private static final String PROGRAM_TITLE = "MyProgram"; + private static final long CHANNEL_ID = 123; + private static final long OTHER_CHANNEL_ID = 321; + private static final String SERIES_ID = "SERIES_ID"; + private static final String OTHER_SERIES_ID = "OTHER_SERIES_ID"; + + private final SeriesRecording mBaseSeriesRecording = new SeriesRecording.Builder() + .setTitle(PROGRAM_TITLE).setChannelId(CHANNEL_ID).setSeriesId(SERIES_ID).build(); + private final SeriesRecording mSeriesRecordingSeason2 = SeriesRecording + .buildFrom(mBaseSeriesRecording).setStartFromSeason(2).build(); + private final SeriesRecording mSeriesRecordingSeason2Episode5 = SeriesRecording + .buildFrom(mSeriesRecordingSeason2).setStartFromEpisode(5).build(); + private final Program mBaseProgram = new Program.Builder().setTitle(PROGRAM_TITLE) + .setChannelId(CHANNEL_ID).setSeriesId(SERIES_ID).build(); + + public void testParcelable() throws Exception { + SeriesRecording r1 = new SeriesRecording.Builder() + .setId(1) + .setChannelId(2) + .setPriority(3) + .setTitle("4") + .setDescription("5") + .setLongDescription("5-long") + .setSeriesId("6") + .setStartFromEpisode(7) + .setStartFromSeason(8) + .setChannelOption(SeriesRecording.OPTION_CHANNEL_ALL) + .setCanonicalGenreIds(new int[] {9, 10}) + .setPosterUri("11") + .setPhotoUri("12") + .build(); + Parcel p1 = Parcel.obtain(); + Parcel p2 = Parcel.obtain(); + try { + r1.writeToParcel(p1, 0); + byte[] bytes = p1.marshall(); + p2.unmarshall(bytes, 0, bytes.length); + p2.setDataPosition(0); + SeriesRecording r2 = SeriesRecording.fromParcel(p2); + assertEquals(r1, r2); + } finally { + p1.recycle(); + p2.recycle(); + } + } + + public void testDoesProgramMatch_simpleMatch() { + assertDoesProgramMatch(mBaseProgram, mBaseSeriesRecording, true); + } + + public void testDoesProgramMatch_differentSeriesId() { + Program program = new Program.Builder(mBaseProgram).setSeriesId(OTHER_SERIES_ID).build(); + assertDoesProgramMatch(program, mBaseSeriesRecording, false); + } + + public void testDoesProgramMatch_differentChannel() { + Program program = new Program.Builder(mBaseProgram).setChannelId(OTHER_CHANNEL_ID).build(); + assertDoesProgramMatch(program, mBaseSeriesRecording, false); + } + + public void testDoesProgramMatch_startFromSeason2() { + Program program = mBaseProgram; + assertDoesProgramMatch(program, mSeriesRecordingSeason2, true); + program = new Program.Builder(program).setSeasonNumber("1").build(); + assertDoesProgramMatch(program, mSeriesRecordingSeason2, false); + program = new Program.Builder(program).setSeasonNumber("2").build(); + assertDoesProgramMatch(program, mSeriesRecordingSeason2, true); + program = new Program.Builder(program).setSeasonNumber("3").build(); + assertDoesProgramMatch(program, mSeriesRecordingSeason2, true); + } + + public void testDoesProgramMatch_startFromSeason2episode5() { + Program program = mBaseProgram; + assertDoesProgramMatch(program, mSeriesRecordingSeason2Episode5, true); + program = new Program.Builder(program).setSeasonNumber("2").build(); + assertDoesProgramMatch(program, mSeriesRecordingSeason2Episode5, true); + program = new Program.Builder(program).setEpisodeNumber("4").build(); + assertDoesProgramMatch(program, mSeriesRecordingSeason2Episode5, false); + program = new Program.Builder(program).setEpisodeNumber("5").build(); + assertDoesProgramMatch(program, mSeriesRecordingSeason2Episode5, true); + program = new Program.Builder(program).setEpisodeNumber("6").build(); + assertDoesProgramMatch(program, mSeriesRecordingSeason2Episode5, true); + program = new Program.Builder(program).setSeasonNumber("3").setEpisodeNumber("1").build(); + assertDoesProgramMatch(program, mSeriesRecordingSeason2Episode5, true); + } + + private void assertDoesProgramMatch(Program p, SeriesRecording seriesRecording, + boolean expected) { + assertEquals(seriesRecording + " doesProgramMatch " + p, expected, + seriesRecording.matchProgram(p)); + } +} diff --git a/tests/unit/src/com/android/tv/dvr/provider/DvrDbSyncTest.java b/tests/unit/src/com/android/tv/dvr/provider/DvrDbSyncTest.java new file mode 100644 index 00000000..9e96a7b8 --- /dev/null +++ b/tests/unit/src/com/android/tv/dvr/provider/DvrDbSyncTest.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.dvr.provider; + +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.anyObject; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.os.Build; +import android.support.test.filters.SdkSuppress; +import android.support.test.filters.SmallTest; +import android.test.AndroidTestCase; + +import com.android.tv.data.ChannelDataManager; +import com.android.tv.data.Program; +import com.android.tv.dvr.DvrDataManagerImpl; +import com.android.tv.dvr.DvrManager; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.data.SeriesRecording; +import com.android.tv.dvr.recorder.SeriesRecordingScheduler; + +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Tests for {@link DvrScheduleManager} + */ +@SmallTest +@SdkSuppress(minSdkVersion = Build.VERSION_CODES.N) +public class DvrDbSyncTest extends AndroidTestCase { + private static final String INPUT_ID = "input_id"; + private static final long BASE_PROGRAM_ID = 1; + private static final long BASE_START_TIME_MS = 0; + private static final long BASE_END_TIME_MS = 1; + private static final String BASE_SEASON_NUMBER = "2"; + private static final String BASE_EPISODE_NUMBER = "3"; + private static final Program BASE_PROGRAM = new Program.Builder().setId(BASE_PROGRAM_ID) + .setStartTimeUtcMillis(BASE_START_TIME_MS).setEndTimeUtcMillis(BASE_END_TIME_MS) + .build(); + private static final Program BASE_SERIES_PROGRAM = new Program.Builder().setId(BASE_PROGRAM_ID) + .setStartTimeUtcMillis(BASE_START_TIME_MS).setEndTimeUtcMillis(BASE_END_TIME_MS) + .setSeasonNumber(BASE_SEASON_NUMBER).setEpisodeNumber(BASE_EPISODE_NUMBER).build(); + private static final ScheduledRecording BASE_SCHEDULE = + ScheduledRecording.builder(INPUT_ID, BASE_PROGRAM).build(); + private static final ScheduledRecording BASE_SERIES_SCHEDULE = + ScheduledRecording.builder(INPUT_ID, BASE_SERIES_PROGRAM).build(); + + private DvrDbSync mDbSync; + @Mock private DvrManager mDvrManager; + @Mock private DvrDataManagerImpl mDataManager; + @Mock private ChannelDataManager mChannelDataManager; + @Mock private SeriesRecordingScheduler mSeriesRecordingScheduler; + + @Override + protected void setUp() throws Exception { + super.setUp(); + MockitoAnnotations.initMocks(this); + when(mChannelDataManager.isDbLoadFinished()).thenReturn(true); + when(mDvrManager.addSeriesRecording(anyObject(), anyObject(), anyInt())) + .thenReturn(SeriesRecording.builder(INPUT_ID, BASE_PROGRAM).build()); + mDbSync = new DvrDbSync(getContext(), mDataManager, mChannelDataManager, + mDvrManager, mSeriesRecordingScheduler); + } + + public void testHandleUpdateProgram_null() { + addSchedule(BASE_PROGRAM_ID, BASE_SCHEDULE); + mDbSync.handleUpdateProgram(null, BASE_PROGRAM_ID); + verify(mDataManager).removeScheduledRecording(BASE_SCHEDULE); + } + + public void testHandleUpdateProgram_changeTimeNotStarted() { + addSchedule(BASE_PROGRAM_ID, BASE_SCHEDULE); + long startTimeMs = BASE_START_TIME_MS + 1; + long endTimeMs = BASE_END_TIME_MS + 1; + Program program = new Program.Builder(BASE_PROGRAM).setStartTimeUtcMillis(startTimeMs) + .setEndTimeUtcMillis(endTimeMs).build(); + mDbSync.handleUpdateProgram(program, BASE_PROGRAM_ID); + assertUpdateScheduleCalled(program); + } + + public void testHandleUpdateProgram_changeTimeInProgressNotCalled() { + addSchedule(BASE_PROGRAM_ID, ScheduledRecording.buildFrom(BASE_SCHEDULE) + .setState(ScheduledRecording.STATE_RECORDING_IN_PROGRESS).build()); + long startTimeMs = BASE_START_TIME_MS + 1; + Program program = new Program.Builder(BASE_PROGRAM).setStartTimeUtcMillis(startTimeMs) + .build(); + mDbSync.handleUpdateProgram(program, BASE_PROGRAM_ID); + verify(mDataManager, never()).updateScheduledRecording(anyObject()); + } + + public void testHandleUpdateProgram_changeSeason() { + addSchedule(BASE_PROGRAM_ID, BASE_SERIES_SCHEDULE); + String seasonNumber = BASE_SEASON_NUMBER + "1"; + String episodeNumber = BASE_EPISODE_NUMBER + "1"; + Program program = new Program.Builder(BASE_SERIES_PROGRAM).setSeasonNumber(seasonNumber) + .setEpisodeNumber(episodeNumber).build(); + mDbSync.handleUpdateProgram(program, BASE_PROGRAM_ID); + assertUpdateScheduleCalled(program); + } + + public void testHandleUpdateProgram_finished() { + addSchedule(BASE_PROGRAM_ID, ScheduledRecording.buildFrom(BASE_SERIES_SCHEDULE) + .setState(ScheduledRecording.STATE_RECORDING_FINISHED).build()); + String seasonNumber = BASE_SEASON_NUMBER + "1"; + String episodeNumber = BASE_EPISODE_NUMBER + "1"; + Program program = new Program.Builder(BASE_SERIES_PROGRAM).setSeasonNumber(seasonNumber) + .setEpisodeNumber(episodeNumber).build(); + mDbSync.handleUpdateProgram(program, BASE_PROGRAM_ID); + verify(mDataManager, never()).updateScheduledRecording(anyObject()); + } + + private void addSchedule(long programId, ScheduledRecording schedule) { + when(mDataManager.getScheduledRecordingForProgramId(programId)).thenReturn(schedule); + } + + private void assertUpdateScheduleCalled(Program program) { + verify(mDataManager).updateScheduledRecording( + eq(ScheduledRecording.builder(INPUT_ID, program).build())); + } +} diff --git a/tests/unit/src/com/android/tv/dvr/provider/EpisodicProgramLoadTaskTest.java b/tests/unit/src/com/android/tv/dvr/provider/EpisodicProgramLoadTaskTest.java new file mode 100644 index 00000000..301c453d --- /dev/null +++ b/tests/unit/src/com/android/tv/dvr/provider/EpisodicProgramLoadTaskTest.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.dvr.provider; + +import android.os.Build; +import android.support.test.filters.SdkSuppress; +import android.support.test.filters.SmallTest; +import android.test.AndroidTestCase; + +import com.android.tv.dvr.data.SeasonEpisodeNumber; + +import java.util.ArrayList; +import java.util.List; + +/** + * Tests for {@link EpisodicProgramLoadTask} + */ +@SmallTest +@SdkSuppress(minSdkVersion = Build.VERSION_CODES.N) +public class EpisodicProgramLoadTaskTest extends AndroidTestCase { + private static final long SERIES_RECORDING_ID1 = 1; + private static final long SERIES_RECORDING_ID2 = 2; + private static final String SEASON_NUMBER1 = "SEASON NUMBER1"; + private static final String SEASON_NUMBER2 = "SEASON NUMBER2"; + private static final String EPISODE_NUMBER1 = "EPISODE NUMBER1"; + private static final String EPISODE_NUMBER2 = "EPISODE NUMBER2"; + + public void testEpisodeAlreadyScheduled_true() { + List seasonEpisodeNumbers = new ArrayList<>(); + SeasonEpisodeNumber seasonEpisodeNumber = new SeasonEpisodeNumber( + SERIES_RECORDING_ID1, SEASON_NUMBER1, EPISODE_NUMBER1); + seasonEpisodeNumbers.add(seasonEpisodeNumber); + assertTrue(seasonEpisodeNumbers.contains( + new SeasonEpisodeNumber(SERIES_RECORDING_ID1, SEASON_NUMBER1, EPISODE_NUMBER1))); + } + + public void testEpisodeAlreadyScheduled_false() { + List seasonEpisodeNumbers = new ArrayList<>(); + SeasonEpisodeNumber seasonEpisodeNumber = new SeasonEpisodeNumber( + SERIES_RECORDING_ID1, SEASON_NUMBER1, EPISODE_NUMBER1); + seasonEpisodeNumbers.add(seasonEpisodeNumber); + assertFalse(seasonEpisodeNumbers.contains( + new SeasonEpisodeNumber(SERIES_RECORDING_ID2, SEASON_NUMBER1, EPISODE_NUMBER1))); + assertFalse(seasonEpisodeNumbers.contains( + new SeasonEpisodeNumber(SERIES_RECORDING_ID1, SEASON_NUMBER2, EPISODE_NUMBER1))); + assertFalse(seasonEpisodeNumbers.contains( + new SeasonEpisodeNumber(SERIES_RECORDING_ID1, SEASON_NUMBER1, EPISODE_NUMBER2))); + } + + public void testEpisodeAlreadyScheduled_null() { + List seasonEpisodeNumbers = new ArrayList<>(); + SeasonEpisodeNumber seasonEpisodeNumber = new SeasonEpisodeNumber( + SERIES_RECORDING_ID1, SEASON_NUMBER1, EPISODE_NUMBER1); + seasonEpisodeNumbers.add(seasonEpisodeNumber); + assertFalse(seasonEpisodeNumbers.contains( + new SeasonEpisodeNumber(SERIES_RECORDING_ID1, null, EPISODE_NUMBER1))); + assertFalse(seasonEpisodeNumbers.contains( + new SeasonEpisodeNumber(SERIES_RECORDING_ID1, SEASON_NUMBER1, null))); + assertFalse(seasonEpisodeNumbers.contains( + new SeasonEpisodeNumber(SERIES_RECORDING_ID1, null, null))); + } +} \ No newline at end of file diff --git a/tests/unit/src/com/android/tv/dvr/recorder/DvrRecordingServiceTest.java b/tests/unit/src/com/android/tv/dvr/recorder/DvrRecordingServiceTest.java new file mode 100644 index 00000000..7ad8d55d --- /dev/null +++ b/tests/unit/src/com/android/tv/dvr/recorder/DvrRecordingServiceTest.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.dvr.recorder; + +import static org.mockito.Mockito.verify; + +import android.os.Build; +import android.support.test.filters.SdkSuppress; +import android.support.test.filters.SmallTest; +import android.test.ServiceTestCase; + +import com.android.tv.common.feature.CommonFeatures; +import com.android.tv.common.feature.TestableFeature; +import com.android.tv.testing.FakeClock; + +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +/** + * Tests for {@link DvrRecordingService}. + */ +@SmallTest +@SdkSuppress(minSdkVersion = Build.VERSION_CODES.N) +public class DvrRecordingServiceTest extends ServiceTestCase { + @Mock Scheduler mMockScheduler; + private final TestableFeature mDvrFeature = CommonFeatures.DVR; + private final FakeClock mFakeClock = FakeClock.createWithCurrentTime(); + + @Override + protected void setUp() throws Exception { + super.setUp(); + mDvrFeature.enableForTest(); + MockitoAnnotations.initMocks(this); + setupService(); + DvrRecordingService service = getService(); + service.setScheduler(mMockScheduler); + } + + @Override + protected void tearDown() throws Exception { + mDvrFeature.resetForTests(); + super.tearDown(); + } + + public DvrRecordingServiceTest() { + super(DvrRecordingService.class); + } + + public void testStartService_null() throws Exception { + startService(null); + verify(mMockScheduler, Mockito.only()).update(); + } +} \ No newline at end of file diff --git a/tests/unit/src/com/android/tv/dvr/recorder/ScheduledProgramReaperTest.java b/tests/unit/src/com/android/tv/dvr/recorder/ScheduledProgramReaperTest.java new file mode 100644 index 00000000..d434a34e --- /dev/null +++ b/tests/unit/src/com/android/tv/dvr/recorder/ScheduledProgramReaperTest.java @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.dvr.recorder; + +import android.os.Build; +import android.support.test.filters.SdkSuppress; +import android.support.test.filters.SmallTest; +import android.test.AndroidTestCase; +import android.test.MoreAsserts; + +import com.android.tv.dvr.DvrDataManagerInMemoryImpl; +import com.android.tv.dvr.DvrManager; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.testing.FakeClock; +import com.android.tv.testing.dvr.RecordingTestUtils; + +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.concurrent.TimeUnit; + +/** + * Tests for {@link ScheduledProgramReaper}. + */ +@SmallTest +@SdkSuppress(minSdkVersion = Build.VERSION_CODES.N) +public class ScheduledProgramReaperTest extends AndroidTestCase { + private static final String INPUT_ID = "input_id"; + private static final int CHANNEL_ID = 273; + private static final long DURATION = TimeUnit.HOURS.toMillis(1); + + private ScheduledProgramReaper mReaper; + private FakeClock mFakeClock; + private DvrDataManagerInMemoryImpl mDvrDataManager; + @Mock private DvrManager mDvrManager; + + @Override + protected void setUp() throws Exception { + super.setUp(); + MockitoAnnotations.initMocks(this); + mFakeClock = FakeClock.createWithTimeOne(); + mDvrDataManager = new DvrDataManagerInMemoryImpl(getContext(), mFakeClock); + mReaper = new ScheduledProgramReaper(mDvrDataManager, mFakeClock); + } + + public void testRun_noRecordings() { + MoreAsserts.assertContentsInAnyOrder(mDvrDataManager.getAllScheduledRecordings()); + mReaper.run(); + MoreAsserts.assertContentsInAnyOrder(mDvrDataManager.getAllScheduledRecordings()); + } + + public void testRun_oneRecordingsTomorrow() { + ScheduledRecording recording = addNewScheduledRecordingForTomorrow(); + MoreAsserts + .assertContentsInAnyOrder(mDvrDataManager.getAllScheduledRecordings(), recording); + mReaper.run(); + MoreAsserts + .assertContentsInAnyOrder(mDvrDataManager.getAllScheduledRecordings(), recording); + } + + public void testRun_oneRecordingsStarted() { + ScheduledRecording recording = addNewScheduledRecordingForTomorrow(); + MoreAsserts + .assertContentsInAnyOrder(mDvrDataManager.getAllScheduledRecordings(), recording); + mFakeClock.increment(TimeUnit.DAYS); + mReaper.run(); + MoreAsserts + .assertContentsInAnyOrder(mDvrDataManager.getAllScheduledRecordings(), recording); + } + + public void testRun_oneRecordingsFinished() { + ScheduledRecording recording = addNewScheduledRecordingForTomorrow(); + MoreAsserts + .assertContentsInAnyOrder(mDvrDataManager.getAllScheduledRecordings(), recording); + mFakeClock.increment(TimeUnit.DAYS); + mFakeClock.increment(TimeUnit.MINUTES, 2); + mReaper.run(); + MoreAsserts + .assertContentsInAnyOrder(mDvrDataManager.getAllScheduledRecordings(), recording); + } + + public void testRun_oneRecordingsExpired() { + ScheduledRecording recording = addNewScheduledRecordingForTomorrow(); + MoreAsserts + .assertContentsInAnyOrder(mDvrDataManager.getAllScheduledRecordings(), recording); + mFakeClock.increment(TimeUnit.DAYS, 1 + ScheduledProgramReaper.DAYS); + mFakeClock.increment(TimeUnit.MILLISECONDS, DURATION); + // After the cutoff and enough so we can see on the clock + mFakeClock.increment(TimeUnit.SECONDS, 1); + + mReaper.run(); + MoreAsserts.assertContentsInAnyOrder( + "Recordings after reaper at " + com.android.tv.util.Utils + .toIsoDateTimeString(mFakeClock.currentTimeMillis()), + mDvrDataManager.getAllScheduledRecordings()); + } + + private ScheduledRecording addNewScheduledRecordingForTomorrow() { + long startTime = mFakeClock.currentTimeMillis() + TimeUnit.DAYS.toMillis(1); + ScheduledRecording recording = RecordingTestUtils.createTestRecordingWithPeriod(INPUT_ID, + CHANNEL_ID, startTime, startTime + DURATION); + return mDvrDataManager.addScheduledRecordingInternal( + ScheduledRecording.buildFrom(recording) + .setState(ScheduledRecording.STATE_RECORDING_FINISHED).build()); + } +} diff --git a/tests/unit/src/com/android/tv/dvr/recorder/SchedulerTest.java b/tests/unit/src/com/android/tv/dvr/recorder/SchedulerTest.java new file mode 100644 index 00000000..94cfaac1 --- /dev/null +++ b/tests/unit/src/com/android/tv/dvr/recorder/SchedulerTest.java @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tv.dvr.recorder; + +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; + +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.os.Build; +import android.os.Looper; +import android.support.test.filters.SdkSuppress; +import android.support.test.filters.SmallTest; +import android.test.AndroidTestCase; + +import com.android.tv.InputSessionManager; +import com.android.tv.data.ChannelDataManager; +import com.android.tv.dvr.DvrDataManagerInMemoryImpl; +import com.android.tv.dvr.DvrManager; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.testing.FakeClock; +import com.android.tv.testing.dvr.RecordingTestUtils; +import com.android.tv.util.TvInputManagerHelper; + +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +import java.util.concurrent.TimeUnit; + +/** + * Tests for {@link Scheduler}. + */ +@SmallTest +@SdkSuppress(minSdkVersion = Build.VERSION_CODES.N) +public class SchedulerTest extends AndroidTestCase { + private static final String INPUT_ID = "input_id"; + private static final int CHANNEL_ID = 273; + + private FakeClock mFakeClock; + private DvrDataManagerInMemoryImpl mDataManager; + private Scheduler mScheduler; + @Mock DvrManager mDvrManager; + @Mock InputSessionManager mSessionManager; + @Mock AlarmManager mMockAlarmManager; + @Mock ChannelDataManager mChannelDataManager; + @Mock TvInputManagerHelper mInputManager; + + @Override + protected void setUp() throws Exception { + super.setUp(); + MockitoAnnotations.initMocks(this); + mFakeClock = FakeClock.createWithCurrentTime(); + mDataManager = new DvrDataManagerInMemoryImpl(getContext(), mFakeClock); + Mockito.when(mChannelDataManager.isDbLoadFinished()).thenReturn(true); + mScheduler = new Scheduler(Looper.myLooper(), mDvrManager, mSessionManager, mDataManager, + mChannelDataManager, mInputManager, getContext(), mFakeClock, mMockAlarmManager); + } + + public void testUpdate_none() throws Exception { + mScheduler.start(); + mScheduler.update(); + verifyZeroInteractions(mMockAlarmManager); + } + + public void testUpdate_nextIn12Hours() throws Exception { + long now = mFakeClock.currentTimeMillis(); + long startTime = now + TimeUnit.HOURS.toMillis(12); + ScheduledRecording r = RecordingTestUtils + .createTestRecordingWithPeriod(INPUT_ID, CHANNEL_ID, startTime, + startTime + TimeUnit.HOURS.toMillis(1)); + mDataManager.addScheduledRecording(r); + mScheduler.start(); + verify(mMockAlarmManager).setExactAndAllowWhileIdle( + eq(AlarmManager.RTC_WAKEUP), + eq(startTime - Scheduler.MS_TO_WAKE_BEFORE_START), + any(PendingIntent.class)); + Mockito.reset(mMockAlarmManager); + mScheduler.update(); + verify(mMockAlarmManager).setExactAndAllowWhileIdle( + eq(AlarmManager.RTC_WAKEUP), + eq(startTime - Scheduler.MS_TO_WAKE_BEFORE_START), + any(PendingIntent.class)); + } + + public void testStartsWithin() throws Exception { + long now = mFakeClock.currentTimeMillis(); + long startTime = now + 3; + ScheduledRecording r = RecordingTestUtils + .createTestRecordingWithPeriod(INPUT_ID, CHANNEL_ID, startTime, startTime + 100); + assertFalse(mScheduler.startsWithin(r, 2)); + assertTrue(mScheduler.startsWithin(r, 3)); + } +} \ No newline at end of file diff --git a/tests/unit/src/com/android/tv/dvr/recorder/SeriesRecordingSchedulerTest.java b/tests/unit/src/com/android/tv/dvr/recorder/SeriesRecordingSchedulerTest.java new file mode 100644 index 00000000..afb9c042 --- /dev/null +++ b/tests/unit/src/com/android/tv/dvr/recorder/SeriesRecordingSchedulerTest.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.dvr.recorder; + +import android.os.Build; +import android.support.test.filters.SdkSuppress; +import android.support.test.filters.SmallTest; +import android.test.AndroidTestCase; +import android.test.MoreAsserts; +import android.util.LongSparseArray; + +import com.android.tv.data.Program; +import com.android.tv.dvr.DvrDataManagerInMemoryImpl; +import com.android.tv.dvr.data.SeriesRecording; +import com.android.tv.testing.FakeClock; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Tests for {@link SeriesRecordingScheduler} + */ +@SmallTest +@SdkSuppress(minSdkVersion = Build.VERSION_CODES.N) +public class SeriesRecordingSchedulerTest extends AndroidTestCase { + private static final String PROGRAM_TITLE = "MyProgram"; + private static final long CHANNEL_ID = 123; + private static final long SERIES_RECORDING_ID1 = 1; + private static final String SERIES_ID = "SERIES_ID"; + private static final String SEASON_NUMBER1 = "SEASON NUMBER1"; + private static final String SEASON_NUMBER2 = "SEASON NUMBER2"; + private static final String EPISODE_NUMBER1 = "EPISODE NUMBER1"; + private static final String EPISODE_NUMBER2 = "EPISODE NUMBER2"; + + private final SeriesRecording mBaseSeriesRecording = new SeriesRecording.Builder() + .setTitle(PROGRAM_TITLE).setChannelId(CHANNEL_ID).setSeriesId(SERIES_ID).build(); + private final Program mBaseProgram = new Program.Builder().setTitle(PROGRAM_TITLE) + .setChannelId(CHANNEL_ID).setSeriesId(SERIES_ID).build(); + + private DvrDataManagerInMemoryImpl mDataManager; + + @Override + protected void setUp() throws Exception { + super.setUp(); + FakeClock fakeClock = FakeClock.createWithCurrentTime(); + mDataManager = new DvrDataManagerInMemoryImpl(getContext(), fakeClock); + } + + public void testPickOneProgramPerEpisode_onePerEpisode() { + SeriesRecording seriesRecording = SeriesRecording.buildFrom(mBaseSeriesRecording) + .setId(SERIES_RECORDING_ID1).build(); + mDataManager.addSeriesRecording(seriesRecording); + List programs = new ArrayList<>(); + Program program1 = new Program.Builder(mBaseProgram).setSeasonNumber(SEASON_NUMBER1) + .setEpisodeNumber(EPISODE_NUMBER1).build(); + programs.add(program1); + Program program2 = new Program.Builder(mBaseProgram).setSeasonNumber(SEASON_NUMBER2) + .setEpisodeNumber(EPISODE_NUMBER2).build(); + programs.add(program2); + LongSparseArray> result = SeriesRecordingScheduler.pickOneProgramPerEpisode( + mDataManager, Collections.singletonList(seriesRecording), programs); + MoreAsserts.assertContentsInAnyOrder(result.get(SERIES_RECORDING_ID1), program1, program2); + } + + public void testPickOneProgramPerEpisode_manyPerEpisode() { + SeriesRecording seriesRecording = SeriesRecording.buildFrom(mBaseSeriesRecording) + .setId(SERIES_RECORDING_ID1).build(); + mDataManager.addSeriesRecording(seriesRecording); + List programs = new ArrayList<>(); + Program program1 = new Program.Builder(mBaseProgram).setSeasonNumber(SEASON_NUMBER1) + .setEpisodeNumber(EPISODE_NUMBER1).setStartTimeUtcMillis(0).build(); + programs.add(program1); + Program program2 = new Program.Builder(program1).setStartTimeUtcMillis(1).build(); + programs.add(program2); + Program program3 = new Program.Builder(mBaseProgram).setSeasonNumber(SEASON_NUMBER2) + .setEpisodeNumber(EPISODE_NUMBER2).build(); + programs.add(program3); + Program program4 = new Program.Builder(program1).setStartTimeUtcMillis(1).build(); + programs.add(program4); + LongSparseArray> result = SeriesRecordingScheduler.pickOneProgramPerEpisode( + mDataManager, Collections.singletonList(seriesRecording), programs); + MoreAsserts.assertContentsInAnyOrder(result.get(SERIES_RECORDING_ID1), program1, program3); + } + + public void testPickOneProgramPerEpisode_nullEpisode() { + SeriesRecording seriesRecording = SeriesRecording.buildFrom(mBaseSeriesRecording) + .setId(SERIES_RECORDING_ID1).build(); + mDataManager.addSeriesRecording(seriesRecording); + List programs = new ArrayList<>(); + Program program1 = new Program.Builder(mBaseProgram).setStartTimeUtcMillis(0).build(); + programs.add(program1); + Program program2 = new Program.Builder(mBaseProgram).setStartTimeUtcMillis(1).build(); + programs.add(program2); + LongSparseArray> result = SeriesRecordingScheduler.pickOneProgramPerEpisode( + mDataManager, Collections.singletonList(seriesRecording), programs); + MoreAsserts.assertContentsInAnyOrder(result.get(SERIES_RECORDING_ID1), program1, program2); + } +} diff --git a/tests/unit/src/com/android/tv/dvr/ui/SortedArrayAdapterTest.java b/tests/unit/src/com/android/tv/dvr/ui/SortedArrayAdapterTest.java index a571e626..8fc8270f 100644 --- a/tests/unit/src/com/android/tv/dvr/ui/SortedArrayAdapterTest.java +++ b/tests/unit/src/com/android/tv/dvr/ui/SortedArrayAdapterTest.java @@ -32,10 +32,10 @@ import java.util.Objects; @SmallTest public class SortedArrayAdapterTest extends TestCase { - public static final TestData P1 = TestData.create(1, "one"); - public static final TestData P2 = TestData.create(2, "before"); - public static final TestData P3 = TestData.create(3, "other"); - public static final TestData EXTRA = TestData.create(4, "extra"); + public static final TestData P1 = TestData.create(1, "c"); + public static final TestData P2 = TestData.create(2, "b"); + public static final TestData P3 = TestData.create(3, "a"); + public static final TestData EXTRA = TestData.create(4, "k"); private TestSortedArrayAdapter mAdapter; @Override @@ -111,6 +111,43 @@ public class SortedArrayAdapterTest extends TestCase { assertContentsInOrder(mAdapter, P1); mAdapter.remove(P1); assertEmpty(); + mAdapter.add(P1); + mAdapter.add(P2); + mAdapter.add(P3); + assertContentsInOrder(mAdapter, P3, P2, P1); + mAdapter.removeItems(0, 2); + assertContentsInOrder(mAdapter, P1); + mAdapter.add(P2); + mAdapter.add(P3); + mAdapter.addExtraItem(EXTRA); + assertContentsInOrder(mAdapter, P3, P2, P1, EXTRA); + mAdapter.removeItems(1, 1); + assertContentsInOrder(mAdapter, P3, P1, EXTRA); + mAdapter.removeItems(1, 2); + assertContentsInOrder(mAdapter, P3); + mAdapter.addExtraItem(EXTRA); + mAdapter.addExtraItem(P2); + mAdapter.add(P1); + assertContentsInOrder(mAdapter, P3, P1, EXTRA, P2); + mAdapter.removeItems(1, 2); + assertContentsInOrder(mAdapter, P3, P2); + mAdapter.add(P1); + assertContentsInOrder(mAdapter, P3, P1, P2); + } + + public void testReplace() { + mAdapter.add(P1); + mAdapter.add(P2); + assertNotEmpty(); + assertContentsInOrder(mAdapter, P2, P1); + mAdapter.replace(1, P3); + assertContentsInOrder(mAdapter, P3, P2); + mAdapter.replace(0, P1); + assertContentsInOrder(mAdapter, P2, P1); + mAdapter.addExtraItem(EXTRA); + assertContentsInOrder(mAdapter, P2, P1, EXTRA); + mAdapter.replace(2, P3); + assertContentsInOrder(mAdapter, P2, P1, P3); } public void testChange_sorting() { @@ -194,7 +231,7 @@ public class SortedArrayAdapterTest extends TestCase { } @Override - long getId(TestData item) { + protected long getId(TestData item) { return item.mId; } } diff --git a/tests/unit/src/com/android/tv/experiments/ExperimentsTest.java b/tests/unit/src/com/android/tv/experiments/ExperimentsTest.java new file mode 100644 index 00000000..ab709e39 --- /dev/null +++ b/tests/unit/src/com/android/tv/experiments/ExperimentsTest.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.experiments; + +import android.support.test.filters.SmallTest; + +import com.android.tv.common.BuildConfig; + +import junit.framework.Assert; +import junit.framework.TestCase; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.List; + +/** + * Tests for {@link Experiments}. + */ +@SmallTest +public class ExperimentsTest extends TestCase { + + @Override + protected void setUp() throws Exception { + super.setUp(); + ExperimentFlag.initForTest(); + } + + + public void testEngOnlyDefault() { + assertEquals("ENABLE_DEVELOPER_FEATURES", Boolean.valueOf(BuildConfig.ENG), + Experiments.ENABLE_DEVELOPER_FEATURES.get()); + } + + +} diff --git a/usbtuner-res/animator/setup_before_entry.xml b/usbtuner-res/animator/setup_before_entry.xml deleted file mode 100644 index 82ed7992..00000000 --- a/usbtuner-res/animator/setup_before_entry.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - diff --git a/usbtuner-res/animator/setup_before_exit.xml b/usbtuner-res/animator/setup_before_exit.xml deleted file mode 100644 index 5c00064c..00000000 --- a/usbtuner-res/animator/setup_before_exit.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - diff --git a/usbtuner-res/animator/setup_entry.xml b/usbtuner-res/animator/setup_entry.xml deleted file mode 100644 index 35fcd4a3..00000000 --- a/usbtuner-res/animator/setup_entry.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - diff --git a/usbtuner-res/animator/setup_exit.xml b/usbtuner-res/animator/setup_exit.xml deleted file mode 100644 index 4ce89cd6..00000000 --- a/usbtuner-res/animator/setup_exit.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - diff --git a/usbtuner-res/drawable-xhdpi/ic_setup_antenna.png b/usbtuner-res/drawable-xhdpi/ic_setup_antenna.png deleted file mode 100644 index bb6d416e..00000000 Binary files a/usbtuner-res/drawable-xhdpi/ic_setup_antenna.png and /dev/null differ diff --git a/usbtuner-res/drawable/ut_selector_background.xml b/usbtuner-res/drawable/ut_selector_background.xml deleted file mode 100644 index fb3899aa..00000000 --- a/usbtuner-res/drawable/ut_selector_background.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - diff --git a/usbtuner-res/layout/ut_activity_playback.xml b/usbtuner-res/layout/ut_activity_playback.xml deleted file mode 100644 index b640e6d5..00000000 --- a/usbtuner-res/layout/ut_activity_playback.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - diff --git a/usbtuner-res/layout/ut_guidance.xml b/usbtuner-res/layout/ut_guidance.xml deleted file mode 100644 index 4f7d3f7a..00000000 --- a/usbtuner-res/layout/ut_guidance.xml +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - - - - - - diff --git a/usbtuner-res/layout/ut_guidedactions.xml b/usbtuner-res/layout/ut_guidedactions.xml deleted file mode 100644 index ae7efc0d..00000000 --- a/usbtuner-res/layout/ut_guidedactions.xml +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/usbtuner-res/values-af/strings.xml b/usbtuner-res/values-af/strings.xml index 4337a696..2154fd9e 100644 --- a/usbtuner-res/values-af/strings.xml +++ b/usbtuner-res/values-af/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "TV-ontvanger" "USB-TV-ontvanger" - "Aan" - "Af" + "Netwerk-TV-ontvanger (BETA)" "Wag asseblief dat verwerking voltooi word" - "Kies jou kanaalbron" - "Geen sein nie" - "Kon nie op %s inskakel nie" - "Kon nie inskakel nie" "Ontvangersagteware is onlangs opgedateer. Herskandeer die kanale asseblief." "Aktiveer omringklank in stelselklankinstellings om oudio te aktiveer" + "Kan nie oudio speel nie. Probeer asseblief \'n ander TV" "Opstelling van kanaalontvanger" "Opstelling van TV-ontvanger" "Opstelling van USB-kanaalontvanger" + "Opstelling van netwerkontvanger" "Maak seker dat jou TV aan \'n TV-seinbron gekoppel is.\n\nAs jy \'n oor-die-lug-antenna gebruik, sal jy dalk sy plasing of rigting moet verander om die meeste kanale te ontvang. Plaas dit vir die beste resultate hoog en naby \'n venster." "Maak seker dat die USB-ontvanger ingeprop en aan \'n TV-seinbron gekoppel is.\n\nAs jy \'n oor-die-lug-antenna gebruik, sal jy dalk sy posisie of rigting moet verander om die meeste kanale te ontvang. Plaas dit vir die beste resultate hoog en naby \'n venster." + "Maak seker dat die netwerkontvanger aangeskakel is en aan \'n TV-seinbron gekoppel is.\n\nAs jy \'n oor-die-lug-antenna gebruik, sal jy dalk sy plasing of rigting waarin hy wys, moet verstel om die meeste kanale te ontvang. Plaas dit vir die beste resultate hoog en naby \'n venster." "Gaan voort" "Nie nou nie" @@ -40,6 +38,7 @@ "Doen kanaalopstelling weer?" "Dit sal die kanale wat ontvang is, uit die TV-ontvanger verwyder en weer nuwe kanale soek.\n\nMaak seker dat jou TV aan \'n TV-seinbron gekoppel is.\n\nAs jy \'n oor-die-lug-antenna gebruik, sal jy dalk sy plasing of rigting moet verander om die meeste kanale te ontvang. Plaas dit vir die beste resultate hoog en naby \'n venster." "Dit sal die kanale wat gevind is, uit die USB-ontvanger verwyder en weer nuwe kanale soek.\n\nMaak seker dat die USB-ontvanger ingeprop en aan \'n TV-seinbron gekoppel is.\n\nAs jy \'n oor-die-lug-antenna gebruik, sal jy dalk sy posisie of rigting moet verander om die meeste kanale te ontvang. Plaas dit vir die beste resultate hoog en naby \'n venster." + "Dit sal die kanale wat gevind is van die netwerkontvanger af verwyder en weer na nuwe kanale soek.\n\nMaak seker dat die netwerkontvanger aangeskakel is en aan \'n TV-seinbron gekoppel is.\n\nAs jy \'n oor-die-lug-antenna gebruik, sal jy dalk sy plasing of rigting waarin hy wys, moet verstel om die meeste kanale te ontvang. Plaas dit vir die beste resultate hoog en naby \'n venster." "Gaan voort" "Kanselleer" @@ -54,6 +53,7 @@ "Opstelling van TV-ontvanger" "Opstelling van USB-kanaalontvanger" + "Opstelling van netwerkkanaalontvanger" "Dit kan \'n paar minute neem" "Seinontvanger is tydelik nie beskikbaar nie of word reeds deur opname gebruik." @@ -76,6 +76,7 @@ "Geen kanale gevind nie" "Geen kanale is in die soektog gevind nie. Maak seker dat jou TV aan \'n TV-seinbron gekoppel is.\n\nAs jy \'n oor-die-lug-antenna gebruik, verander sy plasing of rigting. Plaas dit vir die beste resultate hoog en naby \'n venster en soek weer." "Geen kanale is in die soektog gevind nie. Maak seker dat die USB-ontvanger ingeprop en aan \'n TV-seinbron gekoppel is.\n\nAs jy \'n oor-die-lug-antenna gebruik, verander sy posisie of rigting. Plaas dit vir die beste resultate hoog en naby \'n venster en soek weer." + "Geen kanale is in die soektog gevind nie. Maak seker dat die netwerkontvanger aangeskakel is en aan \'n TV-seinbron gekoppel is.\n\nAs jy \'n oor-die-lug-antenna gebruik, verstel sy plasing of rigting waarin hy wys. Plaas dit vir die beste resultate hoog en naby \'n venster en soek weer." "Soek weer" "Klaar" @@ -83,5 +84,7 @@ "Soek TV-kanale" "Opstelling van TV-ontvanger" "Opstelling van USB-TV-ontvanger" - "USB-TV-ontvanger is ontkoppel." + "Opstelling van netwerk-TV-ontvanger" + "USB-TV-ontvanger is ontkoppel." + "Netwerkontvanger is ontkoppel." diff --git a/usbtuner-res/values-am/strings.xml b/usbtuner-res/values-am/strings.xml index a903ee76..9ae0dc44 100644 --- a/usbtuner-res/values-am/strings.xml +++ b/usbtuner-res/values-am/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "የቴሌቪዥን መቃኛ" "የዩኤስቢ ቴሌቪዥን መቃኛ" - "በርቷል" - "ጠፍቷል" + "የአውታረ መረብ ቴሌቪዥን መቃኛ (ቅድመ-ይሁንታ)" "ማስኬድን ለማጠናቀቅ እባክዎ ይጠብቁ" - "የጣቢያ ምንጭዎን ይምረጡ" - "ምንም ሲግናል የለም" - "ወደ %s መቃኘት አልተሳካም" - "መቃኘት አልተሳካም" "የቴሌቪዥን መቃኛ ሶፍትዌር በቅርብ ጊዜ ተዘምኗል። እባክዎ ሰርጦቹን እንደገና ይቃኟቸው።" "ኦዲዮን ለማንቃት በስርዓት ድምጽ ቅንብሮች ውስጥ የዙሪያ ድምጽን ያንቁ" + "ኦዲዮ ማጫወት አይቻልም። እባክዎ ሌላ ቲቪ ይሞክሩ።" "የጣቢያ መቃኛ ማዋቀር" "የቴሌቪዥን መቃኛ ማዋቀር" "የዩኤስቢ ጣቢያ መቃኛ ማዋቀር" + "የአውታረ መረብ መቃኛ ማዋቀር" "የእርስዎ ቴሌቪዥን ከቴሌቪዥን ሲግናል ምልክት ምንጭ ጋር መገናኘቱን ያረጋግጡ።\n\nየአየር ላይ አንቴና የሚጠቀሙ ከሆነ አብዛኛዎቹን ጣቢያዎች ለመቀበል አቀማመጡን ወይም አቅጣጫውን ማስተካከል ሊኖርብዎት ይችላል። ለተሻሉ ውጤቶች ከፍ አድርገው ከመስኮት አጠገብ ያስቀምጡት።" "የዩኤስቢ መቃኛው መሰካቱን እና ከቴሌቪዥን ምልክት ምንጭ ጋር መገናኘቱን ያረጋግጡ።\n\nየአየር ላይ አንቴና የሚጠቀሙ ከሆነ አቀማመጡን ወይም አቅጣጫውን ያስተካክሉ። ለተሻሉ ውጤቶች ከፍ አድርገው ከመስኮት አጠገብ ያስቀምጡት።" + "የአውታረ መረብ መቃኛው እና ከቴሌቪዥን ምልክት ምንጭ ጋር መገናኘቱን ያረጋግጡ።\n\nየአየር ላይ አንቴና የሚጠቀሙ ከሆነ አቀማመጡን ወይም አቅጣጫውን ያስተካክሉ። ለተሻሉ ውጤቶች ከፍ አድርገው ከመስኮት አጠገብ ያስቀምጡት።" "ቀጥል" "አሁን አይደለም" @@ -40,6 +38,7 @@ "የጣቢያ ቅንብር እንደገና እንዲሄድ ይደረግ?" "ይሄ ከቴሌቪዥን መቃኛ የተገኙ ጣቢያዎችን አስወግዶ አዲስ ጣቢያዎችን እንደገና ይቃኛል።\n\nየእርስዎ ቴሌቪዥን ከቴሌቪዥን ሲግናል ምልክት ምንጭ ጋር መገናኘቱን ያረጋግጡ።\n\nየአየር ላይ አንቴና የሚጠቀሙ ከሆነ አብዛኛዎቹን ጣቢያዎች ለመቀበል አቀማመጡን ወይም አቅጣጫውን ማስተካከል ሊኖርብዎት ይችላል። ለተሻሉ ውጤቶች ከፍ አድርገው ከመስኮት አጠገብ ያስቀምጡት።" "ይሄ ከዩኤስቢ መቃኛ የተገኙ ጣቢያዎችን አስወግዶ አዲስ ጣቢያዎችን እንደገና ይቃኛል።\n\nየዩኤስቢ መቃኛው መሰካቱን እና ከቴሌቪዥን ምልክት ምንጭ ጋር መገናኘቱን ያረጋግጡ።\n\nየአየር ላይ አንቴና የሚጠቀሙ ከሆነ አቀማመጡን ወይም አቅጣጫውን ያስተካክሉ። ለተሻሉ ውጤቶች ከፍ አድርገው ከመስኮት አጠገብ ያስቀምጡት።" + "ይሄ ከአውታረ መረብ መቃኛ የተገኙ ጣቢያዎችን አስወግዶ አዲስ ጣቢያዎችን እንደገና ይቃኛል።\n\nየአውታረ መረብ መቃኛው እና ከቴሌቪዥን ምልክት ምንጩ መገናኘቱን ያረጋግጡ።\n\nየአየር ላይ አንቴና የሚጠቀሙ ከሆነ አቀማመጡን ወይም አቅጣጫውን ያስተካክሉ። ለተሻሉ ውጤቶች ከፍ አድርገው ከመስኮት አጠገብ ያስቀምጡት።" "ቀጥል" "ይቅር" @@ -54,6 +53,7 @@ "የቴሌቪዥን መቃኛ ማዋቀር" "የዩኤስቢ ጣቢያ መቃኛ ማዋቀር" + "የአውታረ መረብ ጣቢያ መቃኛ ማዋቀር" "ይሄ በርካታ ደቂቃዎችን ሊወስድ ይችላል" "መቃኛው ለጊዜው አይገኝም ወይም አስቀድሞ በቀረጻው ጥቅም ላይ ውሏል።" @@ -76,6 +76,7 @@ "ምንም ጣቢያዎች አልተገኙም" "ቅኝቱ ምንም አዲስ ጣቢያዎችን አላገኘም። የእርስዎ ቴሌቪዥን ከቴሌቪዥን ሲግናል ምንጭ ጋር መገናኘቱን ያረጋግጡ።\n\nየአየር ላይ አንቴና ከሆነ የሚጠቀሙት አቀማመጡን ወይም አቅጣጫውን ያስተካክሉት። ለተሻሉ ውጤቶች ከፍ አድርገው ከመስኮት አጠገብ ያስቀምጡትና እንደገና ይቃኙ።" "ቅኝቱ ምንም ጣቢያዎችን አላገኘም። የዩኤስቢ መቃኛው መሰካቱን እና ከቴሌቪዥን ሲግናል ምንጩ ጋር መገናኘቱን ያረጋግጡ።\n\nየአየር ላይ አንቴና የሚጠቀሙ ከሆነ አቀማመጡን ወይም አቅጣጫውን ያስተካክሉ። ለተሻሉ ውጤቶች ከፍ አድርገው ከመስኮት አጠገብ ያስቀምጡት እና እንደገና ይቃኙ።" + "ቅኝቱ ምንም ጣቢያዎችን አላገኘም። የአውታረ መረብ መቃኛው እንደበራ እና ከቴሌቪዥን ሲግናል ምንጩ ጋር መገናኘቱን ያረጋግጡ።\n\nየአየር ላይ አንቴና የሚጠቀሙ ከሆነ አቀማመጡን ወይም አቅጣጫውን ያስተካክሉ። ለተሻሉ ውጤቶች ከፍ አድርገው ከመስኮት አጠገብ ያስቀምጡት እና እንደገና ይቃኙ።" "እንደገና ቃኝ" "ተከናውኗል" @@ -83,5 +84,7 @@ "የቲቪ ጣቢያዎችን ቃኝ" "የቴሌቪዥን መቃኛ ማዋቀር" "የዩኤስቢ ቴሌቪዥን መቃኛ ማዋቀር" - "የUSB TV መቃኛ ግንኙነቱ ተቋርጧል።" + "የአውታረ መረብ ቴሌቪዥን መቃኛ ማዋቀር" + "የዩኤስቢ ቴሌቪዥን መቃኛው ግንኙነት ተቋርጧል።" + "የአውታረ መረብ መቃኛ ግንኙነት ተቋርጧል።" diff --git a/usbtuner-res/values-ar/strings.xml b/usbtuner-res/values-ar/strings.xml index ecfde3e8..748425f4 100644 --- a/usbtuner-res/values-ar/strings.xml +++ b/usbtuner-res/values-ar/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "موالف التلفزيون" "‏موالف التلفزيون عبر USB" - "تشغيل" - "إيقاف" + "موالف التلفزيون على الشبكة (تجريبي)" "الرجاء الانتظار لحين انتهاء المعالجة" - "تحديد مصدر القنوات" - "لا توجد إشارة" - "أخفق الضبط على %s" - "أخفق الضبط" "تم تحديث برنامج الموالف مؤخرًا. الرجاء إعادة البحث عن القنوات." "يمكنك تشغيل الصوت المحيطي في إعدادات صوت النظام لتفعيل الصوت" + "لا يمكن تشغيل الصوت. الرجاء تجربة تلفزيون آخر." "إعداد موالف القنوات" "إعداد موالف التلفزيون" "‏إعداد موالف قنوات USB" + "إعداد موالف الشبكة" "تحقق من توصيل التلفزيون بمصدر إشارة البث التلفزيوني.\n\nإذا كنت تستخدم هوائيًا للتحديث عبر الهواء، فقد تحتاج إلى ضبط موضعه أو اتجاهه لاستقبال معظم القنوات، وللحصول على أفضل النتائج، ضعه عاليًا بالقرب من النافذة." "‏تحقق من توصيل الموالف عبر USB بمصدر إشارة البث التلفزيوني.\n\nإذا كنت تستخدم هوائيًا للتحديث عبر الهواء، فقد تحتاج إلى ضبط موضعه أو اتجاهه لاستقبال معظم القنوات، وللحصول على أفضل النتائج، ضعه عاليًا بالقرب من النافذة." + "تحقق من تشغيل موالف الشبكة وتوصيله بمصدر إشارة التلفزيون.\n\nفي حالة استخدام هوائي للتحديث عبر الهواء، قد تحتاج إلى ضبط موضعه أو تجاهه لاستقبال معظم القنوات. وللحصول على أفضل النتائج، يمكنك وضعه في مكان مرتفع أو بالقرب من النافذة." "متابعة" "ليس الآن" @@ -40,6 +38,7 @@ "هل تريد إعادة تشغيل إعداد القنوات؟" "سيؤدي هذا إلى إزالة القنوات التي تم العثور عليها من موالف التلفزيون والبحث مرة أخرى عن قنوات جديدة.\n\nتحقق من توصيل التلفزيون بمصدر إشارة البث التلفزيوني.\n\nإذا كنت تستخدم هوائيًا للتحديث عبر الهواء، فقد تحتاج إلى ضبط موضعه أو اتجاهه لاستقبال معظم القنوات، وللحصول على أفضل النتائج، ضعه عاليًا بالقرب من النافذة." "‏سيؤدي هذا إلى إزالة القنوات التي تم العثور عليها من الموالف عبر USB والبحث مرة أخرى عن قنوات جديدة.\n\nتحقق من توصيل الموالف عبر USB بمصدر إشارة البث التلفزيوني.\n\nإذا كنت تستخدم هوائيًا للتحديث عبر الهواء، فقد تحتاج إلى ضبط موضعه أو اتجاهه لاستقبال معظم القنوات، وللحصول على أفضل النتائج، ضعه عاليًا بالقرب من النافذة." + "سيؤدي هذا إلى إزالة القنوات الموجودة من موالف الشبكة وإعادة المسح بحثًا عن القنوات الجديدة.\n\nتحقق من تشغيل موالف الشبكة وتوصيله بمصدر إشارة التلفزيون.\n\nفي حالة استخدام هوائي للتحديث عبر الهواء، قد تحتاج إلى ضبط موضعه أو تجاهه لاستقبال معظم القنوات. وللحصول على أفضل النتائج، يمكنك وضعه في مكان مرتفع أو بالقرب من النافذة." "متابعة" "إلغاء" @@ -54,6 +53,7 @@ "إعداد موالف التلفزيون" "‏إعداد موالف قنوات USB" + "إعداد موالف قناة الشبكة" "قد يستغرق هذا عدة دقائق" "لا يتوفر الموالف مؤقتًا أو سبق استخدامه بواسطة التسجيل." @@ -88,6 +88,7 @@ "لم يتم العثور على قنوات" "لم يتم العثور على أي قنوات أثناء البحث، لذا عليك التحقق من توصيل التلفزيون بمصدر إشارة البث التلفزيوني.\n\nإذا كنت تستخدم هوائيًا للتحديث عبر الهواء، فاضبط موضعه أو اتجاهه، وللحصول على أفضل النتائج، ضعه عاليًا بالقرب من النافذة ثم أعد البحث." "‏لم يتم العثور على أي قنوات أثناء البحث، تحقق من توصيل الموالف عبر USB بمصدر إشارة البث التلفزيوني.\n\nإذا كنت تستخدم هوائيًا للتحديث عبر الهواء، فاضبط موضعه أو اتجاهه، وللحصول على أفضل النتائج، ضعه عاليًا بالقرب من النافذة ثم أعد البحث." + "لم يتم العثور على أي قنوات خلال المسح. تحقق من تشغيل موالف الشبكة وتوصيله بمصدر إشارة التلفزيون.\n\nفي حالة استخدام هوائي للتحديث عبر الهواء، يجب ضبط موضعه أو تجاهه. وللحصول على أفضل النتائج، يمكنك وضعه في مكان مرتفع أو بالقرب من النافذة وإعادة المسح." "بحث مرة أخرى" "تم" @@ -95,5 +96,7 @@ "البحث عن قنوات تلفزيونية" "إعداد موالف التلفزيون" "‏إعداد موالف التلفزيون عبر USB" - "‏تم فصل موالف التلفزيون عبر USB." + "إعداد موالف التلفزيون على الشبكة" + "‏تم فصل موالف التلفزيون عبر USB." + "تم فصل موالف الشبكة." diff --git a/usbtuner-res/values-az-rAZ/strings.xml b/usbtuner-res/values-az-rAZ/strings.xml index f5305817..40cc2557 100644 --- a/usbtuner-res/values-az-rAZ/strings.xml +++ b/usbtuner-res/values-az-rAZ/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "TV Kökləyici" "USB TV Kökləyici" - "Aktiv" - "Deaktiv" + "Network TV Tuner (BETA)" "Lütfən, prosesi başa çatdırmaq üçün gözləyin" - "Kanal mənbəyinizi seçin" - "Siqnal Yoxdur" - "%s kanalına sazlamaq mümkün olmadı" - "Sazlamaq uğursuz oldu" "Sazlayıcı proqram təminatı yenicə güncəllənib. Kanalları yenidən skan edin." "Audionu aktiv etmək üçün sistem səs ayarlarında əhatəli səsi aktiv edin" + "Audio oxuna bilmir. Digər TV-dən istifadə edin" "Kanal kökləyici quraşdırması" "TV Kökləyici quraşdırması" "USB Kanal kökləyici quraşdırması" + "Şəbəkə kökləyici quraşdırması" "TV-nizin TV siqnal mənbəyinə qoşulu olduğunu doğrulayın.\n\nHava antenası istifadə etdikdə, daha çox kanal üçün onun yerini və istiqamətini tənzimləməlisiniz. Daha yaxşı nəticələr üçün hündür yerə və pəncərəyə yaxın yerləşdirin." "USB kökləyicinin taxılı olduğunu və TV siqnal mənbəyinə qoşulu olduğunu doğrulayın.\n\nHava antenası istifadə etdikdə, daha çox kanal üçün onun yerini və istiqamətini tənzimləməlisiniz. Daha yaxşı nəticələr üçün hündür yerə və pəncərəyə yaxın yerləşdirin." + "Şəbəkə kökləyicinin yanılı olduğunu və TV siqnal mənbəyinə qoşulu olduğunu doğrulayın.\n\nHava antenası istifadə etdikdə, daha çox kanal üçün onun yerini və istiqamətini tənzimləməlisiniz. Daha yaxşı nəticələr üçün hündür yerə və pəncərəyə yaxın yerləşdirin." "Davam edin" "İndi yox" @@ -40,6 +38,7 @@ "Kanal quraşdırması yenidən işə salınsın?" "Bu, TV kökləyici ilə tapılmış kanalları siləcək və yeni kanalları yenidən skan edəcək.\n\nTV-nizin TV siqnal mənbəyinə qoşulu olduğunu doğrulayın.\n\nHava antenası istifadə etdikdə, daha çox kanal üçün onun yerini və istiqamətini tənzimləməlisiniz. Daha yaxşı nəticələr üçün hündür yerə və pəncərəyə yaxın yerləşdirin." "Bu USB kökləyici ilə tapılmış kanalları siləcək və yeni kanalları yenidən skan edəcək.\n\nUSB kökləyicinin taxılı olduğunu və TV siqnal mənbəyinə qoşulu olduğunu doğrulayın.\n\nHava antenası istifadə etdikdə, daha çox kanal üçün onun yerini və istiqamətini tənzimləməlisiniz. Daha yaxşı nəticələr üçün hündür yerə və pəncərəyə yaxın yerləşdirin." + "Bu Şəbəkə kökləyici ilə tapılmış kanalları siləcək və yeni kanalları yenidən skan edəcək.\n\nŞəbəkə kökləyicinin yanılı olduğunu və TV siqnal mənbəyinə qoşulu olduğunu doğrulayın.\n\nHava antenası istifadə etdikdə, daha çox kanal üçün onun yerini və istiqamətini tənzimləməlisiniz. Daha yaxşı nəticələr üçün hündür yerə və pəncərəyə yaxın yerləşdirin." "Davam edin" "Ləğv edin" @@ -54,6 +53,7 @@ "TV kökləyici quraşdırması" "USB Kanal kökləyici quraşdırması" + "Şəbəkə kanalı kökləyici quraşdırması" "Bu bir neçə dəqiqə çəkə bilər" "Kökləyici müvəqqəti əlçatan deyil və qeydə alma tərəfindən istifadə olunub." @@ -76,6 +76,7 @@ "Kanal tapılmadı" "Skan ilə heç bir kanal tapılmadı. TV-nizin TV siqnal mənbəyinə qoşulu olduğunu doğrulayın.\n\nHava antenası istifadə etdikdə, daha çox kanal üçün onun yerini və istiqamətini tənzimləməlisiniz. Daha yaxşı nəticələr üçün hündür yerə və pəncərəyə yaxın yerləşdirin." "Skan ilə heç bir kanal tapılmadı. USB kökləyicinin taxılı olduğunu və TV siqnal mənbəyinə qoşulu olduğunu doğrulayın.\n\nHava antenası istifadə etdikdə, daha çox kanal üçün onun yerini və istiqamətini tənzimləməlisiniz. Daha yaxşı nəticələr üçün hündür yerə və pəncərəyə yaxın yerləşdirin." + "Skan ilə heç bir kanal tapılmadı. Şəbəkə kökləyicinin yanılı olduğunu və TV siqnal mənbəyinə qoşulu olduğunu doğrulayın.\n\nHava antenası istifadə etdikdə, daha çox kanal üçün onun yerini və istiqamətini tənzimləməlisiniz. Daha yaxşı nəticələr üçün hündür yerə və pəncərəyə yaxın yerləşdirin." "Yenidən skan edin" "Hazırdır" @@ -83,5 +84,7 @@ "TV kanalları üçün skan" "TV Kökləyici quraşdırması" "USB TV Kökləyici quraşdırması" - "USB TV kökləyicisinin bağlantısı kəsildi." + "Şəbəkə TV Kökləyici quraşdırması" + "USB TV kökləyicisinin bağlantısı kəsildi." + "Şəbəkə kökləyicisinin bağlantısı kəsildi." diff --git a/usbtuner-res/values-bg/strings.xml b/usbtuner-res/values-bg/strings.xml index 0854c09e..bb4d4292 100644 --- a/usbtuner-res/values-bg/strings.xml +++ b/usbtuner-res/values-bg/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "Телевизионен тунер" "Телевизионен USB тунер" - "Включване" - "Изключване" + "Network TV Tuner (БЕТА)" "Моля, изчакайте обработването да завърши" - "Изберете източник на канали" - "Няма сигнал" - "Превключването към „%s“ не бе успешно" - "Превключването не бе успешно" "Софтуерът на тунера е актуализиран наскоро. Моля, сканирайте отново каналите." "Активирайте обемния звук от настройките за системния, за да включите аудиото" + "Звукът не може да се възпроизведе. Моля, опитайте на друг телевизор" "Настройване на тунера за канали" "Настройване на телевизионния тунер" "Настройване на USB тунера за канали" + "Настройване на мрежовия тунер" "Уверете се, че телевизорът ви е свързан с източник на телевизионен сигнал.\n\nАко използвате безжична антена, може да се наложи да коригирате разположението или посоката й, за да получите оптимален брой канали. За най-добри резултати я поставете високо и близо до прозорец." "Уверете се, че USB тунерът е включен и свързан с източник на телевизионен сигнал.\n\nАко използвате безжична антена, може да се наложи да коригирате разположението или посоката й, за да получите оптимален брой канали. За най-добри резултати я поставете високо и близо до прозорец." + "Уверете се, че мрежовият тунер е включен и свързан с източник на телевизионен сигнал.\n\nАко използвате безжична антена, може да се наложи да коригирате разположението или посоката й, за да уловите най-много канали. За най-добри резултати я поставете високо и близо до прозорец." "Напред" "Не сега" @@ -40,6 +38,7 @@ "Да се стартира ли отново настройването на каналите?" "Така ще премахнете намерените от телевизионния тунер канали и ще сканирате за нови.\n\nУверете се, че телевизорът ви е свързан с източник на телевизионен сигнал.\n\nАко използвате безжична антена, може да се наложи да коригирате разположението или посоката й, за да получите оптимален брой канали. За най-добри резултати я поставете високо и близо до прозорец." "Така ще премахнете намерените от USB тунера канали и ще сканирате за нови.\n\nУверете се, че USB тунерът е включен и свързан с източник на телевизионен сигнал.\n\nАко използвате безжична антена, може да се наложи да коригирате разположението или посоката й, за да получите оптимален брой канали. За най-добри резултати я поставете високо и близо до прозорец." + "Така ще премахнете намерените от мрежовия тунер канали и ще сканирате за нови.\n\nУверете се, че мрежовият тунер е включен и свързан с източник на телевизионен сигнал.\n\nАко използвате безжична антена, може да се наложи да коригирате разположението или посоката й, за да уловите най-много канали. За най-добри резултати я поставете високо и близо до прозорец." "Напред" "Отказ" @@ -54,6 +53,7 @@ "Настройване на телевизионния тунер" "Настройване на USB тунера за канали" + "Настройване на мрежовия тунер за канали" "Това може да отнеме няколко минути" "Временно няма достъп до тунера или той вече се използва за запис." @@ -76,6 +76,7 @@ "Няма намерени канали" "При сканирането не бяха открити канали. Уверете се, че телевизорът ви е свързан с източник на телевизионен сигнал.\n\nАко използвате безжична антена, коригирайте разположението или посоката й. За най-добри резултати я поставете високо и близо до прозорец и сканирайте отново." "При сканирането не бяха открити канали. Уверете се, че USB тунерът е включен и свързан с източник на телевизионен сигнал.\n\nАко използвате безжична антена, коригирайте разположението или посоката й. За най-добри резултати я поставете високо и близо до прозорец и сканирайте отново." + "При сканирането не бяха открити канали. Уверете се, че мрежовият тунер е включен и свързан с източник на телевизионен сигнал.\n\nАко използвате безжична антена, коригирайте разположението или посоката й. За най-добри резултати я поставете високо и близо до прозорец и сканирайте отново." "Повторно сканиране" "Готово" @@ -83,5 +84,7 @@ "Сканиране за телевизионни канали" "Настройване на телевизионния тунер" "Настройване на телевизионния USB тунер" - "Връзката с телевизионния USB тунер е прекратена." + "Настройване на Network TV Tuner" + "Връзката с телевизионния USB тунер е прекратена." + "Връзката с мрежовия тунер е прекратена." diff --git a/usbtuner-res/values-bn-rBD/strings.xml b/usbtuner-res/values-bn-rBD/strings.xml index 236e2d96..de24ef93 100644 --- a/usbtuner-res/values-bn-rBD/strings.xml +++ b/usbtuner-res/values-bn-rBD/strings.xml @@ -19,27 +19,26 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "TV টিউনার" "USB TV টিউনার" - "চালু আছে" - "বন্ধ করুন" + "নেটওয়ার্ক TV টিউনার (বিটা)" "প্রক্রিয়াকরণ সম্পূর্ণ না হওয়া পর্যন্ত অনুগ্রহ করে অপেক্ষা করুন" - "আপনার চ্যানেলের উৎস নির্বাচন করুন" - "কোনো সংকেত নেই" - "%s এ টিউন করতে ব্যর্থ হয়েছে" - "টিউন করতে ব্যর্থ হয়েছে" - "টিউনার সফ্টওয়্যার সম্প্রতি আপডেট করা হয়েছে৷ অনুগ্রহ করে চ্যানেলগুলি পুনরায় স্ক্যান করুন৷" + "টিউনার সফ্টওয়্যার সম্প্রতি আপডেট করা হয়েছে৷ অনুগ্রহ করে চ্যানেলগুলি আবার স্ক্যান করুন৷" "অডিও সক্ষম করতে সিস্টেম সাউন্ড সেটিংসে সারাউন্ড সাউন্ড সক্ষম করুন" + "অডিও প্লে করা যাবে না৷ অনুগ্রহ করে অন্য টিভি ব্যবহার করার চেষ্ট করুন" "চ্যানেল টিউনার সেট আপ" "TV টিউনার সেট আপ" "USB চ্যানেল টিউনার সেটআপ" + "নেটওয়ার্ক টিউনার সেট আপ" "আপনার TV একটি TV সিগন্যাল উৎসের সাথে সংযুক্ত রয়েছে কিনা যাচাই করুন৷\n\nযদি কোনো ওভার-দ্য-এয়ার অ্যান্টেনা ব্যবহার করা হয় তাহলে অধিকাংশ চ্যানেল পাওয়ার জন্য আপনাকে সেটির অবস্থান এবং দিক ঠিক করতে হতে পারে৷ আরো ভাল ফলাফলের জন্য, এটিকে উচুঁতে কোনো জানলার সামনে রাখুন এবং আবার স্ক্যান করুন৷" "USB টিউনার প্ল্যাগ ইন রয়েছে এবং একটি TV সিগন্যাল উৎসের সাথে সংযুক্ত রয়েছে তা যাচাই করুন৷\n\nযদি কোনো ওভার-দ্য-এয়ার অ্যান্টেনা ব্যবহার করা হয় তাহলে অধিকাংশ চ্যানেল পাওয়ার জন্য আপনাকে সেটির অবস্থান এবং দিক ঠিক করতে হতে পারে৷ আরো ভাল ফলাফলের জন্য, এটিকে উচুঁতে কোনো জানলার সামনে রাখুন এবং আবার স্ক্যান করুন৷" + "নেটওয়ার্ক টিউনার চালু রয়েছে এবং কোনো TV সিগন্যাল উৎসের সাথে সংযুক্ত রয়েছে কিনা যাচাই করুন৷\n\nযদি কোনো ওভার-দ্য-এয়ার অ্যান্টেনা ব্যবহার করা হয় তাহলে অধিকাংশ চ্যানেল পাওয়ার জন্য আপনাকে সেটির অবস্থান এবং দিক ঠিক করতে হতে পারে৷ আরো ভাল ফলাফলের জন্য, এটিকে উঁচুতে কোনো জানলার সামনে রেখে আবার স্ক্যান করুন৷" "চালিয়ে যান" "এখনই নয়" - "পুনরায় চ্যানেল সেট আপ করবেন?" + "আবার চ্যানেল সেট আপ করবেন?" "এটি TV টিউনার থেকে পাওয়া চ্যানেলগুলিকে মুছবে এবং নতুন চ্যানেলগুলির জন্য আবার স্ক্যান করবে৷\n\nআপনার TV একটি TV সিগন্যাল উৎসের সাথে সংযুক্ত রয়েছে কিনা যাচাই করুন৷\n\nযদি কোনো ওভার-দ্য-এয়ার অ্যান্টেনা ব্যবহার করা হয় তাহলে অধিকাংশ চ্যানেল পাওয়ার জন্য আপনাকে সেটির অবস্থান এবং দিক ঠিক করতে হতে পারে৷ আরো ভাল ফলাফলের জন্য, এটিকে উচুঁতে কোনো জানলার সামনে রাখুন এবং আবার স্ক্যান করুন৷" "এটি USB টিউনার থেকে পাওয়া চ্যানেলগুলিকে মুছবে এবং নতুন চ্যানেলগুলির জন্য আবার স্ক্যান করবে৷\n\nUSB টিউনার প্ল্যাগ ইন রয়েছে এবং একটি TV সিগন্যাল উৎসের সাথে সংযুক্ত রয়েছে তা যাচাই করুন৷\n\nযদি কোনো ওভার-দ্য-এয়ার অ্যান্টেনা ব্যবহার করা হয় তাহলে অধিকাংশ চ্যানেল পাওয়ার জন্য আপনাকে সেটির অবস্থান এবং দিক ঠিক করতে হতে পারে৷ আরো ভাল ফলাফলের জন্য, এটিকে উচুঁতে কোনো জানলার সামনে রাখুন এবং আবার স্ক্যান করুন৷" + "এটি নেটওয়ার্ক টিউনার থেকে পাওয়া চ্যানেলগুলি মুছবে এবং নতুন চ্যানেলগুলিকে আবার স্ক্যান করবে৷\n\nনেটওয়ার্ক টিউনার চালু রয়েছে এবং কোনো TV সিগন্যাল উৎসের সাথে সংযুক্ত রয়েছে কিনা যাচাই করুন৷\n\nযদি কোনো ওভার-দ্য-এয়ার অ্যান্টেনা ব্যবহার করা হয় তাহলে অধিকাংশ চ্যানেল পাওয়ার জন্য আপনাকে সেটির অবস্থান এবং দিক ঠিক করতে হতে পারে৷ আরো ভাল ফলাফলের জন্য, এটিকে উঁচুতে কোনো জানলার সামনে রেখে আবার স্ক্যান করুন৷" "চালিয়ে যান" "বাতিল করুন" @@ -54,6 +53,7 @@ "TV টিউনার সেট আপ" "USB চ্যানেল টিউনার সেটআপ" + "নেটওয়ার্ক চ্যানেল টিউনার সেটআপ" "এটি কয়েক মিনিট সময় নিতে পারে" "টিউনার অস্থায়ীভাবে অনুপলব্ধ বা রেকডিংয়ে ইতিমধ্যেই ব্যবহৃত হয়েছে৷" @@ -76,6 +76,7 @@ "কোনো চ্যানেল খুঁজে পাওয়া যায়নি" "স্ক্যান করে কোনো চ্যানেল খুঁজে পাওয়া যায়নি৷ আপনার TV একটি TV সিগন্যাল উৎসের সাথে সংযুক্ত রয়েছে কিনা যাচাই করুন৷\n\nযদি কোনো ওভার-দ্য-এয়ার অ্যান্টেনা ব্যবহার করা হয় তাহলে সেটির অবস্থান এবং দিক ঠিক করুন৷ আরো ভাল ফলাফলের জন্য, এটিকে উচুঁতে কোনো জানলার সামনে রাখুন এবং আবার স্ক্যান করুন৷" "স্ক্যান করে কোনো চ্যানেল খুঁজে পাওয়া যায়নি৷ USB টিউনার প্ল্যাগ ইন রয়েছে এবং একটি TV সিগন্যাল উৎসের সাথে সংযুক্ত রয়েছে তা যাচাই করুন৷\n\nযদি কোনো ওভার-দ্য-এয়ার অ্যান্টেনা ব্যবহার করা হয় তাহলে সেটির অবস্থান এবং দিক ঠিক করুন৷ আরো ভাল ফলাফলের জন্য, এটিকে উচুঁতে কোনো জানলার সামনে রাখুন এবং আবার স্ক্যান করুন৷" + "স্ক্যান করে কোনো চ্যানেল খুঁজে পাওয়া যায়নি৷ নেটওয়ার্ক টিউনার চালু এবং কোনো TV সিগন্যাল উৎসের সাথে সংযুক্ত রয়েছে কিনা যাচাই করুন৷\n\nযদি কোনো ওভার-দ্য-এয়ার অ্যান্টেনা ব্যবহার করা হয় তাহলে সেটির অবস্থান এবং দিক ঠিক করুন৷ আরো ভাল ফলাফলের জন্য, এটিকে উঁচুতে কোনো জানলার সামনে রেখে আবার স্ক্যান করুন৷" "আবার স্ক্যান করুন" "সম্পন্ন" @@ -83,5 +84,7 @@ "টিভি চ্যানেলগুলি স্ক্যান করুন" "TV টিউনার সেট আপ" "USB টিভি টিউনার সেট আপ" - "USB টিভি টিউনারের সংযোগ বিচ্ছিন্ন হয়েছে।" + "নেটওয়ার্ক TV টিউনার সেট আপ" + "USB টিভি টিউনারের সংযোগ বিচ্ছিন্ন হয়েছে।" + "নেটওয়ার্ক টিউনারের সংযোগ বিচ্ছিন্ন হয়েছে।" diff --git a/usbtuner-res/values-ca/strings.xml b/usbtuner-res/values-ca/strings.xml index af90c3d6..ac857f2e 100644 --- a/usbtuner-res/values-ca/strings.xml +++ b/usbtuner-res/values-ca/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "Sintonitzador de televisió" "Sintonitzador de televisió USB" - "Activa" - "Desactiva" + "Sintonitzador de televisió en xarxa (BETA)" "Espera per finalitzar el processament" - "Selecciona la font del canal" - "Sense senyal" - "No s\'ha pogut sintonitzar %s" - "No s\'ha pogut sintonitzar" "El programari del sintonitzador s\'ha actualitzat fa poc. Torna a cercar els canals." "Activa el so envoltant a la configuració de so del sistema per activar l\'àudio" + "No es pot reproduir l\'àudio. Prova-ho amb un altre televisor." "Configuració del sintonitzador de canals" "Configuració del sintonitzador de televisió" "Configuració del sintonitzador de canals USB" + "Configuració del sintonitzador en xarxa" "Verifica que el teu televisor estigui connectat a una font de senyal de televisió.\n\nSi fas servir una antena aèria, pot ser que calgui ajustar-ne la ubicació o la direcció per rebre el màxim de canals. Per obtenir els millors resultats, col·loca-la en un lloc elevat i a prop d\'una finestra." "Verifica que el sintonitzador USB estigui endollat i connectat a una font de senyal de televisió.\n\nSi fas servir una antena aèria, pot ser que calgui ajustar-ne la ubicació o la direcció per rebre el màxim de canals. Per obtenir els millors resultats, col·loca-la en un lloc elevat i a prop d\'una finestra." + "Comprova que el sintonitzador en xarxa estigui engegat i connectat a una font de senyal de televisió.\n\nSi fas servir una antena aèria, pot ser que calgui ajustar-ne la ubicació o la direcció per rebre el màxim de canals. Per obtenir uns resultats millors, col·loca-la en un lloc elevat i a prop d\'una finestra." "Continua" "Ara no" @@ -40,6 +38,7 @@ "Vols tornar a executar la configuració de canals?" "Això farà que se suprimeixin del sintonitzador de televisió els canals que s\'han trobat i que es tornin a cercar canals nous.\n\nVerifica que el teu televisor estigui connectat a una font de senyal de televisió.\n\nSi fas servir una antena aèria, pot ser que calgui ajustar-ne la ubicació o la direcció per rebre el màxim de canals. Per obtenir els millors resultats, col·loca-la en un lloc elevat i a prop d\'una finestra." "Això farà que se suprimeixin del sintonitzador USB els canals que s\'han trobat i que es tornin a cercar canals nous.\n\nVerifica que el sintonitzador USB estigui endollat i connectat a una font de senyal de televisió.\n\nSi fas servir una antena aèria, pot ser que calgui ajustar-ne la ubicació o la direcció per rebre el màxim de canals. Per obtenir els millors resultats, col·loca-la en un lloc elevat i a prop d\'una finestra." + "Això farà que se suprimeixin del sintonitzador en xarxa els canals trobats i que se\'n tornin a cercar de nous.\n\nComprova que el sintonitzador en xarxa estigui engegat i connectat a una font de senyal de televisió.\n\nSi fas servir una antena aèria, pot ser que calgui ajustar-ne la ubicació o la direcció per rebre el màxim de canals. Per obtenir uns resultats millors, col·loca-la en un lloc elevat i a prop d\'una finestra." "Continua" "Cancel·la" @@ -54,6 +53,7 @@ "Configuració del sintonitzador de televisió" "Configuració del sintonitzador de canals USB" + "Configuració del sintonitzador de canals en xarxa" "Aquesta acció pot tardar uns quants minuts" "El sintonitzador no està disponible en aquest moment o bé ja s\'està utilitzant en un enregistrament." @@ -76,6 +76,7 @@ "No s\'ha trobat cap canal" "No s\'ha trobat cap canal. Verifica que el teu televisor estigui connectat a una font de senyal de televisió.\n\nSi fas servir una antena aèria, ajusta\'n la ubicació o la direcció. Per obtenir els millors resultats, col·loca-la en un lloc elevat i a prop d\'una finestra i torna a cercar canals." "La cerca no ha trobat cap canal. Verifica que el sintonitzador USB estigui endollat i connectat a una font de senyal de televisió.\n\nSi fas servir una antena aèria, ajusta\'n la ubicació o la direcció. Per obtenir els millors resultats, col·loca-la en un lloc elevat i a prop d\'una finestra i torna a cercar." + "No s\'ha trobat cap canal. Comprova que el sintonitzador en xarxa estigui engegat i connectat a una font de senyal de televisió.\n\nSi fas servir una antena aèria, ajusta\'n la ubicació o la direcció. Per obtenir uns resultats millors, col·loca-la en un lloc elevat i a prop d\'una finestra i torna a cercar." "Torna a cercar" "Fet" @@ -83,5 +84,7 @@ "Cerca canals de televisió" "Configuració del sintonitzador de televisió" "Configuració del sintonitzador de televisió USB" - "El sintonitzador de televisió USB no està connectat." + "Configuració del sintonitzador de televisió en xarxa" + "El sintonitzador de televisió USB no està connectat." + "El sintonitzador de la xarxa no està connectat." diff --git a/usbtuner-res/values-cs/strings.xml b/usbtuner-res/values-cs/strings.xml index 151083c6..bee1e47c 100644 --- a/usbtuner-res/values-cs/strings.xml +++ b/usbtuner-res/values-cs/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "Televizní tuner" "Televizní tuner USB" - "Zapnout" - "Vypnout" + "Síťový televizní tuner (BETA)" "Vyčkejte prosím, než bude zpracování dokončeno" - "Vyberte zdroj kanálu" - "Žádný signál" - "Kanál %s se nepodařilo naladit" - "Nelze naladit" "Software tuneru byl nedávno aktualizován. Vyhledejte prosím kanály znovu." "Chcete-li zapnout zvuk, v nastavení systémového zvuku povolte prostorový zvuk" + "Zvuk nelze přehrát. Zkuste použít jinou televizi." "Nastavení tuneru kanálů" "Nastavení televizního tuneru" "Nastavení tuneru kanálů USB" + "Nastavení síťového tuneru" "Zkontrolujte, zda je televize připojena ke zdroji televizního signálu.\n\nPokud používáte bezdrátovou anténu, možná budete muset za účelem příjmu co největšího počtu kanálů upravit její umístění nebo nasměrování. Nejlepších výsledků dosáhnete, pokud ji umístíte vysoko a blízko okna." "Zkontrolujte, zda je tuner USB připojen k zařízení a ke zdroji televizního signálu.\n\nPokud používáte bezdrátovou anténu, možná budete muset za účelem příjmu co největšího počtu kanálů upravit její umístění nebo nasměrování. Nejlepších výsledků dosáhnete, pokud ji umístíte vysoko a blízko okna." + "Zkontrolujte, zda je síťový tuner zapnut a připojen ke zdroji televizního signálu.\n\nPokud používáte bezdrátovou anténu, možná budete muset za účelem příjmu co největšího počtu kanálů upravit její umístění nebo nasměrování. Nejlepších výsledků dosáhnete, pokud ji umístíte vysoko a blízko okna." "Pokračovat" "Teď ne" @@ -40,6 +38,7 @@ "Znovu spustit nastavení kanálů?" "Tímto odstraníte kanály nalezené pomocí televizního tuneru a znovu vyhledáte nové kanály.\n\nZkontrolujte, zda je televize připojena ke zdroji televizního signálu.\n\nPokud používáte bezdrátovou anténu, možná budete muset za účelem příjmu co největšího počtu kanálů upravit její umístění nebo nasměrování. Nejlepších výsledků dosáhnete, pokud ji umístíte vysoko a blízko okna." "Tímto odstraníte kanály nalezené pomocí tuneru USB a znovu vyhledáte nové kanály.\n\nZkontrolujte, zda je tuner USB připojen k zařízení a ke zdroji televizního signálu.\n\nPokud používáte bezdrátovou anténu, možná budete muset za účelem příjmu co největšího počtu kanálů upravit její umístění nebo nasměrování. Nejlepších výsledků dosáhnete, pokud ji umístíte vysoko a blízko okna." + "Tímto odstraníte kanály nalezené pomocí síťového tuneru a znovu vyhledáte nové kanály.\n\nZkontrolujte, zda je síťový tuner zapnut a připojen ke zdroji televizního signálu.\n\nPokud používáte bezdrátovou anténu, možná budete muset za účelem příjmu co největšího počtu kanálů upravit její umístění nebo nasměrování. Nejlepších výsledků dosáhnete, pokud ji umístíte vysoko a blízko okna." "Pokračovat" "Zrušit" @@ -54,6 +53,7 @@ "Nastavení televizního tuneru" "Nastavení tuneru kanálů USB" + "Nastavení kanálů síťového tuneru" "Tato akce může trvat několik minut." "Tuner dočasně není k dispozici, případně je právě používán k nahrávání." @@ -82,6 +82,7 @@ "Nebyly nalezeny žádné kanály" "Při vyhledávání nebyly nalezeny žádné kanály. Zkontrolujte, zda je televize připojena ke zdroji televizního signálu.\n\nPokud používáte bezdrátovou anténu, upravte její umístění nebo nasměrování. Nejlepších výsledků dosáhnete, pokud ji umístíte vysoko a blízko okna. Poté spusťte vyhledávání znovu." "Při vyhledávání nebyly nalezeny žádné kanály. Zkontrolujte, zda je tuner USB připojen k zařízení a ke zdroji televizního signálu.\n\nPokud používáte bezdrátovou anténu, upravte její umístění nebo nasměrování. Nejlepších výsledků dosáhnete, pokud ji umístíte vysoko a blízko okna. Poté spusťte vyhledávání znovu." + "Při vyhledávání nebyly nalezeny žádné kanály. Zkontrolujte, zda je síťový tuner zapnut a připojen ke zdroji televizního signálu.\n\nPokud používáte bezdrátovou anténu, upravte její umístění nebo nasměrování. Nejlepších výsledků dosáhnete, pokud ji umístíte vysoko a blízko okna. Poté spusťte vyhledávání znovu." "Znovu vyhledat" "Hotovo" @@ -89,5 +90,7 @@ "Vyhledejte televizní kanály" "Nastavení televizního tuneru" "Nastavení televizního tuneru USB" - "Televizní tuner USB byl odpojen." + "Nastavení síťového televizního tuneru" + "Televizní tuner USB byl odpojen." + "Síťový tuner byl odpojen." diff --git a/usbtuner-res/values-da/strings.xml b/usbtuner-res/values-da/strings.xml index cea5d3c3..7434c481 100644 --- a/usbtuner-res/values-da/strings.xml +++ b/usbtuner-res/values-da/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "TV Tuner" "USB-tuner til fjernsynet" - "Til" - "Fra" + "Netværkstuner til tv (beta)" "Vent, mens behandlingen afsluttes" - "Vælg din kanalkilde" - "Intet signal" - "%s kunne ikke indlæses" - "Indlæsningen lykkedes ikke" "Tunerens software er blevet opdateret for nylig. Scan efter kanalerne igen." "Aktivér surroundsound i systemets lydindstillinger for at aktivere lyd" + "Der kan ikke afspilles lyd. Prøv på et andet fjernsyn" "Konfiguration af kanaltuneren" "Konfiguration med TV Tuner" "Konfiguration af USB-kanaltuneren" + "Konfiguration af netværkstuner" "Kontrollér, at dit fjernsyn er forbundet til en tv-signalkilde.\n\nHvis du bruger en luftantenne, kan det være nødvendigt at justere dens position eller retning for at modtage flest muligt kanaler. Du opnår det bedste resultat ved at placere den højt oppe og i nærheden af et vindue." "Kontrollér, at USB-tuneren er tilsluttet og forbundet til en tv-signalkilde.\n\nHvis du bruger en luftantenne, kan det være nødvendigt at justere dens position eller retning for at modtage flest muligt kanaler. Du opnår det bedste resultat ved at placere den højt oppe og i nærheden af et vindue." + "Kontrollér, at netværkstuneren er tændt og sluttet til en tv-signalkilde.\n\nHvis du bruger en luftantenne, kan det være nødvendigt at justere dens position eller retning for at modtage flest muligt kanaler. Du opnår det bedste resultat ved at placere den højt oppe og i nærheden af et vindue." "Fortsæt" "Ikke nu" @@ -40,6 +38,7 @@ "Vil du gentage kanalkonfigurationen?" "Dette fjerner de kanaler, som blev fundet, fra fjernsynets tuner og scanner efter nye kanaler igen.\n\nKontrollér, at dit fjernsyn er forbundet til en tv-signalkilde.\n\nHvis du bruger en luftantenne, kan det være nødvendigt at justere dens position eller retning for at modtage flest muligt kanaler. Du opnår det bedste resultat ved at placere den højt oppe og i nærheden af et vindue." "Denne handling fjerner de kanaler, der blev fundet af USB-tuneren, og starter en ny scanning efter kanaler.\n\nKontrollér, at USB-tuneren er tilsluttet og forbundet til en tv-signalkilde.\n\nHvis du bruger en luftantenne, kan det være nødvendigt at justere dens position eller retning for at modtage flest muligt kanaler. Du opnår det bedste resultat ved at placere den højt oppe og i nærheden af et vindue." + "Denne handling fjerner de kanaler, der blev fundet af netværkstuneren, og starter en ny kanalsøgning.\n\nKontrollér, at netværkstuneren er tændt og sluttet til en tv-signalkilde.\n\nHvis du bruger en luftantenne, kan det være nødvendigt at justere dens position eller retning for at modtage flest muligt kanaler. Du opnår det bedste resultat ved at placere den højt oppe og i nærheden af et vindue." "Fortsæt" "Annuller" @@ -54,6 +53,7 @@ "Konfiguration med fjernsynets tuner" "Konfiguration af USB-kanaltuneren" + "Konfiguration af netværkstuner til kanalsøgning" "Dette kan tage flere minutter" "Tuneren er midlertidigt utilgængelig eller benyttes allerede til en optagelse." @@ -76,6 +76,7 @@ "Der blev ikke fundet nogen kanaler" "Der blev ikke fundet nogen kanaler under scanningen. Kontrollér, at dit fjernsyn er forbundet til en tv-signalkilde.\n\nHvis du bruger en luftantenne, skal du justere dens position eller retning. Du opnår det bedste resultat ved at placere den højt oppe og i nærheden af et vindue. Scan igen." "Der blev ikke fundet nogen kanaler under scanningen. Kontrollér, at USB-tuneren er tilsluttet og forbundet til en tv-signalkilde.\n\nHvis du bruger en luftantenne, skal du justere dens position eller retning. Du opnår det bedste resultat ved at placere den højt oppe og i nærheden af et vindue. Scan igen." + "Der blev ikke fundet nogen kanaler under søgningen. Kontrollér, at netværkstuneren er tændt og sluttet til en tv-signalkilde.\n\nHvis du bruger en luftantenne, skal du justere dens position eller retning. Du opnår det bedste resultat ved at placere den højt oppe og i nærheden af et vindue. Søg igen." "Scan igen" "Udført" @@ -83,5 +84,7 @@ "Scan efter tv-kanaler" "Konfiguration med TV Tuner" "Konfiguration af USB-TV Tuner" - "USB-tuneren til fjernsynet af frakoblet." + "Konfiguration af netværkstuner til tv" + "USB-tuneren til fjernsynet er ikke tilsluttet." + "Netværkstuneren er ikke tilsluttet." diff --git a/usbtuner-res/values-de/strings.xml b/usbtuner-res/values-de/strings.xml index eab5fb1d..8f5d84ac 100644 --- a/usbtuner-res/values-de/strings.xml +++ b/usbtuner-res/values-de/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "TV-Tuner" "USB-TV-Tuner" - "An" - "Aus" + "Netzwerk-TV-Tuner (BETA)" "Bitte warten Sie, bis die Verarbeitung abgeschlossen ist" - "Kanalquelle auswählen" - "Kein Signal" - "\"%s\" konnte nicht eingestellt werden" - "Fehler beim Einstellen" "Die Tunersoftware wurde kürzlich aktualisiert. Bitte führen Sie die Kanalsuche noch einmal durch." "Aktivieren Sie in den Systemeinstellungen Surround-Sound, um Audio einschalten zu können" + "Audio kann nicht wiedergegeben werden. Bitte versuch es mit einem anderen Fernseher." "Kanaleinrichtung über den Tuner" "TV-Tuner einrichten" "Kanaleinrichtung über den USB-Tuner" + "Einrichtung des Netzwerk-Tuners" "Vergewissern Sie sich, dass Ihr Fernseher mit einer TV-Signalquelle verbunden ist.\n\nWenn Sie eine terrestrische Antenne verwenden, ändern Sie die Position oder Ausrichtung, um mehr Kanäle zu finden. Die besten Ergebnisse erhalten Sie, wenn Sie sie an eine erhöhte Position in Fensternähe stellen." "Vergewissern Sie sich, dass der USB-Tuner angeschlossen und mit einer TV-Signalquelle verbunden ist.\n\nWenn Sie eine terrestrische Antenne verwenden, ändern Sie die Position oder Ausrichtung, um mehr Kanäle zu finden. Die besten Ergebnisse erhalten Sie, wenn Sie sie an einer erhöhten Position in Fensternähe stellen." + "Vergewissere dich, dass der Netzwerk-Tuner eingeschaltet und mit einer TV-Signalquelle verbunden ist.\n\nWenn du eine terrestrische Antenne verwendest, ändere die Position oder Ausrichtung, um mehr Kanäle zu empfangen. Die besten Ergebnisse erhältst du, wenn du die Antenne an eine erhöhte Position in Fensternähe stellst." "Weiter" "Jetzt nicht" @@ -40,6 +38,7 @@ "Kanaleinrichtung erneut durchführen?" "Dies entfernt die vom TV-Tuner gefundenen Kanäle und sucht noch einmal nach neuen Kanälen.\n\nVergewissern Sie sich, dass Ihr Fernseher mit einer TV-Signalquelle verbunden ist.\n\nWenn Sie eine terrestrische Antenne verwenden, ändern Sie die Position oder Ausrichtung, um mehr Kanäle zu finden. Die besten Ergebnisse erhalten Sie, wenn Sie sie an eine erhöhte Position in Fensternähe stellen." "Durch diese Aktion werden die gefundenen Kanäle vom USB-Tuner entfernt und die Kanalsuche wird erneut gestartet.\n\nVergewissern Sie sich, dass der USB-Tuner angeschlossen und mit einer TV-Signalquelle verbunden ist.\n\nWenn Sie eine terrestrische Antenne verwenden, ändern Sie die Position oder Ausrichtung, um mehr Kanäle zu finden. Die besten Ergebnisse erhalten Sie, wenn Sie sie an einer erhöhten Position in Fensternähe stellen." + "Durch diese Aktion werden die vom Netzwerk-Tuner gefundenen Kanäle entfernt und die Kanalsuche wird neu gestartet.\n\nVergewissere dich, dass der Netzwerk-Tuner eingeschaltet und mit einer TV-Signalquelle verbunden ist.\n\nWenn du eine terrestrische Antenne verwendest, ändere die Position oder Ausrichtung, um mehr Kanäle zu empfangen. Die besten Ergebnisse erhältst du, wenn du die Antenne an eine erhöhte Position in Fensternähe stellst." "Weiter" "Abbrechen" @@ -54,6 +53,7 @@ "TV-Tuner einrichten" "Kanaleinrichtung über den USB-Tuner" + "Kanaleinrichtung über den Netzwerk-Tuner" "Dies kann einige Minuten dauern" "Der Tuner ist vorübergehend nicht verfügbar oder wird schon für eine Aufnahme verwendet." @@ -76,6 +76,7 @@ "Keine Kanäle gefunden" "Bei der Suche wurden keine Kanäle gefunden. Vergewissern Sie sich, dass Ihr Fernseher mit einer TV-Signalquelle verbunden ist.\n\nWenn Sie eine terrestrische Antenne verwenden, ändern Sie die Position oder Ausrichtung. Um die besten Ergebnisse zu erhalten, stellen Sie sie an eine erhöhte Position in Fensternähe und führen Sie die Suche noch einmal durch." "Bei der Suche wurden keine Kanäle gefunden. Vergewissern Sie sich, dass der USB-Tuner angeschlossen und mit einer TV-Signalquelle verbunden ist.\n\nWenn Sie eine terrestrische Antenne verwenden, ändern Sie die Position oder Ausrichtung. Die besten Ergebnisse erhalten Sie, wenn Sie sie an einer erhöhten Position in Fensternähe stellen. Führen Sie dann die Suche erneut durch." + "Bei der Suche wurden keine Kanäle gefunden. Vergewissere dich, dass der Netzwerk-Tuner eingeschaltet und mit einer TV-Signalquelle verbunden ist.\n\nWenn du eine terrestrische Antenne verwendest, ändere die Position oder Ausrichtung. Die besten Ergebnisse erhältst du, wenn du sie an eine erhöhte Position in Fensternähe stellst und die Suche noch einmal durchführst." "Noch einmal suchen" "Fertig" @@ -83,5 +84,7 @@ "Nach TV-Kanälen suchen" "TV-Tuner einrichten" "USB-TV-Tuner einrichten" - "Verbindung zum USB-TV-Empfänger wurde aufgehoben." + "Einrichtung des Netzwerk-TV-Tuners" + "Verbindung zum USB-TV-Tuner wurde aufgehoben." + "Verbindung zum Netzwerk-Tuner wurde aufgehoben." diff --git a/usbtuner-res/values-el/strings.xml b/usbtuner-res/values-el/strings.xml index 6a033e3c..86f4cb85 100644 --- a/usbtuner-res/values-el/strings.xml +++ b/usbtuner-res/values-el/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "Δέκτης τηλεόρασης" "Δέκτης τηλεόρασης USB" - "Ενεργό" - "Ανενεργό" + "Δέκτης τηλεόρασης δικτύου (BETA)" "Περιμένετε να ολοκληρωθεί η επεξεργασία" - "Επιλέξτε την πηγή του καναλιού σας" - "Χωρίς σήμα" - "Αποτυχία συντονισμού %s" - "Αποτυχία συντονισμού" "Το λογισμικό δέκτη ενημερώθηκε πρόσφατα. Επαναλάβετε τη σάρωση των καναλιών." "Ενεργοποιήστε τον περιφερειακό ήχο στις ρυθμίσεις ήχου συστήματος για να ενεργοποιήσετε τον ήχο" + "Δεν είναι δυνατή η αναπαραγωγή ήχου. Δοκιμάστε μια άλλη τηλεόραση." "Ρύθμιση δέκτη καναλιών" "Ρύθμιση δέκτη τηλεόρασης" "Ρύθμιση δέκτη καναλιών USB" + "Ρύθμιση δέκτη δικτύου" "Βεβαιωθείτε ότι η τηλεόρασή σας είναι συνδεδεμένη σε μια πηγή τηλεοπτικού σήματος.\n\nΕάν χρησιμοποιείτε ασύρματη κεραία, ίσως χρειαστεί να προσαρμόσετε την τοποθέτηση ή την κατεύθυνσή της για να λάβετε περισσότερα κανάλια. Για καλύτερα αποτελέσματα, τοποθετήστε την ψηλά και κοντά σε ένα παράθυρο." "Βεβαιωθείτε ότι ο δέκτης USB είναι συνδεδεμένος στην πρίζα και σε μια πηγή τηλεοπτικού σήματος.\n\nΕάν χρησιμοποιείτε ασύρματη κεραία, ίσως χρειαστεί να προσαρμόσετε την τοποθέτηση ή την κατεύθυνσή της για να λάβετε περισσότερα κανάλια. Για καλύτερα αποτελέσματα, τοποθετήστε την ψηλά και κοντά σε ένα παράθυρο." + "Βεβαιωθείτε ότι ο δέκτης του δικτύου είναι ενεργοποιημένος και συνδεδεμένος σε μια πηγή τηλεοπτικού σήματος.\n\nΕάν χρησιμοποιείτε ασύρματη κεραία, ίσως χρειαστεί να προσαρμόσετε την τοποθέτηση ή την κατεύθυνσή της για να λάβετε περισσότερα κανάλια. Για καλύτερα αποτελέσματα, τοποθετήστε την ψηλά και κοντά σε ένα παράθυρο." "Συνέχεια" "Όχι τώρα" @@ -40,6 +38,7 @@ "Επανάληψη ρύθμισης καναλιών;" "Με αυτόν τον τρόπο θα καταργηθούν τα κανάλια που βρέθηκαν από τον δέκτη τηλεόρασης και θα γίνει ξανά σάρωση για νέα κανάλια.\n\nΒεβαιωθείτε ότι η τηλεόρασή σας είναι συνδεδεμένη σε μια πηγή τηλεοπτικού σήματος.\n\nΕάν χρησιμοποιείτε ασύρματη κεραία, ίσως χρειαστεί να προσαρμόσετε την τοποθέτηση ή την κατεύθυνσή της για να λάβετε περισσότερα κανάλια. Για καλύτερα αποτελέσματα, τοποθετήστε την ψηλά και κοντά σε ένα παράθυρο." "Με αυτόν τον τρόπο θα καταργηθούν τα κανάλια που βρέθηκαν από τον δέκτη USB και θα γίνει ξανά σάρωση για νέα κανάλια.\n\nΒεβαιωθείτε ότι ο δέκτης USB είναι συνδεδεμένος στην πρίζα και σε μια πηγή τηλεοπτικού σήματος.\n\nΕάν χρησιμοποιείτε ασύρματη κεραία, ίσως χρειαστεί να προσαρμόσετε την τοποθέτηση ή την κατεύθυνσή της για να λάβετε περισσότερα κανάλια. Για καλύτερα αποτελέσματα, τοποθετήστε την ψηλά και κοντά σε ένα παράθυρο." + "Με αυτόν τον τρόπο, τα κανάλια που βρέθηκαν από τον δέκτη του δικτύου θα καταργηθούν και θα γίνει ξανά σάρωση για νέα κανάλια.\n\nΒεβαιωθείτε ότι ο δέκτης του δικτύου είναι ενεργοποιημένος και συνδεδεμένος σε μια πηγή τηλεοπτικού σήματος.\n\nΕάν χρησιμοποιείτε ασύρματη κεραία, ίσως χρειαστεί να προσαρμόσετε την τοποθέτηση ή την κατεύθυνσή της για να λάβετε περισσότερα κανάλια. Για καλύτερα αποτελέσματα, τοποθετήστε την ψηλά και κοντά σε ένα παράθυρο." "Συνέχεια" "Ακύρωση" @@ -54,6 +53,7 @@ "Ρύθμιση δέκτη τηλεόρασης" "Ρύθμιση δέκτη καναλιών USB" + "Ρύθμιση δέκτη καναλιών δικτύου" "Αυτό μπορεί να διαρκέσει αρκετά λεπτά" "Ο δέκτης δεν είναι διαθέσιμος προσωρινά ή χρησιμοποιείται ήδη από την εγγραφή." @@ -76,6 +76,7 @@ "Δεν βρέθηκαν κανάλια" "Δεν βρέθηκαν κανάλια κατά τη σάρωση. Βεβαιωθείτε ότι η τηλεόρασή σας είναι συνδεδεμένη σε μια πηγή τηλεοπτικού σήματος.\n\nΕάν χρησιμοποιείτε ασύρματη κεραία, προσαρμόστε την τοποθέτηση ή την κατεύθυνσή της. Για καλύτερα αποτελέσματα, τοποθετήστε την ψηλά και κοντά σε ένα παράθυρο και επαναλάβετε τη σάρωση." "Δεν βρέθηκαν κανάλια κατά τη σάρωση. Βεβαιωθείτε ότι ο δέκτης USB είναι συνδεδεμένος στην πρίζα και σε μια πηγή τηλεοπτικού σήματος.\n\nΕάν χρησιμοποιείτε ασύρματη κεραία, προσαρμόστε την τοποθέτηση ή την κατεύθυνσή της. Για καλύτερα αποτελέσματα, τοποθετήστε την ψηλά και κοντά σε ένα παράθυρο και επαναλάβετε τη σάρωση." + "Δεν εντοπίστηκε κανένα κανάλι κατά τη σάρωση. Βεβαιωθείτε ότι ο δέκτης του δικτύου είναι ενεργοποιημένος και συνδεδεμένος σε μια πηγή τηλεοπτικού σήματος.\n\nΕάν χρησιμοποιείτε ασύρματη κεραία, προσαρμόστε την τοποθέτηση ή την κατεύθυνσή της. Για καλύτερα αποτελέσματα, τοποθετήστε την ψηλά και κοντά σε κάποιο παράθυρο και επαναλάβετε τη σάρωση." "Εκ νέου σάρωση" "Ολοκληρώθηκε" @@ -83,5 +84,7 @@ "Σάρωση για τηλεοπτικά κανάλια" "Ρύθμιση δέκτη τηλεόρασης" "Ρύθμιση δέκτη τηλεόρασης USB" - "Ο δέκτης τηλεόρασης USB έχει αποσυνδεθεί." + "Ρύθμιση δέκτη τηλεόρασης δικτύου" + "Ο δέκτης τηλεόρασης USB έχει αποσυνδεθεί." + "Ο δέκτης δικτύου έχει αποσυνδεθεί." diff --git a/usbtuner-res/values-en-rAU/strings.xml b/usbtuner-res/values-en-rAU/strings.xml index 11e639fd..3e3d8d3c 100644 --- a/usbtuner-res/values-en-rAU/strings.xml +++ b/usbtuner-res/values-en-rAU/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "TV Tuner" "USB TV Tuner" - "On" - "Off" + "Network TV Tuner (BETA)" "Please wait to finish processing" - "Select your channel source" - "No Signal" - "Failed to tune to %s" - "Failed to tune" "Tuner software has been recently updated. Please re-scan the channels." "To enable audio, enable surround sound in system sound settings" + "Cannot play audio. Please try another TV" "Channel tuner setup" "TV Tuner setup" "USB channel tuner setup" + "Network Tuner Setup" "Check that your TV is connected to a TV signal source.\n\nIf using an over-the-air aerial, you may need to adjust its placement or direction to receive the most channels. For best results, place it high and near a window." "Check that the USB tuner is plugged in and connected to a TV signal source.\n\nIf using an over-the-air aerial, you may need to adjust its placement or direction to receive the most channels. For best results, place it high and near a window." + "Verify the network tuner is powered on and connected to a TV signal source.\n\nIf using an over-the-air antenna, you may need to adjust its placement or direction to receive the most channels. For best results, place it high and near a window." "Continue" "Not now" @@ -40,6 +38,7 @@ "Re-run channel setup?" "This will remove the channels found from the TV tuner and scan for new channels again.\n\nCheck that your TV is connected to a TV signal source.\n\nIf using an over-the-air aerial, you may need to adjust its placement or direction to receive the most channels. For best results, place it high and near a window." "This will remove the channels found from the USB tuner and scan for new channels again.\n\nCheck that the USB tuner is plugged in and connected to a TV signal source.\n\nIf using an over-the-air antenna, you may need to adjust its placement or direction to receive the most channels. For best results, place it high and near a window." + "This will remove the channels found from the network tuner and scan for new channels again.\n\nVerify the network tuner is powered on and connected to a TV signal source.\n\nIf using an over-the-air antenna, you may need to adjust its placement or direction to receive the most channels. For best results, place it high and near a window." "Continue" "Cancel" @@ -54,6 +53,7 @@ "TV tuner setup" "USB channel tuner setup" + "Network channel tuner setup" "This may take several minutes" "Tuner is temporarily unavailable or already used by recording." @@ -76,6 +76,7 @@ "No channels found" "The scan did not find any channels. Check that your TV is connected to a TV signal source.\n\nIf using an over-the-air aerial, adjust its placement or direction. For best results, place it high and near a window and scan again." "The scan did not find any channels. Check that the USB tuner is plugged in and connected to a TV signal source.\n\nIf using an over-the-air aerial, adjust its placement or direction. For best results, place it high and near a window and scan again." + "The scan did not find any channels. Verify the network tuner is powered on and connected to a TV signal source.\n\nIf using an over-the-air antenna, adjust its placement or direction. For best results, place it high and near a window and scan again." "Scan again" "Finished" @@ -83,5 +84,7 @@ "Scan for TV channels" "TV Tuner setup" "USB TV Tuner setup" - "USB TV tuner disconnected." + "Network TV Tuner setup" + "USB TV tuner disconnected." + "Network tuner disconnected." diff --git a/usbtuner-res/values-en-rGB/strings.xml b/usbtuner-res/values-en-rGB/strings.xml index 11e639fd..3e3d8d3c 100644 --- a/usbtuner-res/values-en-rGB/strings.xml +++ b/usbtuner-res/values-en-rGB/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "TV Tuner" "USB TV Tuner" - "On" - "Off" + "Network TV Tuner (BETA)" "Please wait to finish processing" - "Select your channel source" - "No Signal" - "Failed to tune to %s" - "Failed to tune" "Tuner software has been recently updated. Please re-scan the channels." "To enable audio, enable surround sound in system sound settings" + "Cannot play audio. Please try another TV" "Channel tuner setup" "TV Tuner setup" "USB channel tuner setup" + "Network Tuner Setup" "Check that your TV is connected to a TV signal source.\n\nIf using an over-the-air aerial, you may need to adjust its placement or direction to receive the most channels. For best results, place it high and near a window." "Check that the USB tuner is plugged in and connected to a TV signal source.\n\nIf using an over-the-air aerial, you may need to adjust its placement or direction to receive the most channels. For best results, place it high and near a window." + "Verify the network tuner is powered on and connected to a TV signal source.\n\nIf using an over-the-air antenna, you may need to adjust its placement or direction to receive the most channels. For best results, place it high and near a window." "Continue" "Not now" @@ -40,6 +38,7 @@ "Re-run channel setup?" "This will remove the channels found from the TV tuner and scan for new channels again.\n\nCheck that your TV is connected to a TV signal source.\n\nIf using an over-the-air aerial, you may need to adjust its placement or direction to receive the most channels. For best results, place it high and near a window." "This will remove the channels found from the USB tuner and scan for new channels again.\n\nCheck that the USB tuner is plugged in and connected to a TV signal source.\n\nIf using an over-the-air antenna, you may need to adjust its placement or direction to receive the most channels. For best results, place it high and near a window." + "This will remove the channels found from the network tuner and scan for new channels again.\n\nVerify the network tuner is powered on and connected to a TV signal source.\n\nIf using an over-the-air antenna, you may need to adjust its placement or direction to receive the most channels. For best results, place it high and near a window." "Continue" "Cancel" @@ -54,6 +53,7 @@ "TV tuner setup" "USB channel tuner setup" + "Network channel tuner setup" "This may take several minutes" "Tuner is temporarily unavailable or already used by recording." @@ -76,6 +76,7 @@ "No channels found" "The scan did not find any channels. Check that your TV is connected to a TV signal source.\n\nIf using an over-the-air aerial, adjust its placement or direction. For best results, place it high and near a window and scan again." "The scan did not find any channels. Check that the USB tuner is plugged in and connected to a TV signal source.\n\nIf using an over-the-air aerial, adjust its placement or direction. For best results, place it high and near a window and scan again." + "The scan did not find any channels. Verify the network tuner is powered on and connected to a TV signal source.\n\nIf using an over-the-air antenna, adjust its placement or direction. For best results, place it high and near a window and scan again." "Scan again" "Finished" @@ -83,5 +84,7 @@ "Scan for TV channels" "TV Tuner setup" "USB TV Tuner setup" - "USB TV tuner disconnected." + "Network TV Tuner setup" + "USB TV tuner disconnected." + "Network tuner disconnected." diff --git a/usbtuner-res/values-en-rIN/strings.xml b/usbtuner-res/values-en-rIN/strings.xml index 11e639fd..3e3d8d3c 100644 --- a/usbtuner-res/values-en-rIN/strings.xml +++ b/usbtuner-res/values-en-rIN/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "TV Tuner" "USB TV Tuner" - "On" - "Off" + "Network TV Tuner (BETA)" "Please wait to finish processing" - "Select your channel source" - "No Signal" - "Failed to tune to %s" - "Failed to tune" "Tuner software has been recently updated. Please re-scan the channels." "To enable audio, enable surround sound in system sound settings" + "Cannot play audio. Please try another TV" "Channel tuner setup" "TV Tuner setup" "USB channel tuner setup" + "Network Tuner Setup" "Check that your TV is connected to a TV signal source.\n\nIf using an over-the-air aerial, you may need to adjust its placement or direction to receive the most channels. For best results, place it high and near a window." "Check that the USB tuner is plugged in and connected to a TV signal source.\n\nIf using an over-the-air aerial, you may need to adjust its placement or direction to receive the most channels. For best results, place it high and near a window." + "Verify the network tuner is powered on and connected to a TV signal source.\n\nIf using an over-the-air antenna, you may need to adjust its placement or direction to receive the most channels. For best results, place it high and near a window." "Continue" "Not now" @@ -40,6 +38,7 @@ "Re-run channel setup?" "This will remove the channels found from the TV tuner and scan for new channels again.\n\nCheck that your TV is connected to a TV signal source.\n\nIf using an over-the-air aerial, you may need to adjust its placement or direction to receive the most channels. For best results, place it high and near a window." "This will remove the channels found from the USB tuner and scan for new channels again.\n\nCheck that the USB tuner is plugged in and connected to a TV signal source.\n\nIf using an over-the-air antenna, you may need to adjust its placement or direction to receive the most channels. For best results, place it high and near a window." + "This will remove the channels found from the network tuner and scan for new channels again.\n\nVerify the network tuner is powered on and connected to a TV signal source.\n\nIf using an over-the-air antenna, you may need to adjust its placement or direction to receive the most channels. For best results, place it high and near a window." "Continue" "Cancel" @@ -54,6 +53,7 @@ "TV tuner setup" "USB channel tuner setup" + "Network channel tuner setup" "This may take several minutes" "Tuner is temporarily unavailable or already used by recording." @@ -76,6 +76,7 @@ "No channels found" "The scan did not find any channels. Check that your TV is connected to a TV signal source.\n\nIf using an over-the-air aerial, adjust its placement or direction. For best results, place it high and near a window and scan again." "The scan did not find any channels. Check that the USB tuner is plugged in and connected to a TV signal source.\n\nIf using an over-the-air aerial, adjust its placement or direction. For best results, place it high and near a window and scan again." + "The scan did not find any channels. Verify the network tuner is powered on and connected to a TV signal source.\n\nIf using an over-the-air antenna, adjust its placement or direction. For best results, place it high and near a window and scan again." "Scan again" "Finished" @@ -83,5 +84,7 @@ "Scan for TV channels" "TV Tuner setup" "USB TV Tuner setup" - "USB TV tuner disconnected." + "Network TV Tuner setup" + "USB TV tuner disconnected." + "Network tuner disconnected." diff --git a/usbtuner-res/values-es-rUS/strings.xml b/usbtuner-res/values-es-rUS/strings.xml index da7ec699..23fda60a 100644 --- a/usbtuner-res/values-es-rUS/strings.xml +++ b/usbtuner-res/values-es-rUS/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "Sintonizador de TV" "Sintonizador de TV USB" - "Activar" - "Desactivar" + "Sintonizador de TV de red (BETA)" "Espera a que finalice el procesamiento" - "Seleccionar la fuente del canal" - "Sin señal" - "No se pudo sintonizar el canal %s" - "No se pudo sintonizar el canal" "El software del sintonizador se actualizó recientemente. Vuelve a buscar los canales." "Para habilitar el audio, deberás activar el sonido envolvente en la configuración del sistema de sonido" + "No se puede reproducir el audio. Intenta usar otra TV." "Configuración del sintonizador de canales" "Configuración del sintonizador de TV" "Configuración del sintonizador de canales USB" + "Configuración del sintonizador de red" "Verifica que tu TV esté conectada a una fuente de señal de TV.\n\nSi usas una antena inalámbrica, es posible que tengas que ajustar su posición o dirección para recibir la mayor cantidad de canales posible. Para obtener mejores resultados, ubica la antena en un lugar alto y cerca de una ventana." "Verifica que el sintonizador USB esté enchufado y conectado a una fuente de señal de TV.\n\nSi usas una antena inalámbrica, es posible que necesites ajustar su ubicación y dirección para recibir la mayor cantidad de canales. Para obtener mejores resultados, ubica la antena en un lugar alto y cerca de una ventana." + "Comprueba que el sintonizador de red esté encendido y conectado a una fuente de señal de TV.\n\nSi estás usando una antena inalámbrica, es posible que debas ajustar su ubicación o dirección para recibir la mayoría de los canales. Si deseas obtener mejores resultados, colócala en un lugar alto y cerca de una ventana." "Continuar" "Ahora no" @@ -40,6 +38,7 @@ "¿Quieres volver a configurar los canales?" "Esta acción quitará los canales encontrados desde el sintonizador de TV y se volverán a buscar canales nuevos.\n\nVerifica que tu TV esté conectada a una fuente de señal de TV.\n\nSi usas una antena inalámbrica, es posible que tengas que ajustar su posición o dirección para recibir la mayor cantidad de canales posible. Para obtener mejores resultados, ubica la antena en un lugar alto y cerca de una ventana." "Esta acción quitará los canales encontrados desde el sintonizador de TV y se volverán a buscar canales nuevos.\n\nVerifica que el sintonizador USB esté enchufado y conectado a una fuente de señal de TV.\n\nSi usas una antena inalámbrica, es posible que necesites ajustar su ubicación y dirección para recibir la mayor cantidad de canales. Para obtener mejores resultados, ubica la antena en un lugar alto y cerca de una ventana." + "De esta manera, se quitarán los canales encontrados desde el sintonizador de red y se buscarán canales nuevos.\n\nComprueba que el sintonizador de red esté encendido y conectado a una fuente de señal de TV.\n\nSi estás usando una antena inalámbrica, es posible que debas ajustar su ubicación o dirección para recibir la mayor cantidad de canales. Para obtener mejores resultados, colócala en un lugar alto y cerca de una ventana." "Continuar" "Cancelar" @@ -54,6 +53,7 @@ "Configuración del sintonizador de TV" "Configuración del sintonizador de canales USB" + "Configuración del sintonizador de canales de red" "Este proceso podría demorar varios minutos" "El sintonizador no está disponible en este momento o está grabando." @@ -76,6 +76,7 @@ "No se encontraron canales" "No se encontró ningún canal. Verifica que tu TV esté conectada a una fuente de señal de TV.\n\nSi usas una antena inalámbrica, ajusta su posición o dirección. Para obtener mejores resultados, ubícala en un lugar alto y cerca de una ventana. Luego, vuelve a hacer la búsqueda." "No se encontraron canales en la búsqueda. Verifica que el sintonizador USB esté enchufado y conectado a una fuente de señal de TV.\n\nSi usas una antena inalámbrica, ajusta su ubicación o dirección. Para obtener mejores resultados, ubícala en un lugar alto y cerca de una ventana. Luego, vuelve a hacer la búsqueda." + "No se encontró ningún canal durante la búsqueda. Comprueba que el sintonizador de red esté encendido y conectado a una fuente de señal de TV.\n\nSi estás usando una antena inalámbrica, ajusta su ubicación o dirección. Para obtener mejores resultados, colócala en un lugar alto y cerca de una ventana, y repite la búsqueda." "Volver a buscar" "Listo" @@ -83,5 +84,7 @@ "Busca canales de TV" "Configuración del sintonizador de TV" "Configuración del sintonizador de TV USB" - "Se desconectó el sintonizador de TV USB." + "Configuración del sintonizador de TV de red" + "Se desconectó el sintonizador de TV USB." + "Se desconectó el sintonizador de red." diff --git a/usbtuner-res/values-es/strings.xml b/usbtuner-res/values-es/strings.xml index e8e34cb2..b10d02b8 100644 --- a/usbtuner-res/values-es/strings.xml +++ b/usbtuner-res/values-es/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "Sintonizador de canales" "Sintonizador de canales USB" - "Activar" - "Desactivar" + "Sintonizador de TV en red (BETA)" "Espera hasta que finalice el procesamiento" - "Selecciona la fuente de canales" - "Sin señal" - "No se ha podido sintonizar %s" - "No se podido sintonizar" "El software del sintonizador se ha actualizado recientemente. Vuelve a buscar los canales." "Habilita el sonido envolvente en los ajustes del sistema de sonido para activar el audio" + "No se puede reproducir el audio. Prueba con otra TV" "Configuración del sintonizador de canales" "Configuración del sintonizador de canales" "Configuración del sintonizador de canales USB" + "Configuración del sintonizador en red" "Comprueba que la TV esté conectada a una fuente de señal de TV.\n\n Si utilizas una antena inalámbrica, puede que tengas que cambiar su posición o dirección para recibir la mayor cantidad posible de canales. Para conseguir los mejores resultados, colócala en alto y cerca de una ventana." "Comprueba que el sintonizador USB esté enchufado y conectado a una fuente de señal de TV.\n\nSi utilizas una antena inalámbrica, puede que tengas que cambiar su ubicación o dirección para recibir la mayor cantidad posible de canales. Para conseguir los mejores resultados, colócala en alto y cerca de una ventana." + "Comprueba que el sintonizador en red esté encendido y conectado a una fuente de señal de TV.\n\nSi utilizas una antena inalámbrica, es posible que tengas que cambiar su ubicación o su orientación para ver la mayoría de los canales. Para obtener los mejores resultados, colócala en un lugar alto cerca de una ventana." "Continuar" "Ahora no" @@ -40,6 +38,7 @@ "¿Quieres volver a ejecutar la configuración de canales?" "Se quitarán los canales encontrados del sintonizador de canales y se volverán a buscar nuevos canales.\n\nComprueba que la TV esté conectada a una fuente de señal de TV.\n\nSi utilizas una antena inalámbrica, puede que tengas que cambiar su posición o dirección para recibir la mayor cantidad posible de canales. Para conseguir los mejores resultados, colócala en alto y cerca de una ventana." "Se quitarán los canales encontrados con el sintonizador USB y se volverán a buscar nuevos canales.\n\nComprueba que el sintonizador USB esté enchufado y conectado a una fuente de señal de TV.\n\nSi utilizas una antena inalámbrica, puede que tengas que cambiar su ubicación o dirección para recibir la mayor cantidad posible de canales. Para conseguir los mejores resultados, colócala en alto y cerca de una ventana." + "Se quitarán los canales encontrados del sintonizador en red y se repetirá la búsqueda de canales.\n\nComprueba que el sintonizador en red esté encendido y conectado a una fuente de señal de TV.\n\nSi utilizas una antena inalámbrica, es posible que tengas que cambiar su ubicación o su orientación para ver la mayoría de los canales. Para obtener los mejores resultados, colócala en un lugar alto cerca de una ventana." "Continuar" "Cancelar" @@ -54,6 +53,7 @@ "Configuración del sintonizador de canales" "Configuración del sintonizador de canales USB" + "Configuración del sintonizador de canales en red" "Este proceso puede tardar varios minutos" "El sintonizador no está disponible temporalmente o se está utilizando en otra grabación." @@ -76,6 +76,7 @@ "No se han encontrado canales" "No se ha encontrado ningún canal. Comprueba que la TV esté conectada a una fuente de señal de TV.\n\nSi utilizas una antena inalámbrica, cambia su posición o dirección. Para conseguir los mejores resultados, colócala en alto y cerca de una ventana. A continuación, vuelve a realizar la búsqueda." "No se ha encontrado ningún canal. Comprueba que el sintonizador USB esté enchufado y conectado a una fuente de señal de TV.\n\nSi utilizas una antena inalámbrica, cambia su ubicación o dirección. Para conseguir los mejores resultados, colócala en alto y cerca de una ventana. A continuación, vuelve a realizar la búsqueda." + "El análisis no ha encontrado ningún canal. Comprueba que el sintonizador en red esté encendido y conectado a una fuente de señal de TV.\n\nSi utilizas una antena inalámbrica, cambia su ubicación u orientación. Para obtener los mejores resultados, colócala en un lugar alto cerca de una ventana y repite el análisis." "Volver a buscar" "Listo" @@ -83,5 +84,7 @@ "Busca canales de TV" "Configuración del sintonizador de canales" "Configuración de sintonizador de canales USB" - "Sintonizador de canales USB desconectado." + "Configuración del sintonizador de TV en red" + "El sintonizador de canales USB está desconectado." + "El sintonizador en red está desconectado." diff --git a/usbtuner-res/values-et-rEE/strings.xml b/usbtuner-res/values-et-rEE/strings.xml index 70cb238a..a3fbba35 100644 --- a/usbtuner-res/values-et-rEE/strings.xml +++ b/usbtuner-res/values-et-rEE/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "Telerituuner" "USB-telerituuner" - "Sees" - "Väljas" + "Teleri võrgutuuner (BEETA)" "Oodake, kuni töötlemine on lõppenud" - "Valige kanali allikas" - "Pole signaali" - "Kanalile %s häälestamine ebaõnnestus" - "Häälestamine ebaõnnestus" "Tuuneri tarkvara värskendati hiljuti. Otsige kanaleid uuesti." "Heli lubamiseks lubage ruumiline heli süsteemi heliseadetes" + "Heli ei saa esitada. Proovige teist telerit" "Kanalituuneri seadistus" "Telerituuneri seadistus" "USB-kanalituuneri seadistus" + "Võrgutuuneri seadistus" "Veenduge, et teler oleks ühendatud teleri signaaliallikaga.\n\nKui kasutate õhuantenni, peate võimalikult paljude kanalite nägemiseks võib-olla kohandama selle asendit või suunda. Parimate tulemuste saavutamiseks asetage see kõrgele ja akna lähedale." "Veenduge, et USB-tuuner oleks pistikus ja ühendatud teleri signaaliallikaga.\n\nKui kasutate õhuantenni, peate enamiku kanalite nägemiseks võib-olla kohandama selle asendit või suunda. Parimate tulemuste saavutamiseks asetage see kõrgele ja akna lähedale." + "Veenduge, et võrgutuuner oleks sisse lülitatud ja teleri signaaliallikaga ühendatud.\n\nKui kasutate õhuantenni, peate võib-olla muutma selle asendit või suunda, et rohkem kanaleid leida. Parimate tulemuste saavutamiseks asetage see kõrgesse kohta akna lähedale." "Jätka" "Mitte praegu" @@ -40,6 +38,7 @@ "Kas käitada kanali seadistust uuesti?" "See eemaldab telerituuneri leitud kanalid ja otsib uuesti uusi kanaleid.\n\nVeenduge, et teler oleks ühendatud teleri signaaliallikaga.\n\nKui kasutate õhuantenni, peate võimalikult paljude kanalite nägemiseks võib-olla kohandama selle asendit või suunda. Parimate tulemuste saavutamiseks asetage see kõrgele ja akna lähedale." "See eemaldab USB-tuuneri leitud kanalid ja otsib uuesti uusi kanaleid.\n\nVeenduge, et USB-tuuner oleks pistikus ja ühendatud teleri signaaliallikaga.\n\nKui kasutate õhuantenni, peate enamiku kanalite nägemiseks võib-olla kohandama selle asendit või suunda. Parimate tulemuste saavutamiseks asetage see kõrgele ja akna lähedale." + "See eemaldab võrgutuunerist leitud kanalid ja skannib uuesti uusi kanaleid.\n\nVeenduge, et võrgutuuner oleks sisse lülitatud ja teleri signaaliallikaga ühendatud.\n\nKui kasutate õhuantenni, peate võib-olla muutma selle asendit või suunda, et rohkem kanaleid leida. Parimate tulemuste saavutamiseks asetage see kõrgesse kohta akna lähedale." "Jätka" "Tühista" @@ -54,6 +53,7 @@ "Telerituuneri seadistus" "USB-kanalituuneri seadistus" + "Võrgutuuneri kanalite seadistus" "See võib võtta mitu minutit" "Tuuner pole ajutiselt saadaval või seda kasutatakse juba salvestamiseks." @@ -76,6 +76,7 @@ "Ühtegi kanalit ei leitud" "Otsimisel ei leitud ühtegi kanalit. Veenduge, et teler oleks ühendatud teleri signaaliallikaga.\n\nKui kasutate õhuantenni, kohandage selle asendit või suunda. Parimate tulemuste saavutamiseks asetage see kõrgele ja akna lähedale ning otsige uuesti." "Otsimisel ei leitud ühtegi kanalit. Veenduge, et USB-tuuner oleks pistikus ja ühendatud teleri signaaliallikaga.\n\nKui kasutate õhuantenni, kohandage selle asendit või suunda. Parimate tulemuste saavutamiseks asetage see kõrgele ja akna lähedale ning otsige uuesti." + "Skannimisel ei leitud ühtegi kanalit. Veenduge, et võrgutuuner oleks sisse lülitatud ja teleri signaaliallikaga ühendatud.\n\nKui kasutate õhuantenni, muutke selle asendit või suunda. Parimate tulemuste saavutamiseks asetage see kõrgesse kohta akna lähedale ja skannige uuesti." "Otsi uuesti" "Valmis" @@ -83,5 +84,7 @@ "Otsige telekanaleid" "Telerituuneri seadistus" "USB-telerituuneri seadistus" - "USB-telerituuner eemaldati." + "Teleri võrgutuuneri seadistus" + "USB-telerituuner eemaldati." + "Võrgutuuner eemaldati." diff --git a/usbtuner-res/values-eu-rES/strings.xml b/usbtuner-res/values-eu-rES/strings.xml index a0d456eb..a1a5d395 100644 --- a/usbtuner-res/values-eu-rES/strings.xml +++ b/usbtuner-res/values-eu-rES/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "Sintonizadorea" "USB bidezko sintonizadorea" - "Aktibatuta" - "Desaktibatuta" + "Sareko sintonizadorea (BETA)" "Itxaron prozesatzen amaitu arte" - "Hautatu kanalaren iturburua" - "Ez dago seinalerik" - "Ezin izan da sintonizatu %s" - "Ezin izan da sintonizatu" "Sintonizadorearen softwarea berriki eguneratu da. Bilatu kanalak berriro." "Audioa gaitzeko, joan sistemaren soinuaren ezarpenetara eta gaitu soinu inguratzailea" + "Ezin da erreproduzitu audioa. Saiatu beste telebista batean." "Kanal-sintonizadorearen konfigurazioa" "Sintonizadorearen konfigurazioa" "USB kanal-sintonizadorearen konfigurazioa" + "Sareko sintonizadorearen konfigurazioa" "Egiaztatu telebista-seinalearen iturburu batera konektatuta dagoela telebista.\n\nAntena analogiko bat badarabilzu, agian bere kokapena edo norabidea doitu beharko dituzu kanal gehienak eskuratzeko. Emaitzarik onenak lortzeko, ezarri toki altu batean edo leiho baten ondoan." "Egiaztatu USB sintonizadorea entxufatuta eta telebistako seinale-iturburu batera konektatuta dagoela.\n\nHari gabeko antena badarabilzu, doitu bere kokapena edo norabidea. Emaitzarik onenak lortzeko, ezarri toki altu batean edo leiho baten ondoan eta gauzatu bilaketa berriro." + "Egiaztatu sintonizadorea piztuta dagoela eta telebista-seinalea igortzen duen iturburu batera konektatu duzula.\n\nHari-gabeko antena bat erabiltzen ari bazara, doi ezazu haren kokapena edo norabidea ahal bezain beste kanal aurkitzeko. Emaitzarik onenak lortzeko, ezar ezazu ahal bezain altu eta leiho batetik gertu." "Jarraitu" "Orain ez" @@ -40,6 +38,7 @@ "Berriro konfiguratu nahi dituzu kanalak?" "Sintonizadoreak aurkitutako kanalak kenduko dira eta berriro bilatuko dira kanalak.\n\nEgiaztatu telebista-seinalearen iturburu batera konektatuta dagoela telebista.\n\nAntena analogiko bat badarabilzu, agian bere kokapena edo norabidea doitu beharko dituzu kanal gehienak eskuratzeko. Emaitzarik onenak lortzeko, ezarri toki altu batean edo leiho baten ondoan." "USB sintonizadoreak aurkitutako kanalak kenduko dira eta berriro bilatuko dira kanalak.\n\nEgiaztatu USB sintonizadorea entxufatuta eta telebistako seinale-iturburu batera konektatuta dagoela.\n\nAntena analogiko bat badarabilzu, agian bere kokapena edo norabidea doitu beharko dituzu kanal gehienak eskuratzeko. Emaitzarik onenak lortzeko, ezarri toki altu batean edo leiho baten ondoan." + "Hori eginez gero, kendu egingo dira sareko sintonizadoreak aurkitutako kanalak eta zerotik hasiko da berriro kanalak bilatzen.\n\n Egiaztatu sintonizadorea piztuta dagoela eta telebista-seinalea igortzen duen iturburu batera konektatu duzula.\n\nHari-gabeko antena bat erabiltzen ari bazara, doi ezazu haren kokapena edo norabidea ahal bezain beste kanal aurkitzeko. Emaitzarik onenak lortzeko, ezar ezazu ahal bezain altu eta leiho batetik gertu." "Jarraitu" "Utzi" @@ -54,6 +53,7 @@ "Sintonizadorearen konfigurazioa" "USB kanal-sintonizadorearen konfigurazioa" + "Sareko kanalen sintonizadorearen konfigurazioa" "Zenbait minutu beharko dira" "Sintonizadorea ez dago erabilgarri edo beste zerbait ari da grabatzen." @@ -76,6 +76,7 @@ "Ez da aurkitu kanalik" "Bilaketak ez du aurkitu kanalik. Egiaztatu telebista-seinalearen iturburu batera konektatuta dagoela telebista.\n\nAntena analogiko bat badarabilzu, doitu bere kokapena edo norabidea. Emaitzarik onenak lortzeko, ezarri toki altu batean edo leiho baten ondoan eta gauzatu bilaketa berriro." "Bilaketak ez du aurkitu kanalik. Egiaztatu USB sintonizadorea entxufatuta eta telebistako seinale-iturburu batera konektatuta dagoela.\n\nAntena analogiko bat badarabilzu, doitu bere kokapena edo norabidea. Emaitzarik onenak lortzeko, ezarri toki altu batean edo leiho baten ondoan eta gauzatu bilaketa berriro." + "Ez da aurkitu kanalik. Egiaztatu sintonizadorea piztuta dagoela eta telebista-seinalea igortzen duen iturburu batera konektatu duzula.\n\nHari-gabeko antena bat erabiltzen ari bazara, doi ezazu haren kokapena edo norabidea. Emaitzarik onenak lortzeko, ezar ezazu ahal bezain altu eta leiho batetik gertu. Ondoren, bila itzazu kanalak berriro." "Bilatu berriro" "Eginda" @@ -83,5 +84,7 @@ "Bilatu telebista-kanalak" "Sintonizadorearen konfigurazioa" "USB bidezko sintonizadorearen konfigurazioa" - "USB bidezko sintonizadorea deskonektatu da." + "Sareko sintonizadorearen konfigurazioa" + "Deskonektatu da USB bidezko sintonizadorea." + "Deskonektatu da sareko sintonizadorea." diff --git a/usbtuner-res/values-fa/strings.xml b/usbtuner-res/values-fa/strings.xml index 8a51a4fc..3ae44d10 100644 --- a/usbtuner-res/values-fa/strings.xml +++ b/usbtuner-res/values-fa/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "تنظیم‌کننده تلویزیون" "‏تنظیم‌کننده تلویزیون USB" - "روشن" - "خاموش" + "تنظیم‌کننده تلویزیون شبکه (بتا)" "لطفاً تا پایان پردازش صبر کنید" - "منبع کانال را انتخاب کنید" - "بدون سیگنال" - "تنظیم به %s انجام نشد" - "تنظیم ناموفق بود" "نرم‌افزار تنظیم‌کننده اخیراً به‌روزرسانی شده است. لطفاً کانال‌ها را دوباره اسکن کنید." "برای فعال کردن صدا، صدای فراگیر را در تنظیمات صدای سیستم فعال کنید" + "صوت پخش نمی‌شود. لطفاً تلویزیون دیگری را امتحان کنید." "راه‌اندازی تنظیم‌کننده کانال" "راه‌اندازی تنظیم‌کننده تلویزیون" "‏راه‌اندازی تنظیم‌کننده کانال USB" + "راه‌اندازی تنظیم‌کننده شبکه‌ای" "مطمئن شوید تلویزیونتان به منبع سیگنال تلویزیونی متصل است.\n\nدر صورت استفاده از آنتن بی‌سیم شاید لازم باشد برای دریافت کانال‌های بیشتر، موقعیت یا جهت آن را تنظیم کنید. برای بهترین نتایج، آن را در جای بلند و نزدیک پنجره قرار دهید." "‏مطمئن شوید تنظیم‌کننده USB به منبع نیرو و منبع سیگنال تلویزیونی متصل است.\n\nدر صورت استفاده از آنتن بی‌سیم، موقعیت یا جهت آن را تنظیم کنید. برای بهترین نتایج، آن را در جای بلند و نزدیک پنجره قرار دهید." + "مطمئن شوید تنظیم‌کننده شبکه‌ای به برق و منبع سیگنال تلویزیونی وصل است.\n\nاگر از آنتن هوایی استفاده می‌کنید، ممکن است لازم باشد برای دریافت بیشترین تعداد کانال مکان و موقعیت آن را تنظیم کنید. برای بهترین نتایج، آن را در جای بلند و نزدیک پنجره قرار دهید." "ادامه" "فعلاً نه" @@ -40,6 +38,7 @@ "تنظیم کانال دوباره اجرا شود؟" "با این کار کانال‌های پیداشده از تنظیم‌کننده تلویزیون حذف می‌شوند و دوباره برای کانال‌های جدید اسکن می‌کند.\n\nمطمئن شوید تلویزیونتان به منبع سیگنال تلویزیونی متصل است.\n\nدر صورت استفاده از آنتن بی‌سیم، شاید لازم باشد برای دریافت کانال‌های بیشتر، موقعیت یا جهت آن را تنظیم کنید. برای بهترین نتایج، آن را در جای بلند و نزدیک پنجره قرار دهید." "‏این کار کانال‌های پیداشده از تنظیم‌کننده USB را حذف می‌کند و دوباره کانال‌های جدید را اسکن می‌کند.\n\nمطمئن شوید تنظیم‌کننده USB به منبع نیرو و منبع سیگنال تلویزیونی متصل است.\n\nدر صورت استفاده از آنتن بی‌سیم، برای دریافت کانال‌های بیشتر، موقعیت یا جهت آن را تنظیم کنید. برای بهترین نتایج، آن را در جای بلند و نزدیک پنجره قرار دهید و دوباره اسکن کنید." + "این کار کانال‌های پیداشده را از تنظیم‌کننده شبکه‌ای حذف می‌کند و دوباره کانال‌های جدید را اسکن می‌کند.\n\nمطمئن شوید تنظیم‌کننده شبکه‌ای به برق و منبع سیگنال تلویزیونی وصل است.\n\nاگر از آنتن هوایی استفاده می‌کنید، ممکن است لازم باشد برای دریافت بیشترین تعداد کانال مکان و موقعیت آن را تنظیم کنید. برای بهترین نتایج، آن را در جای بلند و نزدیک پنجره قرار دهید." "ادامه" "لغو" @@ -54,6 +53,7 @@ "راه‌اندازی تنظیم‌کننده تلویزیون" "‏راه‌اندازی تنظیم‌کننده کانال USB" + "راه‌اندازی تنظیم‌کننده کانال شبکه‌ای" "ممکن است چند دقیقه طول بکشد" "تنظیم‌کننده موقتاً دردسترس نیست یا در این لحظه در ضبط شدن استفاده می‌شود." @@ -76,6 +76,7 @@ "کانالی پیدا نشد" "اسکن هیچ کانالی پیدا نکرد. مطمئن شوید تلویزیونتان به یک منبع سیگنال تلویزیونی متصل است. \n\nدر صورت استفاده از آنتن بی‌سیم، موقعیت یا جهت آن را تنظیم کنید. برای بهترین نتایج، آن را در جای بلند و نزدیک پنجره قرار دهید و دوباره اسکن کنید." "‏اسکن هیچ کانالی پیدا نکرد. مطمئن شوید تنظیم‌کننده USB به منبع نیرو و منبع سیگنال تلویزیونی متصل است.\n\nدر صورت استفاده از آنتن بی‌سیم، موقعیت یا جهت آن را تنظیم کنید. برای بهترین نتایج، آن را در جای بلند و نزدیک پنجره قرار دهید و دوباره اسکن کنید." + "اسکن، کانالی پیدا نکرد. مطمئن شوید تنظیم‌کننده شبکه‌ای به برق و منبع سیگنال تلویزیونی وصل است.\n\nاگر از آنتن هوایی استفاده می‌کنید، موقعیت یا جهت آن را تنظیم کنید. برای بهترین نتایج، آن را در جای بلند و نزدیک پنجره قرار دهید و دوباره اسکن کنید." "اسکن دوباره" "تمام" @@ -83,5 +84,7 @@ "اسکن کانال‌های تلویزیون" "راه‌اندازی تنظیم‌کننده تلویزیون" "‏راه‌اندازی تنظیم‌کننده تلویزیون USB" - "‏تیونر تلویزیون USB قطع شد." + "راه‌اندازی تنظیم‌کننده تلویزیون شبکه‌ای" + "‏ارتباط تیونر USB تلویزیون قطع شد." + "ارتباط تیونر شبکه قطع شد." diff --git a/usbtuner-res/values-fi/strings.xml b/usbtuner-res/values-fi/strings.xml index 79d0c7f5..1b646750 100644 --- a/usbtuner-res/values-fi/strings.xml +++ b/usbtuner-res/values-fi/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "TV-viritin" "USB-TV-viritin" - "Ota käyttöön" - "Poista käytöstä" + "Antenni-TV-viritin (BETA)" "Odota, että käsittely on valmis." - "Valitse kanavalähde." - "Ei signaalia" - "Kanavan %s virittäminen epäonnistui." - "Virittäminen epäonnistui." "Viritinohjelmisto on päivitetty äskettäin. Hae kanavat uudelleen." "Ota ääni käyttöön kytkemällä tilaääni päälle järjestelmän ääniasetuksissa." + "Äänen toistaminen ei onnistu. Yritä käyttää toista TV:tä." "Kanavavirittimen määritys" "TV-virittimen määritys" "USB-kanavavirittimen määritys" + "Antennivirittimen määritys" "Tarkista, että TV on liitetty TV-signaalilähteeseen.\n\nJos käytät antennia, useampien kanavien löytäminen saattaa edellyttää sen paikan tai suuntauksen säätämistä. Saat parhaan tuloksen asettamalla antennin korkealle ja lähelle ikkunaa." "Tarkista, että USB-viritin on kytketty oikein ja liitetty TV-signaalilähteeseen.\n\nJos käytät antennia, useampien kanavien löytäminen saattaa edellyttää sen paikan ja suuntauksen säätämistä. Saat parhaan tuloksen asettamalla antennin korkealle ja lähelle ikkunaa." + "Tarkista, että viritin on päällä ja liitetty TV-signaalilähteeseen.\n\nJos käytät antennia, säädä sen paikkaa tai suuntausta. Aseta antenni korkealle ja lähelle ikkunaa, jotta saat parhaat tulokset, ja tee uusi haku." "Jatka" "Ei nyt" @@ -40,6 +38,7 @@ "Haetaanko kanavia uudelleen?" "Tämä poistaa TV-virittimen löytämät kanavat ja tekee kanavahaun uudelleen.\n\nTarkista, että TV on liitetty TV-signaalilähteeseen.\n\nJos käytät antennia, useampien kanavien löytäminen saattaa edellyttää sen paikan tai suuntauksen säätämistä. Saat parhaan tuloksen asettamalla antennin korkealle ja lähelle ikkunaa." "Tämä poistaa USB-virittimen löytämät kanavat ja tekee kanavahaun uudelleen.\n\nTarkista, että USB-viritin on kytketty oikein ja liitetty TV-signaalilähteeseen.\n\nJos käytät antennia, useampien kanavien löytäminen saattaa edellyttää sen paikan tai suuntauksen säätämistä. Saat parhaan tuloksen asettamalla antennin korkealle ja lähelle ikkunaa." + "Tämä poistaa virittimen löytämät kanavat ja etsii kanavia uudestaan.\n\nTarkista, että viritin on päällä ja liitetty TV-signaalilähteeseen.\n\nJos käytät antennia, säädä sen paikkaa tai suuntausta. Aseta antenni korkealle ja lähelle ikkunaa, jotta saat parhaat tulokset, ja tee uusi haku." "Jatka" "Peruuta" @@ -54,6 +53,7 @@ "TV-virittimen määritys" "USB-kanavavirittimen määritys" + "Antennikanavavirittimen määritys" "Tämä voi kestää useita minuutteja." "Viritin ei ole toistaiseksi käytettävissä tai se on nauhoitteen käytössä." @@ -76,6 +76,7 @@ "Kanavia ei löytynyt" "Haku ei löytänyt yhtään kanavaa. Tarkista, että TV on liitetty TV-signaalilähteeseen.\n\nJos käytät antennia, säädä sen paikkaa tai suuntausta. Saat parhaan tuloksen asettamalla antennin korkealle ja lähelle ikkunaa. Tee haku sitten uudelleen." "Haku ei löytänyt yhtään kanavaa. Tarkista, että USB-viritin on kytketty oikein ja liitetty TV-signaalilähteeseen.\n\nJos käytät antennia, säädä sen paikkaa tai suuntausta. Saat parhaan tuloksen asettamalla antennin korkealle ja lähelle ikkunaa. Tee haku sitten uudelleen." + "Haussa ei löytynyt kanavia. Tarkista, että viritin on päällä ja liitetty TV-signaalilähteeseen.\n\nJos käytät antennia, säädä sen paikkaa tai suuntausta. Aseta antenni korkealle ja lähelle ikkunaa, jotta saat parhaat tulokset, ja tee uusi haku." "Hae uudelleen" "Valmis" @@ -83,5 +84,7 @@ "Hae TV-kanavia" "TV-virittimen määritys" "USB-TV-virittimen määritys" - "USB-TV-viritin ei ole kytketty" + "Antenni-TV-virittimen määritys" + "USB-TV-viritin ei ole kytketty." + "Verkkoviritin ei ole kytketty." diff --git a/usbtuner-res/values-fr-rCA/strings.xml b/usbtuner-res/values-fr-rCA/strings.xml index 54e8759e..795ae859 100644 --- a/usbtuner-res/values-fr-rCA/strings.xml +++ b/usbtuner-res/values-fr-rCA/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "Syntoniseur télé" "Syntoniseur télé USB" - "Activer" - "Désactivé" + "Syntoniseur télé réseau (BÊTA)" "Veuillez patienter jusqu\'à la fin du traitement" - "Sélectionnez votre source de chaînes" - "Aucun signal" - "Impossible de syntoniser %s" - "Échec de syntonisation" "Le logiciel du syntoniseur a été mis à jour récemment. Veuillez rechercher les chaînes à nouveau." "Pour activer l\'audio, vous devez activer le son ambiophonique dans les paramètres sonores du système" + "Impossible de lire l\'audio. Veuillez essayer sur un autre téléviseur." "Configuration du syntoniseur de chaînes" "Configuration du syntoniseur télé" "Configurer les chaînes du syntoniseur USB" + "Configuration du syntoniseur réseau" "Vérifiez que le téléviseur est connecté à la source d\'un signal de télévision.\n\nSi vous utilisez une antenne, vous devrez peut-être ajuster sa position ou son orientation pour recevoir le plus de chaînes possible. Pour obtenir les meilleurs résultats, placez-la dans une position élevée et près d\'une fenêtre." "Vérifiez que le syntoniseur USB est branché et connecté à la source d\'un signal de télévision.\n\nSi vous utilisez une antenne, ajustez sa position ou son orientation. Pour obtenir les meilleurs résultats, placez-la dans une position élevée et près d\'une fenêtre, puis relancez la recherche." + "Assurez-vous que le syntoniseur réseau est allumé et connecté à une source de signal télé.\n\nSi vous utilisez une antenne, il se peut que vous deviez ajuster sa position ou sa direction pour capter un maximum de chaînes. Pour obtenir des résultats optimaux, placez-la en hauteur et près d\'une fenêtre." "Continuer" "Pas maintenant" @@ -40,6 +38,7 @@ "Relancer la configuration de la chaîne?" "Cette opération permet de supprimer les chaînes détectées du syntoniseur télé et d\'en rechercher de nouvelles.\n\nVérifiez que le téléviseur est connecté à la source d\'un signal de télévision.\n\nSi vous utilisez une antenne, vous devrez peut-être ajuster sa position ou son orientation pour recevoir le plus de chaînes possible. Pour obtenir les meilleurs résultats, placez-la dans une position élevée et près d\'une fenêtre." "Cette opération permet de supprimer les chaînes détectées du syntoniseur USB et d\'en rechercher de nouvelles.\n\nVérifiez que le syntoniseur USB est branché et connecté à la source d\'un signal de télévision.\n\nSi vous utilisez une antenne, vous devrez peut-être ajuster sa position ou son orientation pour recevoir le plus de chaînes possible. Pour obtenir les meilleurs résultats, placez-la dans une position élevée et près d\'une fenêtre." + "Cela supprimera les chaînes trouvées du syntoniseur réseau et lancera une nouvelle recherche.\n\nAssurez-vous que le syntoniseur réseau est allumé et connecté à une source de signal télé.\n\nSi vous utilisez une antenne, il se peut que vous deviez ajuster sa position ou sa direction pour capter un maximum de chaînes. Pour obtenir des résultats optimaux, placez-la en hauteur et près d\'une fenêtre." "Continuer" "Annuler" @@ -54,6 +53,7 @@ "Configuration du syntoniseur télé" "Configurer les chaînes du syntoniseur USB" + "Configuration du syntoniseur de chaînes réseau" "Cela peut prendre plusieurs minutes" "Le syntoniseur n\'est pas accessible ou bien il est en cours d\'utilisation par l\'enregistreur." @@ -76,6 +76,7 @@ "Aucune chaîne" "La recherche n\'a détecté aucune chaîne. Vérifiez que le téléviseur est connecté à la source d\'un signal de télévision.\n\nSi vous utilisez une antenne, ajustez sa position ou son orientation. Pour obtenir les meilleurs résultats, placez-la dans une position élevée et près d\'une fenêtre, puis relancez la recherche." "La recherche n\'a détecté aucune chaîne. Vérifiez que le syntoniseur USB est branché et connecté à la source d\'un signal de télévision.\n\nSi vous utilisez une antenne, ajustez sa position ou son orientation. Pour obtenir les meilleurs résultats, placez-la dans une position élevée et près d\'une fenêtre, puis relancez la recherche." + "La recherche n\'a détecté aucune chaîne. Assurez-vous que le syntoniseur réseau est allumé et connecté à une source de signal télé.\n\nSi vous utilisez une antenne, ajustez sa position ou sa direction. Pour obtenir des résultats optimaux, placez-la en hauteur et près d\'une fenêtre, puis relancez la recherche." "Rechercher à nouveau" "Terminé" @@ -83,5 +84,7 @@ "Rechercher les chaînes de télévision" "Configuration du syntoniseur télé" "Configurer le syntoniseur télé USB" - "Le syntoniseur télé USB est débranché." + "Configuration du syntoniseur télé réseau" + "Le syntoniseur télé USB est déconnecté." + "Le syntoniseur réseau est déconnecté." diff --git a/usbtuner-res/values-fr/strings.xml b/usbtuner-res/values-fr/strings.xml index 4cb551f7..f25bf9c1 100644 --- a/usbtuner-res/values-fr/strings.xml +++ b/usbtuner-res/values-fr/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "Tuner TV" "Tuner TV USB" - "Activé" - "Désactivé" + "Tuner TV réseau (VERSION BÊTA)" "Veuillez patienter jusqu\'à la fin du traitement." - "Sélectionnez la source de la chaîne." - "Aucun signal" - "Échec de sélection de la chaîne %s" - "Échec de la sélection de la chaîne" "Le logiciel du tuner a été mis à jour récemment. Veuillez lancer une nouvelle recherche des chaînes." "Activer le son surround dans les paramètres sonores du système pour activer l\'audio" + "Impossible de lire le fichier audio. Veuillez utiliser un autre téléviseur" "Configuration du tuner de chaînes" "Configuration du tuner TV" "Configuration du tuner de chaînes USB" + "Configuration du tuner réseau" "Vérifiez que le téléviseur est connecté à la source d\'un signal de télévision.\n\nSi vous utilisez une antenne Over The Air, vous devrez peut-être ajuster sa position ou son orientation pour recevoir le plus de chaînes possible. Pour obtenir les meilleurs résultats, placez-la dans une position élevée et près d\'une fenêtre." "Vérifiez que le tuner USB est branché et connecté à la source d\'un signal de télévision.\n\nSi vous utilisez une antenne Over The Air, ajustez sa position ou son orientation. Pour obtenir les meilleurs résultats, placez-la dans une position élevée et près d\'une fenêtre, puis relancez la recherche." + "Vérifiez que le tuner réseau est allumé et connecté à la source d\'un signal de télévision.\n\nSi vous utilisez une antenne Over The Air, vous devrez peut-être ajuster sa position ou son orientation pour recevoir le plus de chaînes possible. Pour de meilleurs résultats, placez-la en hauteur et près d\'une fenêtre." "Continuer" "Pas maintenant" @@ -40,6 +38,7 @@ "Relancer la configuration de la chaîne ?" "Cette opération permet de supprimer les chaînes détectées du tuner TV et d\'en rechercher de nouvelles.\n\nVérifiez que le téléviseur est connecté à la source d\'un signal de télévision.\n\nSi vous utilisez une antenne Over The Air, vous devrez peut-être ajuster sa position ou son orientation pour recevoir le plus de chaînes possible. Pour obtenir les meilleurs résultats, placez-la dans une position élevée et près d\'une fenêtre." "Cette opération permet de supprimer les chaînes détectées du tuner USB et d\'en rechercher de nouvelles.\n\nVérifiez que le tuner USB est branché et connecté à la source d\'un signal de télévision.\n\nSi vous utilisez une antenne Over The Air, vous devrez peut-être ajuster sa position ou son orientation pour recevoir le plus de chaînes possible. Pour obtenir les meilleurs résultats, placez-la dans une position élevée et près d\'une fenêtre." + "Cette opération permet de supprimer les chaînes détectées du tuner réseau et d\'en rechercher de nouvelles.\n\nVérifiez que le tuner réseau est allumé et connecté à la source d\'un signal de télévision.\n\nSi vous utilisez une antenne Over The Air, vous devrez peut-être ajuster sa position ou son orientation pour recevoir le plus de chaînes possible. Pour de meilleurs résultats, placez-la en hauteur et près d\'une fenêtre." "Continuer" "Annuler" @@ -54,6 +53,7 @@ "Configuration du tuner TV" "Configuration du tuner de chaînes USB" + "Configuration du tuner de chaînes réseau" "Cette opération peut prendre plusieurs minutes." "Le tuner est temporairement indisponible ou est déjà utilisé pour l\'enregistrement." @@ -76,6 +76,7 @@ "Aucune chaîne n\'a été détectée" "La recherche n\'a détecté aucune chaîne. Vérifiez que le téléviseur est connecté à la source d\'un signal de télévision.\n\nSi vous utilisez une antenne Over The Air, ajustez sa position ou son orientation. Pour obtenir les meilleurs résultats, placez-la dans une position élevée et près d\'une fenêtre, puis relancez la recherche." "La recherche n\'a détecté aucune chaîne. Vérifiez que le tuner USB est branché et connecté à la source d\'un signal de télévision.\n\nSi vous utilisez une antenne Over The Air, ajustez sa position ou son orientation. Pour obtenir les meilleurs résultats, placez-la dans une position élevée et près d\'une fenêtre, puis relancez la recherche." + "La recherche n\'a détecté aucune chaîne. Vérifiez que le tuner réseau est allumé et connecté à la source d\'un signal de télévision.\n\nSi vous utilisez une antenne Over The Air, ajustez sa position ou son orientation. Pour de meilleurs résultats, placez-la en hauteur et près d\'une fenêtre, puis relancez la recherche." "Rechercher à nouveau" "OK" @@ -83,5 +84,7 @@ "Rechercher les chaînes de télévision" "Configuration du tuner TV" "Configuration du tuner TV USB" - "Le tuner TV USB est déconnecté." + "Configuration du tuner TV réseau" + "Le tuner TV USB est déconnecté." + "Tuner réseau déconnecté." diff --git a/usbtuner-res/values-gl-rES/strings.xml b/usbtuner-res/values-gl-rES/strings.xml index 58c14605..48e4828c 100644 --- a/usbtuner-res/values-gl-rES/strings.xml +++ b/usbtuner-res/values-gl-rES/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "Sintonizador de televisión" "Sintonizador USB de televisión" - "Activar" - "Desactivar" + "Sintonizador de televisión de rede (beta)" "Espera a que finalice o procesamento" - "Selecciona a fonte da túa canle" - "Sen sinal" - "Produciuse un erro ao sintonizar %s" - "Produciuse un erro ao sintonizar" "O software do sintonizador actualizouse recentemente. Volve buscar canles." "Activa o son envolvente na configuración de son do sistema para activar o audio" + "Non se pode reproducir o audio. Proba con outra televisión" "Configuración do sintonizador de canles" "Configuración do sintonizador de televisión" "Configuración do sintonizador de canles USB" + "Configuración do sintonizador de rede" "Comproba que a televisión está conectada a unha fonte de sinal de televisión.\n\nSe usas unha antena sen fíos, é posible que teñas que axustar a súa posición ou dirección para recibir a maioría das canles. Para conseguir os mellores resultados, colócaa nun lugar alto e preto dunha ventá." "Comproba que o sintonizador USB está enchufado e conectado a unha fonte de sinal de televisión.\n\nSe usas unha antena sen fíos, é posible que teñas que axustar a súa posición ou dirección para recibir a maioría das canles. Para conseguir os mellores resultados, colócaa nun lugar alto e preto dunha ventá." + "Verifica que o sintonizador de rede estea acendido e conectado a unha fonte de sinal de televisión.\n\nSe usas unha antena sen fíos, é posible que teñas que axustar a súa posición ou dirección para recibir a maioría das canles. Para conseguir os mellores resultados, colócaa nun lugar alto e preto dunha ventá." "Continuar" "Agora non" @@ -40,6 +38,7 @@ "Queres volver executar a configuración da canle?" "Esta acción eliminará as canles que atopaches co sintonizador de televisión e buscará outras novas.\n\nComproba que a televisión está conectada a unha fonte de sinal de televisión.\n\nSe usas unha antena sen fíos, é posible que teñas que axustar a súa posición ou dirección para recibir a maioría das canles. Para conseguir os mellores resultados, colócaa nun lugar alto e preto dunha ventá." "Esta acción eliminará as canles que atopaches co sintonizador USB e buscará outras novas.\n\nComproba que o sintonizador USB está enchufado e conectado a unha fonte de sinal de televisión.\n\nSe usas unha antena sen fíos, é posible que teñas que axustar a súa posición ou dirección para recibir a maioría das canles. Para conseguir os mellores resultados, colócaa nun lugar alto e preto dunha ventá." + "Con esta acción quitaranse as canles atopadas do teu sintonizador de rede e buscaranse novas canles outra vez.\n\nVerifica que o sintonizador de rede estea acendido e conectado a unha fonte de sinal de televisión.\n\n}Se usas unha antena sen fíos, é posible que teñas que axustar a súa posición ou dirección para recibir a maioría das canles. Para conseguir os mellores resultados, colócaa nun lugar alto e preto dunha ventá." "Continuar" "Cancelar" @@ -54,6 +53,7 @@ "Configuración do sintonizador de televisión" "Configuración do sintonizador de canles USB" + "Configuración do sintonizador de canles de rede" "Esta acción pode tardar varios minutos" "O sintonizador non está dispoñible temporalmente ou xa se utiliza para unha gravación." @@ -76,6 +76,7 @@ "Non se atopou ningunha canle" "Durante a busca non se atopou ningunha canle. Comproba que a televisión está conectada a unha fonte de sinal de televisión.\n\nSe usas unha antena sen fíos, axusta a súa posición ou dirección. Para conseguir os mellores resultados, colócaa nun lugar alto, preto dunha ventá e realiza a fai a busca de novo." "A busca non atopou ningunha canle. Comproba que o sintonizador USB está enchufado e conectado a unha fonte de sinal de televisión.\n\nSe usas unha antena sen fíos, axusta a súa posición ou dirección. Para conseguir os mellores resultados, colócaa nun lugar alto, preto dunha ventá e realiza a busca de novo." + "A busca de canles non obtivo resultados. Verifica que o sintonizador de rede estea acendido e conectado a unha fonte de sinal de televisión.\n\nSe usas unha antena sen fíos, axusta a súa posición ou dirección. Para conseguir os mellores resultados, colócaa nun lugar alto, preto dunha ventá, e realiza a busca de novo." "Buscar de novo" "Feito" @@ -83,5 +84,7 @@ "Busca canles de televisión" "Configuración do sintonizador de televisión" "Configuración do sintonizador USB de televisión" - "Desconectouse o sintonizador de televisión USB." + "Configuración do sintonizador de televisión de rede" + "Desconectouse o sintonizador de televisión USB." + "Desconectouse o sintonizador de rede." diff --git a/usbtuner-res/values-hi/strings.xml b/usbtuner-res/values-hi/strings.xml index ea0d51c1..d95199bb 100644 --- a/usbtuner-res/values-hi/strings.xml +++ b/usbtuner-res/values-hi/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "टीवी ट्यूनर" "USB टीवी ट्यूनर" - "चालू करें" - "बंद करें" + "नेटवर्क टीवी ट्यूनर (बीटा)" "कृपया प्रक्रिया पूरी होने का इंतज़ार करें" - "अपना चैनल स्रोत चुनें" - "कोई सिग्नल नहीं" - "%s को ट्यून करने में विफल रहा" - "ट्यून करने में विफल रहा" "ट्यूनर सॉफ़्टवेयर को हाल ही में अपडेट किया गया है. कृपया चैनलों के लिए दोबारा स्कैन करें." "ऑडियो सक्षम करने के लिए सिस्टम साउंड सेटिंग में सराउंड साउंड सक्षम करें" + "ऑडियो नहीं चल पा रहा है. कृपया कोई दूसरा टीवी आज़माएं" "चैनल ट्यूनर सेटअप" "टीवी ट्यूनर सेटअप" "USB चैनल ट्यूनर सेटअप" + "नेटवर्क ट्यूनर सेटअप" "पुष्टि करें कि आपका टीवी किसी टीवी सिग्नल स्रोत से कनेक्ट किया हुआ है.\n\nयदि ओवर द एयर एंटेना का उपयोग कर रहे हैं, तो अधिकांश चैनल पाने के लिए आपको उसकी स्थिति या दिशा समायोजित करने की आवश्यकता हो सकती है. सबसे अच्छे परिणामों के लिए, उसे ऊंचाई पर और किसी खिड़की के पास लगाएं." "पुष्टि करें कि USB ट्यूनर प्लग इन है और टीवी सिग्नल स्रोत से कनेक्ट किया हुआ है.\n\nयदि आप ओवर द एयर एंटेना का उपयोग कर रहे हैं, तो अधिकांश चैनल पाने के लिए आपको उसकी स्थिति या दिशा समायोजित करने की आवश्यकता हो सकती है. अच्छे परिणामों के लिए, उसे ऊंचाई पर और किसी खिड़की के पास लगाएं." + "सत्यापित करें कि नेटवर्क ट्यूनर चालू है और किसी टीवी सिग्नल स्रोत से कनेक्ट है.\n\nयदि किसी ओवर-द-एयर एंटेना का उपयोग कर रहे हैं, तो अधिकांश चैनल प्राप्त करने के लिए आपको एंटेना का स्थान या दिशा समायोजित करनी पड़ सकती है. सर्वश्रेष्ठ परिणामों के लिए, उसे ऊंचाई पर और किसी खिड़की के पास रखें." "जारी रखें" "अभी नहीं" @@ -40,6 +38,7 @@ "चैनल सेटअप फिर से चलाएं?" "इससे टीवी ट्यूनर से मिले चैनल निकल जाएंगे और नए चैनल दोबारा स्कैन किए जाएंगे.\n\nपुष्टि करें कि आपका टीवी किसी टीवी सिग्नल स्रोत से कनेक्ट किया हुआ है.\n\nयदि ओवर द एयर एंटेना का उपयोग कर रहे हैं, तो अधिकांश चैनल पाने के लिए आपको उसकी स्थिति या दिशा समायोजित करने की आवश्यकता हो सकती है. सबसे अच्छे परिणामों के लिए, उसे ऊंचाई पर और किसी खिड़की के पास लगाएं." "इससे USB ट्यूनर से मिले चैनल निकल जाएंगे और नए चैनलों के लिए फिर से स्कैन किया जाएगा.\n\nपुष्टि करें कि USB ट्यूनर प्लग इन है और टीवी सिग्नल स्रोत से कनेक्ट किया हुआ है.\n\nयदि आप ओवर द एयर एंटेना का उपयोग कर रहे हैं, तो अधिकांश चैनल पाने के लिए आपको उसकी स्थिति या दिशा समायोजित करने की आवश्यकता हो सकती है. अच्छे परिणामों के लिए, उसे ऊंचाई पर और किसी खिड़की के पास लगाएं." + "ऐसा करने से नेटवर्क ट्यूनर से मिले चैनल निकाल दिए जाएंगे और नए चैनल के लिए फिर से स्कैन किया जाएगा.\n\nसत्यापित करें कि नेटवर्क ट्यूनर चालू है और किसी टीवी सिग्नल स्रोत से कनेक्ट है.\n\nयदि किसी ओवर-द-एयर एंटेना का उपयोग कर रहे हैं, तो अधिकांश चैनल प्राप्त करने के लिए आपको एंटेना का स्थान या दिशा समायोजित करनी पड़ सकती है. सर्वश्रेष्ठ परिणामों के लिए, उसे ऊंचाई पर और किसी खिड़की के पास रखें." "जारी रखें" "रद्द करें" @@ -54,8 +53,9 @@ "टीवी ट्यूनर सेटअप" "USB चैनल ट्यूनर सेटअप" + "नेटवर्क चैनल ट्यूनर सेटअप" "इसमें कुछ मिनट लग सकते हैं" - "ट्यूनर अस्थायी रूप से उपलब्ध नहीं है या रिकॉर्डिंग में उसका उपयोग पहले ही कर लिया गया है." + "ट्यूनर अस्थायी रूप से उपलब्ध नहीं है या रिकॉर्डिंग में उसका उपयोग पहले ही हो रहा है." %1$d चैनल मिले %1$d चैनल मिले @@ -76,6 +76,7 @@ "कोई चैनल नहीं मिला" "स्कैन करने से कोई भी चैनल नहीं मिला. पुष्टि करें कि आपका टीवी किसी टीवी सिग्नल स्रोत से कनेक्ट किया हुआ है.\n\nयदि ओवर द एयर एंटेना का उपयोग कर रहे हैं, तो उसकी स्थिति या दिशा समायोजित करें. सबसे अच्छे परिणामों के लिए, उसे ऊंचाई पर और किसी खिड़की के पास लगाएं और दोबारा स्कैन करें." "स्कैन करने से कोई चैनल नहीं मिला. पुष्टि करें कि USB ट्यूनर प्लग इन है और टीवी सिग्नल स्रोत से कनेक्ट किया हुआ है.\n\nयदि आप ओवर द एयर एंटेना का उपयोग कर रहे हैं, तो उसकी स्थिति या दिशा समायोजित करें. अच्छे परिणामों के लिए, उसे ऊंचाई पर और किसी खिड़की के पास लगाएं और दोबारा स्कैन करें." + "स्कैन में कोई चैनल नहीं मिला. सत्यापित करें कि नेटवर्क ट्यूनर चालू है और किसी टीवी संकेत स्रोत से कनेक्ट है.\n\nयदि किसी ओवर-द-एयर एंटेना का उपयोग कर रहे हैं, तो उसका स्थान या दिशा समायोजित करें. सर्वश्रेष्ठ परिणामों के लिए, उसे ऊंचाई पर और किसी खिड़की के पास रखें और फिर से स्कैन करें." "दोबारा स्‍कैन करें" "हो गया" @@ -83,5 +84,7 @@ "टीवी चैनलों के लिए स्‍कैन करें" "टीवी ट्यूनर सेटअप" "USB टीवी ट्यूनर सेटअप" - "USB टीवी ट्यूनर डिस्कनेक्ट किया गया." + "नेटवर्क टीवी ट्यूनर सेटअप" + "USB टीवी ट्यूनर डिसकनेक्ट किया गया." + "नेटवर्क ट्यूनर डिसकनेक्ट किया गया." diff --git a/usbtuner-res/values-hr/strings.xml b/usbtuner-res/values-hr/strings.xml index 4a00deaf..7d0733ec 100644 --- a/usbtuner-res/values-hr/strings.xml +++ b/usbtuner-res/values-hr/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "TV prijemnik" "USB TV prijemnik" - "Uključeno" - "Isključeno" + "Mrežni TV prijemnik (BETA)" "Pričekajte da obrada završi" - "Odaberite izvor kanala" - "Nema signala" - "Namještanje na kanal %s nije uspjelo" - "Namještanje nije uspjelo" "Softver prijemnika nedavno je ažuriran. Ponovite traženje kanala." "Omogućite okružujući zvuk u postavkama zvuka na razini sustava da biste omogućili audio" + "Zvuk se ne može reproducirati. Pokušajte s drugim televizorom" "Postavljanje prijemnika kanala" "Postavljanje TV prijemnika" "Postavljanje USB prijemnika za kanale" + "Postavljanje mrežnog prijemnika" "Provjerite je li televizor povezan s izvorom televizijskog signala.\n\nAko upotrebljavate antenu zemaljske televizije, možda ćete joj morati promijeniti položaj ili smjer da biste pronašli najviše kanala. Za najbolje rezultate postavite je visoko i u blizini prozora." "Provjerite je li USB prijemnik priključen i povezan s izvorom TV signala.\n\nAko upotrebljavate antenu zemaljske televizije, možda trebate prilagoditi njezin položaj ili smjer da biste primali najviše kanala. Za najbolje rezultate postavite je visoko i u blizini prozora." + "Provjerite je li mrežni prijemnik uključen i povezan s izvorom TV signala.\n\nAko upotrebljavate bežičnu antenu, možda ćete trebati prilagoditi položaj ili smjer da biste pronašli najviše kanala. Za najbolje rezultate postavite je visoko i blizu prozora." "Nastavi" "Ne sada" @@ -40,6 +38,7 @@ "Želite li ponovo pokrenuti postavljanje kanala?" "Time će se ukloniti kanali pronađeni pomoću TV prijemnika i ponovo pokrenuti pretraživanje kanala.\n\nProvjerite je li televizor povezan s izvorom televizijskog signala.\n\nAko upotrebljavate antenu zemaljske televizije, možda ćete joj morati promijeniti položaj ili smjer da biste pronašli najviše kanala. Za najbolje rezultate postavite je visoko i u blizini prozora." "Time će se ukloniti kanali pronađeni putem USB prijemnika i ponoviti pretraživanje kanala.\n\nProvjerite je li USB prijemnik priključen i povezan s izvorom televizijskog signala.\n\nAko upotrebljavate antenu zemaljske televizije, možda trebate prilagoditi njezin položaj ili smjer da biste primali najviše kanala. Za najbolje rezultate postavite je visoko i u blizini prozora." + "Time će se ukloniti kanali pronađeni mrežnim prijemnikom i pokrenuti pretraživanje novih kanala.\n\nProvjerite je li mrežni prijemnik uključen i povezan s izvorom TV signala.\n\nAko upotrebljavate bežičnu antenu, možda ćete trebati prilagoditi položaj ili smjer da biste pronašli najviše kanala. Za najbolje rezultate postavite je visoko i blizu prozora." "Nastavi" "Odustani" @@ -54,6 +53,7 @@ "Postavljanje TV prijemnika" "Postavljanje USB prijemnika za kanale" + "Postavljanje prijemnika za mrežne kanale" "To može potrajati nekoliko minuta" "Prijemnik trenutačno nije dostupan ili se već upotrebljava za snimanje." @@ -79,6 +79,7 @@ "Nije pronađen nijedan kanal" "Tijekom pretraživanja nije pronađen nijedan kanal. Provjerite je li televizor povezan s izvorom televizijskog signala.\n\nAko upotrebljavate antenu zemaljske televizije, promijenite joj položaj ili smjer. Za najbolje rezultate postavite je visoko i u blizini prozora, a zatim pretražite ponovo." "Pretraživanjem nije pronađen nijedan kanal. Provjerite je li USB prijemnik priključen i povezan s izvorom televizijskog signala.\n\nAko upotrebljavate antenu zemaljske televizije, prilagodite joj položaj ili smjer. Za najbolje rezultate postavite je visoko i u blizini prozora i pretražite ponovo." + "Nije pronađen nijedan kanal. Provjerite je li mrežni prijemnik uključen i povezan s izvorom TV signala.\n\nAko upotrebljavate bežičnu antenu, prilagodite položaj ili smjer. Za najbolje rezultate postavite je visoko i blizu prozora i pretražite ponovo." "Pretraži ponovo" "Gotovo" @@ -86,5 +87,7 @@ "Pretražite TV kanale" "Postavljanje TV prijemnika" "Postavljanje USB TV prijemnika" - "USB TV prijemnik isključen." + "Postavljanje mrežnog TV prijemnika" + "USB TV prijemnik isključen." + "Mrežni prijemnik isključen." diff --git a/usbtuner-res/values-hu/strings.xml b/usbtuner-res/values-hu/strings.xml index e2bca51c..fc25f636 100644 --- a/usbtuner-res/values-hu/strings.xml +++ b/usbtuner-res/values-hu/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "Tévétuner" "USB-s tévétuner" - "Be" - "Ki" + "Hálózati tévétuner (BÉTA)" "Kérjük, várja meg a folyamat befejezését" - "Válassza ki a csatornaforrást" - "Nincs jel" - "Nem sikerült behangolni a(z) %s csatornát" - "Nem sikerült a hangolás" "A tuner szoftverét nemrég frissítették. Kérjük, ismételje meg a csatornakeresést." "A hang aktiválásához engedélyezze a térhatású hangot a rendszerszintű hangbeállításokban" + "A hangot nem lehet lejátszani. Kérjük, próbálkozzon másik tévén" "Csatornatuner beállítása" "Tévétuner beállítása" "USB-s csatornatuner beállítása" + "Hálózati tuner beállítása" "Ellenőrizze, hogy tévéje csatlakoztatva van-e a televíziós jelforráshoz.\n\nAntenna használata esetén szükség lehet az elhelyezés, illetve az irány módosítására a lehető legtöbb csatorna befogásához. A legjobb eredmény érdekében helyezze magasra és ablak közelébe." "Győződjön meg arról, hogy az USB-tuner be van dugva, és csatlakoztatva van a televíziós jelforráshoz.\n\nAntenna használata esetén szükség lehet az elhelyezés, illetve irány módosítására a lehető legtöbb csatorna befogásához. A legjobb eredmény érdekében helyezze magasra és ablak közelébe." + "Ellenőrizze, hogy a hálózati tuner be van-e kapcsolva, és csatlakozik-e televíziós jelforráshoz.\n\nHa antennát használ, módosítsa az elhelyezkedését, illetve irányát, hogy minél több csatornát fogjon. A legjobb eredmény érdekében helyezze magasra és ablak közelébe." "Folytatás" "Most nem" @@ -40,6 +38,7 @@ "Újra végrehajtja a csatornabeállítást?" "Ezzel eltávolítja a tévétuner segítségével megtalált csatornákat, és új csatornakeresést indít.\n\nEllenőrizze, hogy tévéje csatlakoztatva van-e a televíziós jelforráshoz.\n\nAntenna használata esetén szükség lehet az elhelyezés, illetve az irány módosítására a lehető legtöbb csatorna befogásához. A legjobb eredmény érdekében helyezze magasra és ablak közelébe." "Ezzel eltávolítja a már megtalált csatornákat az USB-tunerről, és újból elvégzi a csatornakeresést.\n\nGyőződjön meg arról, hogy az USB-tuner be van dugva, és csatlakoztatva van a televíziós jelforráshoz.\n\nAntenna használata esetén szükség lehet az elhelyezés, illetve irány módosítására a lehető legtöbb csatorna befogásához. A legjobb eredmény érdekében helyezze magasra és ablak közelébe." + "Ezzel eltávolítja a hálózati tuner által talált csatornákat, és újabb csatornakeresést indít el.\n\nEllenőrizze, hogy a hálózati tuner be van-e kapcsolva, és csatlakozik-e televíziós jelforráshoz.\n\nHa antennát használ, módosítsa az elhelyezkedését, illetve irányát, hogy minél több csatornát fogjon. A legjobb eredmény érdekében helyezze magasra és ablak közelébe." "Folytatás" "Mégse" @@ -54,6 +53,7 @@ "Tévétuner beállítása" "USB-s csatornatuner beállítása" + "Hálózati csatornatuner beállítása" "Ez néhány percet is igénybe vehet" "A tuner átmenetileg nem áll rendelkezésre, vagy már fel lett használva a felvétel során." @@ -76,6 +76,7 @@ "Nem található csatorna" "A keresés nem talált csatornát. Ellenőrizze, hogy tévéje csatlakoztatva van-e a televíziós jelforráshoz.\n\nAntenna használata esetén módosítsa annak elhelyezését, illetve irányát. A legjobb eredmény érdekében helyezze magasra és ablak közelébe, majd ismételje meg a keresést." "A keresés nem talált csatornát. Győződjön meg arról, hogy az USB-tuner be van dugva, és csatlakoztatva van a televíziós jelforráshoz.\n\nHa antennát használ, állítson annak helyzetén, illetve irányán. A legjobb eredmény érdekében helyezze magasra és ablak közelébe, majd ismételje meg a keresést." + "A rendszer nem talált egyetlen csatornát sem. Ellenőrizze, hogy a hálózati tuner be van-e kapcsolva, és csatlakozik-e televíziós jelforráshoz.\n\nHa antennát használ, módosítsa az elhelyezkedését, illetve irányát. A legjobb eredmény érdekében helyezze magasra és ablak közelébe, majd ismételje meg a keresést." "Keresés újra" "Kész" @@ -83,5 +84,7 @@ "Tévécsatornák keresése" "Tévétuner beállítása" "USB-s tévétuner beállítása" - "Megszakadt a kapcsolat az USB-s tévétunerrel." + "Hálózati tévétuner beállítása" + "Megszakadt a kapcsolat az USB-s tévétunerrel." + "Megszakadt a kapcsolat a hálózati tunerrel." diff --git a/usbtuner-res/values-hy-rAM/strings.xml b/usbtuner-res/values-hy-rAM/strings.xml index a8e33eac..6f4c3aac 100644 --- a/usbtuner-res/values-hy-rAM/strings.xml +++ b/usbtuner-res/values-hy-rAM/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "Հեռուստակարգավորիչ" "USB հեռուստակարգավորիչ" - "Միացնել" - "Անջատել" + "Ցանցային հեռուստաընդունիչ (ԲԵՏԱ)" "Սպասեք՝ մինչ որոնումը ավարտվի" - "Ընտրեք ալիքի աղբյուրը" - "Ազդանշան չկա" - "Չհաջողվեց անցնել %s ալիքին" - "Չհաջողվեց անցնել ալիքին" "Ընդունիչի ծրագրակազմը վերջերս թարմացվել է: Նորից որոնեք ալիքները:" "Ձայնը միացնելու համար համակարգի ձայնի կարգավորումներում ակտիվացրեք ծավալային ձայնը" + "Հնարավոր չէ վերարտադրել ձայնը: Փորձեք մեկ այլ հեռուստացույց:" "Ալիքների կարգավորիչի տեղադրում" "Հեռուստակարգավորիչի տեղադրում" "Ալիքների USB կարգավորիչի տեղադրում" + "Ցանցային ընդունիչի կարգավորում" "Համոզվեք, որ հեռուստացույցը միացված է հեռուստատեսային ազդանշանի աղբյուրին:\n\nԵթերային ալեհավաք օգտագործելիս հնարավոր է՝ անհրաժեշտ լինի կարգավորել դրա դիրքն ու ուղղությունը՝ ավելի շատ ալիքներ գտնելու համար: Լավագույն արդյունքներն ապահովելու համար այն դրեք բարձր տեղում՝ պատուհանի մոտ:" "Համոզվեք, որ USB կարգավորիչը միացված է ցանցին և հեռուստատեսային ազդանշանի աղբյուրին:\n\nԵթերային ալեհավաք օգտագործելիս հնարավոր է՝ անհրաժեշտ լինի կարգավորել դրա դիրքն ու ուղղությունը՝ ավելի շատ ալիքներ գտնելու համար: Լավագույն արդյունքներն ապահովելու համար այն դրեք բարձր տեղում՝ պատուհանի մոտ:" + "Համոզվեք, որ ցանցային ընդունիրչը միացված է և կապակցված հեռուստատեսային ազդանշանի աղբյուրին:\n\nԵթերային ալեհավաք օգտագործելիս հնարավոր է՝ անհրաժեշտ լինի կարգավորել դրա դիրքն ու ուղղությունը՝ ավելի շատ ալիքներ գտնելու համար: Լավագույն արդյունքներն ապահովելու համար այն դրեք բարձր տեղում՝ պատուհանի մոտ:" "Շարունակել" "Ոչ հիմա" @@ -40,6 +38,7 @@ "Կրկի՞ն կարգավորել ալիքները:" "Այս գործողության արդյունքում կհեռացվեն հեռուստակարգավորիչից ստացված ալիքները և կկատարվի ալիքների նոր որոնում:\n\nՀամոզվեք, որ հեռուստացույցը միացված է հեռուստատեսային ազդանշանի աղբյուրին:\n\nԵթերային ալեհավաք օգտագործելիս հնարավոր է՝ անհրաժեշտ լինի կարգավորել դրա դիրքն ու ուղղությունը՝ ավելի շատ ալիքներ գտնելու համար: Լավագույն արդյունքներն ապահովելու համար այն դրեք բարձր տեղում՝ պատուհանի մոտ:" "Այս գործողության արդյունքում կհեռացվեն USB կարգավորիչից ստացված ալիքները և կկատարվի ալիքների նոր որոնում:\n\nՀամոզվեք, որ USB կարգավորիչը միացված է ցանցին և հեռուստատեսային ազդանշանի աղբյուրին:\n\nԵթերային ալեհավաք օգտագործելիս հնարավոր է՝ անհրաժեշտ լինի կարգավորել դրա դիրքն ու ուղղությունը՝ ավելի շատ ալիքներ գտնելու համար: Լավագույն արդյունքներն ապահովելու համար այն դրեք բարձր տեղում՝ պատուհանի մոտ:" + "Այս գործողության արդյունքում կհեռացվեն ցանցային ընդունիչից ստացված ալիքները և կկատարվի ալիքների նոր որոնում:\n\nՀամոզվեք, որ ցանցային ընդունիչը միացված է և կապակցված հեռուստատեսային ազդանշանի աղբյուրին:\n\nԵթերային ալեհավաք օգտագործելիս հնարավոր է՝ անհրաժեշտ լինի կարգավորել դրա դիրքն ու ուղղությունը՝ ավելի շատ ալիքներ գտնելու համար: Լավագույն արդյունքներն ապահովելու համար այն դրեք բարձր տեղում՝ պատուհանի մոտ:" "Շարունակել" "Չեղարկել" @@ -54,6 +53,7 @@ "Հեռուստակարգավորիչի տեղադրում" "Ալիքների USB կարգավորիչի տեղադրում" + "Ցանցային ալիքների ընդունիչի տեղադրում" "Դա կարող է տևել մի քանի րոպե" "Ընդունիչը ժամանակավորապես անհասանելի է կամ արդեն օգտագործվում է տեսագրելու համար:" @@ -76,6 +76,7 @@ "Ալիքներ չեն գտնվել" "Որոնման արդյունքում ալիքներ չեն գտնվել: Համոզվեք, որ հեռուստացույցը միացված է հեռուստատեսային ազդանշանի աղբյուրին:\n\nԵթերային ալեհավաք օգտագործելիս կարգավորեք դրա դիրքն ու ուղղությունը: Լավագույն արդյունքներն ապահովելու համար այն դրեք բարձր տեղում՝ պատուհանի մոտ, ապա որոնեք նորից:" "Որոնման արդյունքում ալիքներ չեն գտնվել: Համոզվեք, որ USB կարգավորիչը միացված է ցանցին և հեռուստատեսային ազդանշանի աղբյուրին:\n\nԵթերային ալեհավաք օգտագործելիս կարգավորեք դրա դիրքն ու ուղղությունը: Լավագույն արդյունքներն ապահովելու համար այն դրեք բարձր տեղում՝ պատուհանի մոտ, ապա որոնեք նորից:" + "Որոնման արդյունքում ալիքներ չեն գտնվել: Համոզվեք, որ ցանցային ընդունիչը միացված է և կապակցված հեռուստատեսային ազդանշանի աղբյուրին:\n\nԵթերային ալեհավաք օգտագործելիս կարգավորեք դրա դիրքն ու ուղղությունը: Լավագույն արդյունքներն ապահովելու համար այն դրեք բարձր տեղում՝ պատուհանի մոտ, ապա որոնեք նորից:" "Կրկին որոնել" "Պատրաստ է" @@ -83,5 +84,7 @@ "Հեռուստաալիքների որոնում" "Հեռուստակարգավորիչի տեղադրում" "USB հեռուստակարգավորիչի տեղադրում" - "USB հեռուստակարգավորիչն անջատված է:" + "Ցանցային հեռուստաընդունիչի կարգավորում" + "USB հեռուստաընդունիչն անջատված է:" + "Ցանցային ընդունիչն անջատված է։" diff --git a/usbtuner-res/values-in/strings.xml b/usbtuner-res/values-in/strings.xml index e534c9c6..7fe7eea7 100644 --- a/usbtuner-res/values-in/strings.xml +++ b/usbtuner-res/values-in/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "Tuner TV" "Tuner TV USB" - "Aktif" - "Nonaktif" + "Tuner TV Jaringan (BETA)" "Harap tunggu sampai pemrosesan selesai" - "Pilih sumber saluran Anda" - "Tidak Ada Sinyal" - "Gagal menyetel ke %s" - "Gagal menyetel" "Perangkat lunak tuner ini baru saja diperbarui. Pindai ulang saluran Anda." "Aktifkan suara surround di setelan suara sistem untuk mengaktifkan audio" + "Tidak dapat memutar audio. Coba TV lainnya" "Penyiapan penyetel saluran" "Penyiapan Tuner TV" "Penyiapan tuner saluran USB" + "Penyiapan tuner jaringan" "Pastikan TV terhubung ke sumber sinyal TV.\n\nJika menggunakan antena udara, Anda mungkin perlu menyesuaikan tempat atau arahnya untuk menerima sebagian besar saluran. Untuk hasil terbaik, tempatkan di lokasi yang tinggi dan dekat dengan jendela." "Pastikan tuner USB dicolokkan dan tersambung ke sumber sinyal TV.\n\nJika menggunakan antena udara, Anda mungkin perlu menyesuaikan tempat atau arahnya untuk menerima jumlah saluran paling banyak. Untuk hasil terbaik, letakkan di tempat yang tinggi dan dekat jendela." + "Pastikan tuner jaringan aktif dan terhubung ke sumber sinyal TV.\n\nJika menggunakan antena udara, Anda mungkin perlu menyesuaikan tempat dan arahnya agar dapat menerima sebagian besar saluran. Untuk hasil terbaik, tempatkan antena di lokasi yang tinggi dan dekat jendela." "Lanjutkan" "Jangan sekarang" @@ -40,6 +38,7 @@ "Jalankan lagi penyiapan saluran?" "Tindakan ini akan menghapus saluran yang ditemukan dari tuner TV dan memindai saluran baru lagi.\n\nPastikan TV terhubung ke sumber sinyal TV.\n\nJika menggunakan antena udara, mungkin Anda perlu menyesuaikan tempat atau arahnya untuk menerima sebagian besar saluran. Untuk hasil terbaik, tempatkan di lokasi yang tinggi dan dekat dengan jendela." "Tindakan ini akan menghapus saluran yang ditemukan dari tuner USB dan memindai saluran baru lagi.\n\nPastikan tuner USB dicolokkan dan tersambung ke sumber sinyal TV.\n\nJika menggunakan antena udara, Anda mungkin perlu menyesuaikan tempat atau arahnya untuk menerima jumlah saluran paling banyak. Untuk hasil terbaik, letakkan di tempat yang tinggi dan dekat jendela." + "Hal ini akan menghapus saluran yang ditemukan dari tuner jaringan dan memindai saluran baru lagi.\n\nPastikan tuner jaringan aktif dan terhubung ke sumber sinyal TV.\n\nJika menggunakan antena udara, Anda mungkin perlu menyesuaikan penempatan atau arahnya agar dapat menerima sebagian besar saluran. Untuk hasil terbaik, tempatkan antena di lokasi yang tinggi dan dekat dengan jendela." "Lanjutkan" "Batal" @@ -54,6 +53,7 @@ "Penyiapan tuner TV" "Penyiapan tuner saluran USB" + "Penyiapan tuner saluran jaringan" "Proses ini dapat memakan waktu beberapa menit" "Tuner sementara tidak tersedia atau sudah digunakan oleh rekaman." @@ -76,6 +76,7 @@ "Tidak ditemukan Saluran" "Pemindaian tidak menemukan saluran apa pun. Pastikan TV terhubung ke sumber sinyal TV.\n\nJika menggunakan antena udara, sesuaikan tempat atau arahnya. Untuk hasil terbaik, tempatkan di lokasi yang tinggi dan dekat dengan jendela kemudian pindai lagi." "Pemindaian tidak menemukan saluran apa pun. Pastikan tuner USB dicolokkan dan tersambung ke sumber sinyal TV.\n\nJika menggunakan antena udara, sesuaikan tempat atau arahnya. Untuk hasil terbaik, letakkan di tempat yang tinggi dan dekat jendela lalu pindai lagi." + "Tidak menemukan saluran apa pun saat memindai. Pastikan tuner jaringan aktif dan terhubung ke sumber sinyal TV.\n\nJika Anda menggunakan antena udara, sesuaikan penempatan atau arahnya. Untuk hasil terbaik, tempatkan di lokasi yang tinggi dan dekat jendela, lalu pindai lagi." "Pindai lagi" "Selesai" @@ -83,5 +84,7 @@ "Pindai saluran TV" "Penyiapan Tuner TV" "Penyiapan Tuner TV USB" - "Koneksi tuner TV USB terputus." + "Penyiapan Tuner TV Jaringan" + "Koneksi tuner TV USB terputus." + "Koneksi tuner jaringan terputus." diff --git a/usbtuner-res/values-is-rIS/strings.xml b/usbtuner-res/values-is-rIS/strings.xml index f63d9810..f9d1188b 100644 --- a/usbtuner-res/values-is-rIS/strings.xml +++ b/usbtuner-res/values-is-rIS/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "Sjónvarpsmóttakari" "USB-sjónvarpsmóttakari" - "Kveikja" - "Slökkva" + "Netsjónvarpsmóttakari (TILRAUNAÚTGÁFA)" "Bíddu þar til vinnslu lýkur" - "Veldu inntak rása" - "Ekkert merki" - "Mistókst að stilla á %s" - "Mistókst að stilla" "Hugbúnaður sjónvarpsmóttakarans var uppfærður nýlega. Leitaðu aftur að rásum." "Kveikja á víðómastillingu í hljóðstillingum kerfisins til að kveikja á hljóði" + "Ekki er hægt að spila hljóð. Prófaðu annað sjónvarp" "Uppsetning sjónvarpskorts" "Uppsetning sjónvarpsmóttakara" "Uppsetning USB-sjónvarpsrásakorts" + "Uppsetning netsjónvarpsmóttakara" "Gakktu úr skugga um að sjónvarpið sé tengt við sjónvarpstengi.\n\nEf þú ert að nota loftnet skaltu færa það á annan stað eða snúa því í aðra átt. Best er að hafa það hátt uppi og nálægt glugga þegar leitað er." "Gakktu úr skugga um að USB-sjónvarpskortið sé í sambandi og tengt við sjónvarpstengi.\n\nEf þú ert að nota loftnet skaltu færa það á annan stað eða snúa því í aðra átt til að ná fleiri rásum. Best er að hafa það hátt uppi og nálægt glugga." + "Gakktu úr skugga um að kveikt sé á netsjónvarpsmóttakaranum og að hann sé tengdur við sjónvarpstengi.\n\nEf þú ert að nota loftnet skaltu færa það á annan stað eða snúa því í aðra átt. Best er að hafa það hátt uppi og nálægt glugga þegar leitað er." "Halda áfram" "Ekki núna" @@ -40,6 +38,7 @@ "Viltu keyra rásauppsetningu aftur?" "Þetta fjarlægir stöðvar sem sjónvarpsmóttakarinn fann og leitar aftur að nýjum stöðvum.\n\nGakktu úr skugga um að sjónvarpið sé tengt við sjónvarpstengi.\n\nEf þú ert að nota loftnet skaltu færa það á annan stað eða snúa því í aðra átt. Best er að hafa það hátt uppi og nálægt glugga þegar leitað er." "Þetta fjarlægir rásir sem skráðar eru á USB-sjónvarpskortinu og leitar að nýjum stöðvum.\n\nGakktu úr skugga um að USB-sjónvarpskortið sé í sambandi og tengt við sjónvarpstengi.\n\nEf þú ert að nota loftnet skaltu færa það á annan stað eða snúa því í aðra átt til að ná fleiri rásum. Best er að hafa það hátt uppi og nálægt glugga." + "Þetta fjarlægir rásirnar sem sjónvarpsmóttakarinn þinn fann og leitar aftur að nýjum rásum.\n\nGakktu úr skugga um að kveikt sé á netsjónvarpsmóttakaranum og að hann sé tengdur við sjónvarpstengi.\n\nEf þú ert að nota loftnet skaltu færa það á annan stað eða snúa því í aðra átt. Best er að hafa það hátt uppi og nálægt glugga þegar leitað er." "Halda áfram" "Hætta við" @@ -54,6 +53,7 @@ "Uppsetning sjónvarpsmóttakara" "Uppsetning USB-sjónvarpsrásakorts" + "Uppsetning netsjónvarpsmóttakara" "Þetta getur tekið nokkrar mínútur" "Móttakari er tímabundið ekki í boði eða er þegar að taka upp." @@ -76,6 +76,7 @@ "Engar rásir fundust" "Engar rásir fundust. Gakktu úr skugga um að sjónvarpið sé tengt við sjónvarpstengi.\n\nEf þú ert að nota loftnet skaltu færa það á annan stað eða snúa því í aðra átt. Best er að hafa það hátt uppi og nálægt glugga þegar leitað er." "Engar rásir fundust. Gakktu úr skugga um að USB-sjónvarpskortið sé í sambandi og tengt við sjónvarpstengi.\n\nEf þú ert að nota loftnet skaltu færa það á annan stað eða snúa því í aðra átt. Best er að hafa það hátt uppi og nálægt glugga þegar leitað er." + "Leitin fann engar stöðvar. Gakktu úr skugga um að kveikt sé á netsjónvarpsmóttakaranum og hann sé tengdur við sjónvarpstengi.\n\nEf þú ert að nota loftnet skaltu færa það á annan stað eða snúa því í aðra átt. Best er að hafa það hátt uppi og nálægt glugga þegar leitað er." "Leita aftur" "Lokið" @@ -83,5 +84,7 @@ "Skanna eftir sjónvarpsstöðvum" "Uppsetning sjónvarpsmóttakara" "Uppsetning USB-sjónvarpsmóttakara" - "USB-sjónvarpsmóttakari tekinn úr sambandi." + "Uppsetning netsjónvarpsmóttakara" + "USB-sjónvarpsmóttakari tekinn úr sambandi." + "Netmóttakari tekinn úr sambandi." diff --git a/usbtuner-res/values-it/strings.xml b/usbtuner-res/values-it/strings.xml index 7787d019..fdefdeb1 100644 --- a/usbtuner-res/values-it/strings.xml +++ b/usbtuner-res/values-it/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "Sintonizzatore TV" "Sintonizzatore TV USB" - "Attiva" - "Disattiva" + "Network TV Tuner (BETA)" "Attendi il completamento dell\'elaborazione" - "Seleziona la tua fonte canale" - "Nessun segnale" - "Sintonizzazione su %s non riuscita" - "Sintonizzazione non riuscita" "Il software del sintonizzatore è stato aggiornato di recente. Esegui nuovamente la scansione dei canali." "Per attivare l\'audio, attiva l\'audio surround nelle impostazioni del sistema" + "Impossibile riprodurre l\'audio. Prova con un\'altra TV" "Configurazione del sintonizzatore di canali" "Configurazione del sintonizzatore TV" "Configurazione del sintonizzatore di canali USB" + "Configurazione del sintonizzatore di rete" "Verifica che la TV sia connessa a un\'origine del segnale TV.\n\nSe utilizzi un\'antenna OTA, regolane la posizione o la direzione per ricevere la maggior parte dei canali. Per ottenere risultati ottimali, posizionala in alto e vicino a una finestra." "Verifica che il sintonizzatore USB sia collegato e connesso a un\'origine del segnale TV.\n\nSe utilizzi un\'antenna OTA, potresti dover regolare la sua posizione o direzione per ricevere la maggior parte dei canali. Per risultati ottimali, posizionala in alto e vicino a una finestra." + "Verifica che il sintonizzatore di rete sia acceso e connesso a un\'origine del segnale TV.\n\nSe utilizzi un\'antenna OTA (over-the-air), potresti doverne regolare la posizione o la direzione per ricevere il maggior numero di canali. Per ottenere risultati ottimali, posizionala in alto, vicino a una finestra." "Continua" "Non ora" @@ -40,6 +38,7 @@ "Eseguire nuovamente la configurazione dei canali?" "Questa operazione rimuoverà i canali trovati dal sintonizzatore TV ed eseguirà la ricerca di nuovi canali.\n\nVerifica che la TV sia connessa a un\'origine del segnale TV.\n\nSe utilizzi un\'antenna OTA, regolane la posizione o la direzione per ricevere la maggior parte dei canali. Per ottenere risultati ottimali, posizionala in alto e vicino a una finestra." "Questa operazione rimuoverà i canali trovati dal sintonizzatore USB ed eseguirà la ricerca di nuovi canali.\n\nVerifica che il sintonizzatore USB sia collegato e connesso a un\'origine del segnale TV.\n\nSe utilizzi un\'antenna OTA, potresti dover regolare la sua posizione o direzione per ricevere il maggior numero di canali. Per ottenere risultati ottimali, posizionala in alto e vicino a una finestra." + "I canali trovati dal sintonizzatore di rete verranno rimossi e verrà eseguita nuovamente la ricerca di nuovi canali.\n\nVerifica che il sintonizzatore di rete sia acceso e connesso a un\'origine del segnale TV.\n\nSe utilizzi un\'antenna OTA (over-the-air), potresti doverne regolare la posizione o la direzione per ricevere il maggior numero di canali. Per ottenere risultati ottimali, posizionala in alto, vicino a una finestra." "Continua" "Annulla" @@ -54,6 +53,7 @@ "Configurazione del sintonizzatore TV" "Configurazione del sintonizzatore di canali USB" + "Configurazione del sintonizzatore di canali della rete" "L\'operazione potrebbe richiedere alcuni minuti" "Il sintonizzatore è temporaneamente non disponibile o già utilizzato dal registratore." @@ -76,6 +76,7 @@ "Nessun canale trovato" "Nessun canale trovato durante la ricerca. Verifica che la TV sia connessa a un\'origine del segnale TV.\n\nSe utilizzi un\'antenna OTA, regolane la posizione o la direzione. Per ottenere risultati ottimali, posizionala in alto, vicino a una finestra ed esegui nuovamente la ricerca." "Nessun canale trovato durante la ricerca. Verifica che il sintonizzatore USB sia collegato e connesso a un\'origine del segnale TV.\n\nSe utilizzi un\'antenna OTA (over-the-air), regolane la posizione o la direzione. Per ottenere risultati ottimali, posizionala in alto, vicino a una finestra ed esegui nuovamente la ricerca." + "Nessun canale trovato durante la ricerca. Verifica che il sintonizzatore di rete sia acceso e connesso a un\'origine del segnale TV.\n\nSe utilizzi un\'antenna OTA (over-the-air), regolane la posizione o la direzione. Per ottenere risultati ottimali, posizionala in alto, vicino a una finestra ed esegui nuovamente la ricerca." "Cerca di nuovo" "Fine" @@ -83,5 +84,7 @@ "Cerca canali TV" "Configurazione del sintonizzatore TV" "Configurazione del sintonizzatore TV USB" - "Sintonizzatore TV USB disconnesso." + "Configurazione di Network TV Tuner" + "Sintonizzatore TV USB disconnesso." + "Sintonizzatore di rete disconnesso." diff --git a/usbtuner-res/values-iw/strings.xml b/usbtuner-res/values-iw/strings.xml index 5dab5f15..631110e8 100644 --- a/usbtuner-res/values-iw/strings.xml +++ b/usbtuner-res/values-iw/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "טיונר טלוויזיה" "‏טיונר ה-USB בטלוויזיה" - "מופעל" - "כבוי" + "טיונר טלוויזיה לרשת (ביטא)" "המתן לסיום העיבוד" - "בחר את מקור הערוצים" - "אין אות" - "לא ניתן היה לכוון לערוץ %s" - "הכוונון נכשל" "תוכנת הטיונר עודכנה לאחרונה. סרוק מחדש את הערוצים." "הפעל סראונד בהגדרות צלילי מערכת כדי להפעיל אודיו" + "אין אפשרות להשמיע אודיו. נסה ערוץ טלוויזיה אחר." "הגדרת טיונר ערוצים" "הגדרת טיונר טלוויזיה" "‏הגדרת טיונר ערוצים בחיבור USB" + "הגדרת מקלט הרשת" "ודא שהטלוויזיה מחוברת למקור אות טלוויזיה.\n\nאם אתה משתמש באנטנה אלחוטית, ייתכן שיהיה עליך לשנות את המיקום או את הכיוון שלה כדי לקלוט כמה שיותר ערוצים. לקבלת התוצאות הטובות ביותר, הצב אותה במקום גבוה ליד חלון." "‏ודא שטיונר USB מחובר למקור אות בטלוויזיה. \n\n אם אתה משתמש באנטנה אלחוטית, ייתכן שיהיה עליך לכוון את מיקומה או את כיוונה כדי לקלוט כמה שיותר ערוצים. לתוצאות מיטביות, הצב אותה במקום גבוה ליד חלון וסרוק שוב." + "ודא שמקלט הרשת מופעל ומחובר למקור אות של טלוויזיה.\n\nאם אתה משתמש באנטנה לקליטת שידורים אלחוטיים, ייתכן שיהיה עליך לשנות את המיקום או את הכיוון שלה כדי לקלוט כמה שיותר ערוצים. לקבלת התוצאות הטובות ביותר, הצב אותה במקום גבוה ליד חלון." "המשך" "לא עכשיו" @@ -40,6 +38,7 @@ "האם להפעיל מחדש את הגדרת הערוצים?" "פעולה זו תסיר את הערוצים שנמצאו בטיונר הטלוויזיה ותסרוק שוב ערוצים חדשים.\n\nודא שהטלוויזיה מחוברת למקור אות טלוויזיה.\n\nאם אתה משתמש באנטנה אלחוטית, ייתכן שיהיה עליך לשנות את המיקום או את הכיוון שלה כדי לקלוט כמה שיותר ערוצים. לקבלת התוצאות הטובות ביותר, הצב אותה במקום גבוה ליד חלון." "‏פעולה זו תסיר את הערוצים שנמצאו מטיונר ה-USB ותתבצע סריקה נוספת לערוצים חדשים.\n\nודא שטיונר USB מחובר למקור אות בטלוויזיה.\n\nאם אתה משתמש באנטנה אלחוטית, ייתכן שיהיה עליך לשנות את מיקומה או את כיוונה כדי לקלוט כמה שיותר ערוצים. לתוצאות מיטביות, הצב אותה במקום גבוה ליד חלון וסרוק שוב." + "הפעולה תסיר את הערוצים שנמצאו ממקלט הרשת ותתבצע שוב סריקה לאיתור ערוצים חדשים.\n\nודא שמקלט הרשת מופעל ומחובר למקור אות של טלוויזיה.\n\nאם אתה משתמש באנטנה לקליטת שידורים אלחוטיים, ייתכן שיהיה עליך לשנות את המיקום או את הכיוון שלה כדי לקלוט כמה שיותר ערוצים. לקבלת התוצאות הטובות ביותר, הצב אותה במקום גבוה ליד חלון." "המשך" "ביטול" @@ -54,6 +53,7 @@ "הגדרת טיונר טלוויזיה" "‏הגדרת טיונר ערוצים בחיבור USB" + "הגדרת מקלט ערוצים לרשת" "פעולה זו עשויה להימשך מספר דקות" "הטיונר אינו זמין באופן זמני או שהוא כבר נמצא בשימוש של הקלטה." @@ -82,6 +82,7 @@ "לא נמצאו ערוצים" "בסריקה לא נמצאו ערוצים. ודא שהטלוויזיה מחוברת למקור אות טלוויזיה.\n\nאם אתה משתמש באנטנה אלחוטית, שנה את המיקום או את הכיוון שלה. לקבלת התוצאות הטובות ביותר, הצב אותה במקום גבוה ליד חלון וסרוק שוב." "‏לא נמצאו ערוצים בסריקה. ודא שטיונר USB מחובר למקור אות בטלוויזיה. \n\n אם אתה משתמש באנטנה אלחוטית, שנה את מיקומה או את כיוונה. לתוצאות מיטביות, הצב אותה במקום גבוה ליד חלון וסרוק שוב." + "בסריקה לא נמצאו ערוצים. ודא שמקלט הרשת מופעל ומחובר למקור אות של טלוויזיה.\n\nאם אתה משתמש באנטנה לקליטת שידורים אלחוטיים, שנה את המיקום או את הכיוון שלה. לקבלת התוצאות הטובות ביותר, הצב אותה במקום גבוה ליד חלון וסרוק שוב." "סרוק שוב" "סיום" @@ -89,5 +90,7 @@ "סריקה לאיתור ערוצי טלוויזיה" "הגדרת טיונר טלוויזיה" "‏הגדרת טיונר ה-USB בטלוויזיה" - "‏טיונר ה-USB שבטלוויזיה מנותק." + "הגדרת מקלט טלוויזיה לרשת" + "‏טיונר ה-USB שבטלוויזיה מנותק." + "טיונר הרשת מנותק." diff --git a/usbtuner-res/values-ja/strings.xml b/usbtuner-res/values-ja/strings.xml index 88af255a..d850d4ce 100644 --- a/usbtuner-res/values-ja/strings.xml +++ b/usbtuner-res/values-ja/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "テレビ チューナー" "USB テレビ チューナー" - "ON" - "OFF" + "ネットワーク テレビ チューナー(ベータ版)" "処理が完了するまでこのままお待ちください" - "チャンネル ソースを選択してください" - "信号がありません" - "「%s」に合わせることができませんでした" - "合わせることができませんでした" "最近チューナー ソフトウェアが更新されています。チャンネルをもう一度スキャンしてください。" "音声を有効にするには、システムのサウンド設定でサラウンド サウンドをオンにしてください" + "音声を再生できません。別のテレビをお試しください" "チャンネル チューナーのセットアップ" "テレビ チューナーのセットアップ" "USB チャンネル チューナーのセットアップ" + "ネットワーク チューナーのセットアップ" "テレビがテレビの信号源に接続されていることを確認してください。\n\n無線アンテナを使用している場合は、チャンネルの多くを受信するためにアンテナの場所や方向の調節が必要になる場合があります。最良の結果を得るには、アンテナを窓際の高い位置に設置します。" "USB チューナーが電源に接続され、テレビの信号源に接続されていることを確認してください。\n\n無線アンテナを使用している場合は、チャンネルの多くを受信するためにアンテナの場所や方向の調節が必要になる場合があります。最良の結果を得るには、アンテナを窓際の高い位置に設置します。" + "ネットワーク チューナーの電源がオンになっていて、テレビの信号源に接続されていることを確認してください。\n\n無線アンテナを使用している場合は、チャンネルの多くを受信するためにアンテナの場所や方向の調節が必要になる場合があります。最良の結果を得るには、アンテナを窓際の高い位置に設置します。" "次へ" "後で" @@ -40,6 +38,7 @@ "もう一度チャンネルを設定しますか?" "テレビ チューナーで見つかったチャンネルを削除して新しいチャンネルをもう一度スキャンします。\n\nテレビがテレビの信号源に接続されていることを確認してください。\n\n無線アンテナを使用している場合は、チャンネルの多くを受信するためにアンテナの場所や方向の調節が必要になる場合があります。最良の結果を得るには、アンテナを窓際の高い位置に設置します。" "USB チューナーで見つかったチャンネルを削除して新しいチャンネルをもう一度スキャンします。\n\nUSB チューナーが電源に接続され、テレビの信号源に接続されていることを確認してください。\n\n無線アンテナを使用している場合は、チャンネルの多くを受信するためにアンテナの場所や方向の調節が必要になる場合があります。最良の結果を得るには、アンテナを窓際の高い位置に設置します。" + "ネットワーク チューナーで見つかったチャンネルを削除して新しいチャンネルをもう一度スキャンします。\n\nネットワーク チューナーの電源がオンになっていて、テレビの信号源に接続されていることを確認してください。\n\n無線アンテナを使用している場合は、チャンネルの多くを受信するためにアンテナの場所や方向の調節が必要になる場合があります。最良の結果を得るには、アンテナを窓際の高い位置に設置します。" "次へ" "キャンセル" @@ -54,6 +53,7 @@ "テレビ チューナーのセットアップ" "USB チャンネル チューナーのセットアップ" + "ネットワーク チャンネル チューナーのセットアップ" "この処理には数分かかることがあります" "チューナーを一時的に使用できないか、すでに録画に使用しています。" @@ -76,6 +76,7 @@ "チャンネルが見つかりませんでした" "スキャンの結果、チャンネルは見つかりませんでした。テレビがテレビの信号源に接続されていることを確認してください。\n\n無線アンテナを使用している場合は、アンテナの場所や方向を調節してください。最良の結果を得るには、アンテナを窓際の高い位置に設置してから、もう一度スキャンします。" "スキャンの結果、チャンネルは見つかりませんでした。USB チューナーが電源に接続され、テレビの信号源に接続されていることを確認してください。\n\n無線アンテナを使用している場合は、アンテナの場所や方向を調節してください。最良の結果を得るには、アンテナを窓際の高い位置に設置してから、もう一度スキャンします。" + "スキャンの結果、チャンネルは見つかりませんでした。ネットワーク チューナーの電源がオンになっていて、テレビの信号源に接続されていることを確認してください。\n\n無線アンテナを使用している場合は、アンテナの場所や方向を調節してください。最良の結果を得るには、アンテナを窓際の高い位置に設置してから、もう一度スキャンします。" "再スキャン" "完了" @@ -83,5 +84,7 @@ "テレビのチャンネルをスキャン" "テレビ チューナーのセットアップ" "USB テレビ チューナーのセットアップ" - "USB TV チューナーの接続が解除されています。" + "ネットワーク テレビ チューナーのセットアップ" + "USB テレビ チューナーの接続が解除されています。" + "ネットワーク チューナーの接続が解除されています。" diff --git a/usbtuner-res/values-ka-rGE/strings.xml b/usbtuner-res/values-ka-rGE/strings.xml index e09428d7..c2e3e846 100644 --- a/usbtuner-res/values-ka-rGE/strings.xml +++ b/usbtuner-res/values-ka-rGE/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "TV-ტუნერი" "USB TV-ტუნერი" - "ჩართვა" - "გამორთვა" + "ქსელის TV-ტუნერი (Beta)" "გთხოვთ, მოითმინოთ დამუშავების დასრულებამდე" - "აირჩიეთ არხების წყარო" - "სიგნალი არ არის" - "%s-ზე გადართვა ვერ მოხერხდა" - "არხზე გადართვა ვერ მოხერხდა" "ტუნერის პროგრამული უზრუნველყოფა ახლახან განახლდა. გთხოვთ, ხელახლა დაასკანიროთ არხები." "აუდიოს ჩასართავად, სისტემის ხმის პარამეტრებში ჩართეთ მოცულობითი ხმა" + "აუდიოს დაკვრა ვერ ხერხდება. გთხოვთ, ცადოთ სხვა არხი" "არხების ტუნერის დაყენება" "TV-ტუნერის დაყენება" "არხების USB ტუნერის დაყენება" + "ქსელის ტუნერის დაყენება" "დარწმუნდით, რომ თქვენი ტელევიზორი მიერთებულია სატელევიზიო სიგნალის წყაროსთან..\n\nტერესტრიული ანტენის გამოყენების შემთხვევაში, არხების უმეტესობის მისაღებად, მისი განლაგების ან მიმართულების დარეგულება მოგიწევთ. საუკეთესო შედეგის მისაღებად, განათავსეთ ის სიმაღლეზე, ფანჯარასთან ახლოს." "დარწმუნდით, რომ USB ტუნერი ბუდეშია და მიერთებულია სატელევიზიო სიგნალის წყაროსთან.\n\nტერესტრიული ანტენის გამოყენების შემთხვევაში, არხების უმეტესობის მისაღებად, მისი განლაგების ან მიმართულების დარეგულება მოგიწევთ. საუკეთესო შედეგის მისაღებად, განათავსეთ ის სიმაღლეზე, ფანჯარასთან ახლოს." + "დარწმუნდით, რომ ქსელის ტუნერი ჩართულია და მიერთებულია სატელევიზიო სიგნალის წყაროსთან.\n\nტერესტრიული ანტენის გამოყენების შემთხვევაში, არხების უმეტესობის მისაღებად, მისი განლაგების ან მიმართულების დარეგულირება მოგიწევთ. საუკეთესო შედეგის მისაღებად, განათავსეთ ის სიმაღლეზე, ფანჯარასთან ახლოს." "გაგრძელება" "ახლა არა" @@ -40,6 +38,7 @@ "გსურთ არხების ხელახლა დაყენება?" "ეს მოქმედება ამოშლის TV ტუნერით ნაპოვნ არხებს და ახალი არხების სკანირება ხელახლა მოხდება.\n\nდარწმუნდით, რომ თქვენი ტელევიზორი მიერთებულია სატელევიზიო სიგნალის წყაროსთან.\n\nტერესტრიული ანტენის გამოყენების შემთხვევაში, არხების უმეტესობის მისაღებად, მისი განლაგების ან მიმართულების დარეგულება მოგიწევთ. საუკეთესო შედეგის მისაღებად, განათავსეთ ის სიმაღლეზე, ფანჯარასთან ახლოს." "ეს მოქმედება ამოშლის USB ტუნერით ნაპოვნ არხებს და ხელახლა მოხდება ახალი არხების სკანირება.\n\nდარწმუნდით, რომ USB ტუნერი ბუდეშია და მიერთებულია სატელევიზიო სიგნალის წყაროსთან.\n\nტერესტრიული ანტენის გამოყენების შემთხვევაში, არხების უმეტესობის მისაღებად, მისი განლაგების ან მიმართულების დარეგულება მოგიწევთ. საუკეთესო შედეგის მისაღებად, განათავსეთ ის სიმაღლეზე, ფანჯარასთან ახლოს." + "ეს მოქმედება ამოშლის ქსელის ტუნერით ნაპოვნ არხებს და ახალი არხების სკანირება ხელახლა მოხდება.\n\nდარწმუნდით, რომ ქსელის ტუნერი ჩართულია და მიერთებულია სატელევიზიო სიგნალის წყაროსთან.\n\nტერესტრიული ანტენის გამოყენების შემთხვევაში, არხების უმეტესობის მისაღებად, მისი განლაგების ან მიმართულების დარეგულირება მოგიწევთ. საუკეთესო შედეგის მისაღებად, განათავსეთ ის სიმაღლეზე, ფანჯარასთან ახლოს." "გაგრძელება" "გაუქმება" @@ -54,6 +53,7 @@ "TV-ტუნერის დაყენება" "არხების USB ტუნერის დაყენება" + "არხების ქსელის ტუნერის დაყენება" "ამას შეიძლება რამდენიმე წუთი დასჭირდეს" "ტუნერი დროებით მიუწვდომელია, ან უკვე გამოიყენება ჩასაწერად." @@ -76,6 +76,7 @@ "არხები ვერ მოიძებნა" "სკანირებისას არხები ვერ მოიძებნა. დარწმუნდით, რომ თქვენი ტელევიზორი მიერთებულია სატელევიზიო სიგნალის წყაროსთან.\n\nტერესტრიული ანტენის გამოყენების შემთხვევაში, დაარეგულირეთ მისი განლაგება ან მიმართულება. საუკეთესო შედეგის მისაღებად, განათავსეთ ის სიმაღლეზე, ფანჯარასთან ახლოს და ხელახლა დაასკანირეთ." "სკანირებისას არხები ვერ მოიძებნა. დარწმუნდით, რომ USB ტუნერი ბუდეშია და მიერთებულია სატელევიზიო სიგნალის წყაროსთან. \n\nტერესტრიული ანტენის გამოყენების შემთხვევაში, დაარეგულირეთ მისი განლაგება ან მიმართულება. საუკეთესო შედეგის მისაღებად, განათავსეთ ის სიმაღლეზე, ფანჯარასთან ახლოს და ხელახლა დაასკანირეთ." + "სკანირებისას არხები ვერ მოიძებნა. დარწმუნდით, რომ ქსელის ტუნერი ჩართულია და მიერთებულია სატელევიზიო სიგნალის წყაროსთან. \n\nტერესტრიული ანტენის გამოყენების შემთხვევაში, დაარეგულირეთ მისი განლაგება ან მიმართულება. საუკეთესო შედეგის მისაღებად, განათავსეთ ის სიმაღლეზე, ფანჯარასთან ახლოს და ხელახლა დაასკანირეთ." "ხელახლა სკანირება" "მზადაა" @@ -83,5 +84,7 @@ "სატელევიზიო არხების სკანირება" "TV-ტუნერის დაყენება" "USB TV-ტუნერის დაყენება" - "USB TV-ტუნერი გაითიშა." + "ქსელის TV-ტუნერის დაყენება" + "USB TV-ტუნერი გაითიშა." + "ქსელის ტუნერი გაითიშა." diff --git a/usbtuner-res/values-kk-rKZ/strings.xml b/usbtuner-res/values-kk-rKZ/strings.xml index 3c2ba4f3..7b9fced0 100644 --- a/usbtuner-res/values-kk-rKZ/strings.xml +++ b/usbtuner-res/values-kk-rKZ/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "ТД тюнері" "USB TД тюнері" - "Қосу" - "Өшіру" + "Желілік теледидар тюнері (БЕТА НҰСҚАСЫ)" "Өңдеу аяқталғанша күте тұрыңыз" - "Арна көзін таңдаңыз" - "Сигнал жоқ" - "%s арнасына реттелмеді" - "Реттелмеді" "Тюнердің бағдарламалық құралы жақында жаңартылды. Арналарды қайта іздеңіз." "Аудиомазмұнды қосу үшін жүйенің параметрлерінде көлемдік дыбысты қосыңыз" + "Аудиомазмұн ойнатылмайды. Басқа теледидар арнасын қосып көріңіз" "Арна тюнерін орнату" "ТД тюнерін орнату" "USB арна тюнерін орнату" + "Желі тюнерін реттеу" "Теледидардың ТД сигнал көзіне қосылғанын тексеріңіз.\n\nСымсыз антеннаны пайдалансаңыз, көптеген арналарды қабылдау үшін оның орнын немесе бағытын реттеу қажет болады. Ең жақсы нәтижеге қол жеткізу үшін оны жоғарырақ және терезеге жақын орналастырыңыз." "USB тюнерін жалғанғанын және ТД сигнал көзіне қосылғанын тексеріңіз.\n\nСымсыз антеннаны пайдалансаңыз, көптеген арналарды қабылдау үшін оның орнын немесе бағытын реттеу қажет болады. Ең жақсы нәтижеге қол жеткізу үшін оны жоғарырақ және терезеге жақын орналастырыңыз." + "USB тюнері қосылып, теледидардың сигнал көзіне жалғанғанын тексеріңіз.\n\nСымсыз антеннаны пайдалансаңыз, көптеген арналарды қабылдау үшін оның орнын ауыстырыңыз немесе бағытын реттеңіз. Ең жақсы нәтижеге қол жеткізу үшін оны жоғарырақ және терезеге жақын жерге орналастырыңыз." "Жалғастыру" "Қазір емес" @@ -40,6 +38,7 @@ "Арналарды қайта орнату қажет пе?" "ТД тюнерінде табылған арналар жойылып, жаңа арналар ізделеді.\n\nТеледидардың ТД сигнал көзіне қосылғанын тексеріңіз.\n\nСымсыз антеннаны пайдалансаңыз, көптеген арналарды қабылдау үшін оның орнын немесе бағытын реттеу қажет болады. Ең жақсы нәтижеге қол жеткізу үшін оны жоғарырақ және терезеге жақын орналастырыңыз." "USB тюнерінде табылған арналар жойылып, жаңа арналар ізделеді.\n\nUSB тюнері жалғанғанын және ТД сигнал көзіне қосылғанын тексеріңіз.\n\nСымсыз антеннаны пайдалансаңыз, көптеген арналарды қабылдау үшін оның орнын немесе бағытын реттеу қажет болады. Ең жақсы нәтижеге қол жеткізу үшін оны жоғарырақ және терезеге жақын орналастырыңыз." + "Желі тюнерінде табылған арналар өшіріліп, жаңа арналар ізделеді.\n\nЖелі тюнері қосылып, теледидардың сигнал көзіне жалғанғанын тексеріңіз.\n\nСымсыз антеннаны пайдалансаңыз, көптеген арналарды қабылдау үшін оның орнын ауыстырыңыз немесе бағытын реттеңіз. Ең жақсы нәтижеге қол жеткізу үшін оны жоғарырақ және терезеге жақын жерге орналастырыңыз." "Жалғастыру" "Бас тарту" @@ -54,8 +53,9 @@ "ТД тюнерін орнату" "USB арна тюнерін орнату" + "Желілік арна тюнерін реттеу" "Бұл бірнеше минутты алуы мүмкін" - "Тюнер қол жетімсіз немесе жазу барысында қолданылуда." + "Тюнер қолжетімсіз немесе жазу барысында қолданылуда." %1$d арна табылды %1$d арна табылды @@ -76,6 +76,7 @@ "Арналар табылмады" "Іздеу нәтижесінде ешқандай арна табылмады. Теледидардың ТД сигнал көзіне қосылғанын тексеріңіз.\n\nСымсыз антеннаны пайдалансаңыз, орнын немесе бағытын реттеңіз. Ең жақсы нәтижеге қол жеткізу үшін оны жоғарырақ және терезеге жақын орналастырып, қайта іздеп көріңіз." "Іздеу нәтижесінде арналар табылмады. USB тюнері жалғанғанын және ТД сигнал көзіне қосылғанын тексеріңіз.\n\nСымсыз антеннаны пайдалансаңыз, орнын немесе бағытын реттеңіз. Ең жақсы нәтижеге қол жеткізу үшін оны жоғарырақ және терезеге жақын орналастырып, қайта іздеп көріңіз." + "Іздеу нәтижесінде арналар табылмады. Желі тюнері қосылып, теледидардың сигнал көзіне жалғанғанын тексеріңіз.\n\nСымсыз антеннаны пайдалансаңыз, оның орнын ауыстырыңыз немесе бағытын реттеңіз. Ең жақсы нәтижеге қол жеткізу үшін оны жоғарырақ және терезеге жақын жерге орналастырып, қайта іздеп көріңіз." "Қайта іздеу" "Орындалды" @@ -83,5 +84,7 @@ "ТД арналарын іздеу" "ТД тюнерін орнату" "USB TД тюнерін орнату" - "USB TД тюнері ажыратылды." + "Желілік ТД тюнерін реттеу" + "USB TД тюнері ажыратылды." + "Желі тюнері ажыратылды." diff --git a/usbtuner-res/values-km-rKH/strings.xml b/usbtuner-res/values-km-rKH/strings.xml index 904f3e5d..e6a359ef 100644 --- a/usbtuner-res/values-km-rKH/strings.xml +++ b/usbtuner-res/values-km-rKH/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "អង្គរាវប៉ុស្តិ៍ទូរទស្សន៍" "ឧបករណ៍ USB រាវប៉ុស្តិ៍ទូរទស្សន៍" - "បើក" - "បិទ" + "កម្មវិធី​រាវ​បណ្តាញ​ទូរទស្សន៍ (បេតា)" "សូមរង់ចាំដើម្បីបញ្ចប់ដំណើរការ" - "ជ្រើសប្រភពប៉ុស្តិ៍របស់អ្នក" - "គ្មានរលកសញ្ញាទេ" - "បានបរាជ័យក្នុងការបើកប៉ុស្តិ៍ %s" - "បរាជ័យក្នុងការរាវប៉ុស្តិ៍" "កម្មវិធីអង្គរាវប៉ុស្តិ៍បានអាប់ដេតថ្មីៗនេះ។ សូមស្កេនរកប៉ុស្តិ៍ទាំងនេះម្តងទៀត។" "បើកដំណើរការសំឡេងជុំវិញនៅក្នុងការកំណត់សំឡេងប្រព័ន្ធដើម្បីបើកដំណើរការអូឌីយ៉ូ" + "មិនអាចចាក់អូឌីយ៉ូបានទេ។ សូមសាកល្បងប្រើទូរទស្សន៍ផ្សេង" "ការដំឡើងអង្គរាវប៉ុស្តិ៍" "ការដំឡើងអង្គរាវប៉ុស្តិ៍ទូរទស្សន៍" "ការដំឡើងឧបករណ៍ USB រាវប៉ុស្តិ៍" + "រៀបចំឧបករណ៍រាវបណ្តាញ" "សូមផ្ទៀងផ្ទាត់ថាប៉ុស្តិ៍ទូរទស្សន៍របស់អ្នកត្រូវបានភ្ជាប់ទៅប្រភពរលកសញ្ញាទូរទស្សន៍។\n\nប្រសិនបើអ្នកប្រើអង់តែនឥតខ្សែ សូមសម្រួលទីតាំង ឬទិសដៅរបស់វាដើម្បីទទួលបានប៉ុស្តិ៍ច្រើនបំផុត។ ដើម្បីទទួលបានលទ្ធផលល្អបំផុត សូមដាក់វានៅកន្លែងខ្ពស់ និងនៅជិតបង្អួច។" "ផ្ទៀងផ្ទាត់ថាអ្នកបានដោតឧបករណ៍ USB រាវប៉ុស្តិ៍ និងបានភ្ជាប់ទៅប្រភពសញ្ញាទូរទស្សន៍\n\nប្រសិនបើអ្នកប្រើអង់តែនឥតខ្សែ អ្នកប្រហែលជាត្រូវកែសម្រួលទីតាំង និងទិសដៅរបស់វាដើម្បីទទួលបានប៉ុស្តិ៍ច្រើនបំផុត។ ដើម្បីទទួលបានលទ្ធផលល្អបំផុត សូមដាក់វានៅកន្លែងដែលខ្ពស់ក្បែរបង្អួច។" + "ផ្ទៀងផ្ទាត់ថាអ្នកបានបើកឧបករណ៍រាវបណ្តាញនេះ និងបានភ្ជាប់ទៅប្រភពរលកសញ្ញាទូរទស្សន៍។\n\nប្រសិនបើអ្នកប្រើអង់តែនឥតខ្សែ អ្នកត្រូវកែសម្រួលទីតាំង និងទិសដៅរបស់វា ដើម្បីទទួលបានប៉ុស្តិ៍ច្រើនបំផុត។ ដើម្បីទទួលបានលទ្ធផលល្អបំផុត សូមដាក់វានៅកន្លែងដែលខ្ពស់ក្បែរបង្អួច។" "បន្ត" "មិនមែនឥឡូវនេះទេ" @@ -40,6 +38,7 @@ "ដំណើរការដំឡើងប៉ុស្តិ៍ឡើងវិញឬទេ?" "វានឹងលុបប៉ុស្តិ៍ដែលបានរកឃើញដោយអង្គរាវប៉ុស្តិ៍ទូរទស្សន៍ចេញ ហើយស្កេនរកប៉ុស្តិ៍ថ្មីម្តងទៀត។\n\nផ្ទៀងផ្ទាត់ថាអ្នកបានដោតទូរទស្សន៍ទៅប្រភពរលកសញ្ញាទូរទស្សន៍\n\nប្រសិនបើអ្នកប្រើអង់តែនឥតខ្សែ អ្នកប្រហែលជាត្រូវសម្រួលទីតាំង និងទិសដៅរបស់វាដើម្បីទទួលបានប៉ុស្តិ៍ច្រើនបំផុត។ ដើម្បីទទួលបានលទ្ធផលល្អបំផុត សូមដាក់វានៅកន្លែងដែលខ្ពស់ក្បែរបង្អួច។" "វានឹងលុបប៉ុស្តិ៍ដែលបានរកឃើញដោយឧបករណ៍ USB រាវប៉ុស្តិ៍ចេញ ហើយស្កេនរកប៉ុស្តិ៍ថ្មីម្តងទៀត។\n\nផ្ទៀងផ្ទាត់ថាអ្នកបានដោតឧបករណ៍ USB រាវប៉ុស្តិ៍ និងបានភ្ជាប់ទៅប្រភពសញ្ញាទូរទស្សន៍\n\nប្រសិនបើអ្នកប្រើអង់តែនឥតខ្សែ អ្នកប្រហែលជាត្រូវសម្រួលទីតាំង និងទិសដៅរបស់វាដើម្បីទទួលបានប៉ុស្តិ៍ច្រើនបំផុត។ ដើម្បីទទួលបានលទ្ធផលល្អបំផុត សូមដាក់វានៅកន្លែងដែលខ្ពស់ក្បែរបង្អួច។" + "វានឹងលុបប៉ុស្តិ៍ដែលបានរកឃើញដោយឧបករណ៍រាវបណ្តាញ ហើយបន្ទាប់មកស្កេនរកបណ្តាញថ្មី។\n\nផ្ទៀងផ្ទាត់ថាអ្នកបានបើកអង្គរាវបណ្តាញនេះ និងបានភ្ជាប់ទៅប្រភពរលកសញ្ញាទូរទស្សន៍។\n\nប្រសិនបើអ្នកប្រើអង់តែនឥតខ្សែ អ្នកត្រូវសម្រួលទីតាំង និងទិសដៅរបស់វា ដើម្បីទទួលបានប៉ុស្តិ៍ច្រើនបំផុត។ ដើម្បីទទួលបានលទ្ធផលល្អបំផុត សូមដាក់វានៅកន្លែងដែលខ្ពស់ក្បែរបង្អួច។" "បន្ត" "បោះបង់" @@ -54,6 +53,7 @@ "ការដំឡើងអង្គរាវប៉ុស្តិ៍ទូរទស្សន៍" "ការដំឡើងឧបករណ៍ USB រាវប៉ុស្តិ៍ទូរទស្សន៍" + "ការរៀបចំឧបករណ៍រាវបណ្តាញប៉ុស្តិ៍" "វាអាចចំណាយពេលច្រើននាទី" "អង្គរាវប៉ុស្តិ៍មិនអាចប្រើបានជាបណ្តោះអាសន្ន ឬបានប្រើសម្រាប់ការថតរួចទៅហើយ។" @@ -76,6 +76,7 @@ "រកមិនឃើញប៉ុស្តិ៍ទេ" "ការស្កេននេះរកមិនឃើញប៉ុស្តិ៍ណាមួយឡើយ។ សូមផ្ទៀងផ្ទាត់ថាប៉ុស្តិ៍ទូរទស្សន៍របស់អ្នកត្រូវបានភ្ជាប់ទៅប្រភពរលកសញ្ញាទូរទស្សន៍។\n\nប្រសិនបើអ្នកប្រើអង់តែនឥតខ្សែ សូមសម្រួលទីតាំង ឬទិសដៅរបស់វា។ ដើម្បីទទួលបានលទ្ធផលល្អបំផុត សូមដាក់វានៅកន្លែងខ្ពស់ និងនៅជិតបង្អួច បន្ទាប់មកធ្វើការស្កេនម្តងទៀត។" "ការស្កេនរកមិនឃើញប៉ុស្តិ៍ទេ។ ផ្ទៀងផ្ទាត់ថាអ្នកបានដោតឧបករណ៍ USB រាវប៉ុស្តិ៍ និងបានភ្ជាប់ទៅប្រភពសញ្ញាទូរទស្សន៍។\n\nប្រសិនបើអ្នកប្រើអង់តែនឥតខ្សែ សូមសម្រួលទីតាំង និងទិសដៅរបស់វា។ ដើម្បីទទួលបានលទ្ធផលល្អបំផុត សូមដាក់វានៅកន្លែងដែលខ្ពស់ក្បែរបង្អួច ហើយបន្ទាប់មកស្កេនម្តងទៀត។" + "ការស្កេនរកមិនឃើញប៉ុស្តិ៍ណាមួយទេ។ ផ្ទៀងផ្ទាត់ថាអ្នកបានបើកឧបករណ៍រាវបណ្តាញនេះ និងបានភ្ជាប់ទៅប្រភពរលកសញ្ញាទូរទស្សន៍។\n\nប្រសិនបើអ្នកប្រើអង់តែនឥតខ្សែ សូមសម្រួលទីតាំង និងទិសដៅរបស់វា។ ដើម្បីទទួលបានលទ្ធផលល្អបំផុត សូមដាក់វានៅកន្លែងដែលខ្ពស់ក្បែរបង្អួច បន្ទាប់មកស្កេនម្តងទៀត។" "ស្កេនម្តងទៀត" "រួចរាល់" @@ -83,5 +84,7 @@ "ស្កេនរកប៉ុស្តិ៍ទូរទស្សន៍" "ការដំឡើងអង្គរាវប៉ុស្តិ៍ទូរទស្សន៍" "ការដំឡើងឧបករណ៍ USB រាវប៉ុស្តិ៍ទូរទស្សន៍" - "អង្គរាវប៉ុស្តិ៍ USB សម្រាប់ទូរទស្សន៍ត្រូវបានផ្តាច់" + "រៀបចំឧបករណ៍រាវបណ្តាញទូរទស្សន៍" + "អង្គរាវរកប៉ុស្តិ៍ USB សម្រាប់ទូរទស្សន៍ត្រូវបានផ្តាច់។" + "អង្គរាវរក​បណ្តាញ​ត្រូវ​បាន​ផ្ដាច់។" diff --git a/usbtuner-res/values-kn-rIN/strings.xml b/usbtuner-res/values-kn-rIN/strings.xml index fb97ae2b..47cfc134 100644 --- a/usbtuner-res/values-kn-rIN/strings.xml +++ b/usbtuner-res/values-kn-rIN/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "ಟಿವಿ ಟ್ಯೂನರ್" "USB ಟಿವಿ ಟ್ಯೂನರ್" - "ಆನ್" - "ಆಫ್" + "ನೆಟ್‌ವರ್ಕ್ ಟಿವಿ ಟ್ಯೂನರ್ (ಬೀಟಾ)‌‌" "ಪ್ರಕ್ರಿಯೆಗೊಳಿಸುವುದನ್ನು ಪೂರೈಸಲು ದಯವಿಟ್ಟು ಕಾಯಿರಿ" - "ನಿಮ್ಮ ಚಾನಲ್ ಮೂಲವನ್ನು ಆಯ್ಕೆಮಾಡಿ" - "ಯಾವುದೇ ಸಂಕೇತವಿಲ್ಲ" - "%s ಗೆ ಟ್ಯೂನ್ ಮಾಡುವಲ್ಲಿ ವಿಫಲವಾಗಿದೆ" - "ಟ್ಯೂನ್ ಮಾಡಲು ವಿಫಲವಾಗಿದೆ" "ಟ್ಯೂನರ್ ಸಾಫ್ಟ್‌ವೇರ್‍ ಅನ್ನು ಇತ್ತೀಚಿಗೆ ಅಪ್‌ಡೇಟ್ ಮಾಡಲಾಗಿದೆ. ದಯವಿಟ್ಟು ಚಾನಲ್‌ಗಳನ್ನು ಮರು-ಸ್ಕ್ಯಾನ್‌ ಮಾಡಿ." "ಆಡಿಯೊ ಸಕ್ರಿಯಗೊಳಿಸಲು ಸಿಸ್ಟಂ ಧ್ವನಿ ಸೆಟ್ಟಿಂಗ್‌ಗಳಲ್ಲಿ ಸರೌಂಡ್ ಧ್ವನಿಯನ್ನು ಸಕ್ರಿಯಗೊಳಿಸಿ" + "ಆಡಿಯೊವನ್ನು ಪ್ಲೇ ಮಾಡಲು ಸಾಧ್ಯವಿಲ್ಲ. ದಯವಿಟ್ಟು ಬೇರೊಂದು ಟಿವಿಯಲ್ಲಿ ಪ್ರಯತ್ನಿಸಿ" "ಚಾನಲ್ ಟ್ಯೂನರ್ ಸೆಟಪ್" "ಟಿವಿ ಟ್ಯೂನರ್ ಸೆಟಪ್" "USB ಚಾನಲ್ ಟ್ಯೂನರ್ ಸೆಟಪ್" + "ನೆಟ್‌ವರ್ಕ್ ಟ್ಯೂನರ್ ಸೆಟಪ್" "ನಿಮ್ಮ ಟಿವಿಯನ್ನು ಟಿವಿ ಸಿಗ್ನಲ್‌ ಮೂಲಕ್ಕೆ ಸಂಪರ್ಕಪಡಿಸಲಾಗಿದೆಯೆ ಎಂದು ಪರಿಶೀಲಿಸಿ.\n\nನೀವು ಪ್ರಸಾರದ ಮೂಲಕ ಆಂಟೆನಾ ಬಳಸುತ್ತಿದ್ದರೆ, ಹೆಚ್ಚಿನ ಚಾನಲ್‌ಗಳನ್ನು ಸ್ವೀಕರಿಸಲು ನೀವು ಅದರ ಸ್ಥಾನ ಅಥವಾ ದಿಕ್ಕನ್ನು ಹೊಂದಿಸಬೇಕಾಗಬಹುದು. ಉತ್ತಮ ಫಲಿತಾಂಶಗಳಿಗೆ, ಇದನ್ನು ಎತ್ತರದಲ್ಲಿ ಮತ್ತು ಕಿಟಕಿಯ ಬಳಿ ಇರಿಸಿ." "USB ಟ್ಯೂನಲ್ ಅನ್ನು ಪ್ಲಗ್ ಇನ್ ಮಾಡಲಾಗಿದೆಯೇ ಮತ್ತು ಟಿವಿ ಸಿಗ್ನಲ್ ಮೂಲಕ್ಕೆ ಸಂಪರ್ಕಪಡಿಸಲಾಗಿದೆಯೆ ಎಂದು ಪರಿಶೀಲಿಸಿ.\n\nಪ್ರಸಾರದ ಮೂಲಕ ಆಂಟೆನಾ ಬಳಸುತ್ತಿದ್ದರೆ, ಹೆಚ್ಚಿನ ಚಾನಲ್‌ಗಳನ್ನು ಸ್ವೀಕರಿಸಲು ನೀವು ಅದರ ಸ್ಥಾನ ಅಥವಾ ದಿಕ್ಕನ್ನು ಹೊಂದಿಸಬೇಕಾಗಬಹುದು. ಉತ್ತಮ ಫಲಿತಾಂಶಗಳಿಗೆ, ಇದನ್ನು ಎತ್ತರದಲ್ಲಿ ಮತ್ತು ಕಿಟಕಿಯ ಬಳಿ ಇರಿಸಿ ಹಾಗೂ ಮತ್ತೆ ಸ್ಕ್ಯಾನ್ ಮಾಡಿ." + "ನೆಟ್‌ವರ್ಕ್ ಟ್ಯೂನರ್ ಆನ್ ಮಾಡಲಾಗಿದೆಯೇ ಮತ್ತು ಟಿವಿ ಸಿಗ್ನಲ್ ಮೂಲಕ್ಕೆ ಸಂಪರ್ಕಪಡಿಸಲಾಗಿದೆಯೇ ಎಂದು ಪರಿಶೀಲಿಸಿ.\n\nನೀವು ನೆಟ್‌ವರ್ಕ್ ಸಂಪರ್ಕದ ಮೂಲಕ ಆಂಟೆನಾ ಬಳಸುತ್ತಿದ್ದರೆ, ಹೆಚ್ಚಿನ ಚಾನಲ್‌ಗಳನ್ನು ಸ್ವೀಕರಿಸಲು ನೀವು ಅದರ ಸ್ಥಾನ ಅಥವಾ ದಿಕ್ಕನ್ನು ಹೊಂದಿಸಬೇಕಾಗಬಹುದು. ಉತ್ತಮ ಫಲಿತಾಂಶಗಳಿಗೆ, ಅದನ್ನು ಎತ್ತರದಲ್ಲಿ ಮತ್ತು ಕಿಟಕಿಯ ಬಳಿ ಇರಿಸಿ." "ಮುಂದುವರಿಸು" "ಸದ್ಯಕ್ಕೆ ಬೇಡ" @@ -40,6 +38,7 @@ "ಚಾನಲ್ ಸೆಟಪ್ ಅನ್ನು ಮರುರನ್ ಮಾಡುವುದೇ?" "ಟಿವಿ ಟ್ಯೂನರ್‌ನಿಂದ ಪತ್ತೆ ಮಾಡಲಾದ ಚಾನಲ್‌ಗಳನ್ನು ಇದು ತೆಗೆದುಹಾಕುತ್ತದೆ ಹಾಗೂ ಮತ್ತೆ ಹೊಸ ಚಾನಲ್‌ಗಳಿಗೆ ಸ್ಕ್ಯಾನ್ ಮಾಡಲಾಗುತ್ತದೆ.\n\nನಿಮ್ಮ ಟಿವಿಯನ್ನು ಟಿವಿ ಸಿಗ್ನಲ್‌ ಮೂಲಕ್ಕೆ ಸಂಪರ್ಕಪಡಿಸಲಾಗಿದೆಯೆ ಎಂದು ಪರಿಶೀಲಿಸಿ.\n\nನೀವು ಪ್ರಸಾರದ ಮೂಲಕ ಆಂಟೆನಾ ಬಳಸುತ್ತಿದ್ದರೆ, ಹೆಚ್ಚಿನ ಚಾನಲ್‌ಗಳನ್ನು ಸ್ವೀಕರಿಸಲು ನೀವು ಅದರ ಸ್ಥಾನ ಅಥವಾ ದಿಕ್ಕನ್ನು ಹೊಂದಿಸಬೇಕಾಗಬಹುದು. ಉತ್ತಮ ಫಲಿತಾಂಶಗಳಿಗೆ, ಇದನ್ನು ಎತ್ತರದಲ್ಲಿ ಮತ್ತು ಕಿಟಕಿಯ ಬಳಿ ಇರಿಸಿ." "USB ಟ್ಯೂನರ್‌ನಿಂದ ಪತ್ತೆ ಮಾಡಲಾದ ಚಾನಲ್‌ಗಳನ್ನು ಇದು ತೆಗೆದುಹಾಕುತ್ತದೆ ಹಾಗೂ ಮತ್ತೆ ಹೊಸ ಚಾನಲ್‌ಗಳಿಗೆ ಸ್ಕ್ಯಾನ್ ಮಾಡಲಾಗುತ್ತದೆ.\n\nUSB ಟ್ಯೂನಲ್ ಅನ್ನು ಪ್ಲಗ್ ಇನ್ ಮಾಡಲಾಗಿದೆಯೇ ಮತ್ತು ಟಿವಿ ಸಿಗ್ನಲ್ ಮೂಲಕ್ಕೆ ಸಂಪರ್ಕಪಡಿಸಲಾಗಿದೆಯೆ ಎಂದು ಪರಿಶೀಲಿಸಿ.\n\nನೀವು ಪ್ರಸಾರದ ಮೂಲಕ ಆಂಟೆನಾ ಬಳಸುತ್ತಿದ್ದರೆ, ಹೆಚ್ಚಿನ ಚಾನಲ್‌ಗಳನ್ನು ಸ್ವೀಕರಿಸಲು ನೀವು ಅದರ ಸ್ಥಾನ ಅಥವಾ ದಿಕ್ಕನ್ನು ಹೊಂದಿಸಬೇಕಾಗಬಹುದು. ಉತ್ತಮ ಫಲಿತಾಂಶಗಳಿಗೆ, ಇದನ್ನು ಎತ್ತರದಲ್ಲಿ ಮತ್ತು ಕಿಟಕಿಯ ಬಳಿ ಇರಿಸಿ ಹಾಗೂ ಮತ್ತೆ ಸ್ಕ್ಯಾನ್ ಮಾಡಿ." + "ನೆಟ್‌ವರ್ಕ್ ಟ್ಯೂನರ್‌ನಿಂದ ಪತ್ತೆ ಮಾಡಲಾದ ಚಾನಲ್‌ಗಳನ್ನು ಇದು ತೆಗೆದುಹಾಕುತ್ತದೆ ಹಾಗೂ ಮತ್ತೆ ಹೊಸ ಚಾನಲ್‌ಗಳಿಗೆ ಸ್ಕ್ಯಾನ್ ಮಾಡುತ್ತದೆ.\n\nನೆಟ್‌ವರ್ಕ್ ಟ್ಯೂನಲ್ ಅನ್ನು ಆನ್ ಮಾಡಲಾಗಿದೆಯೇ ಮತ್ತು ಟಿವಿ ಸಿಗ್ನಲ್ ಮೂಲಕ್ಕೆ ಸಂಪರ್ಕಪಡಿಸಲಾಗಿದೆಯೇ ಎಂದು ಪರಿಶೀಲಿಸಿ.\n\nನೀವು ನೆಟ್‌ವರ್ಕ್ ಸಂಪರ್ಕದ ಮೂಲಕ ಆಂಟೆನಾ ಬಳಸುತ್ತಿದ್ದರೆ, ಹೆಚ್ಚಿನ ಚಾನಲ್‌ಗಳನ್ನು ಸ್ವೀಕರಿಸಲು ನೀವು ಅದರ ಸ್ಥಾನ ಅಥವಾ ದಿಕ್ಕನ್ನು ಹೊಂದಿಸಬೇಕಾಗಬಹುದು. ಉತ್ತಮ ಫಲಿತಾಂಶಗಳಿಗೆ, ಅದನ್ನು ಎತ್ತರದಲ್ಲಿ ಮತ್ತು ಕಿಟಕಿಯ ಬಳಿ ಇರಿಸಿ." "ಮುಂದುವರಿಸು" "ರದ್ದುಮಾಡಿ" @@ -54,6 +53,7 @@ "ಟಿವಿ ಟ್ಯೂನರ್ ಸೆಟಪ್" "USB ಚಾನಲ್ ಟ್ಯೂನರ್ ಸೆಟಪ್" + "ನೆಟ್‌ವರ್ಕ್ ಚಾನಲ್ ಟ್ಯೂನರ್ ಸೆಟಪ್" "ಇದಕ್ಕೆ ಹಲವಾರು ನಿಮಿಷಗಳು ತೆಗೆದುಕೊಳ್ಳಬಹುದು" "ಟ್ಯೂನರ್ ತಾತ್ಕಾಲಿಕವಾಗಿ ಲಭ್ಯವಿಲ್ಲ ಅಥವಾ ಈಗಾಗಲೇ ರೆಕಾರ್ಡಿಂಗ್‌ಗೆ ಬಳಸಲಾಗಿದೆ." @@ -76,6 +76,7 @@ "ಯಾವುದೇ ಚಾನಲ್‌ಗಳು ಕಂಡುಬಂದಿಲ್ಲ" "ಸ್ಕ್ಯಾನ್ ಯಾವುದೇ ಚಾನಲ್‌ಗಳನ್ನು ಪತ್ತೆ ಮಾಡಿಲ್ಲ. ನಿಮ್ಮ ಟಿವಿಯನ್ನು ಟಿವಿ ಸಿಗ್ನಲ್‌ ಮೂಲಕ್ಕೆ ಸಂಪರ್ಕಪಡಿಸಲಾಗಿದೆಯೆ ಎಂದು ಪರಿಶೀಲಿಸಿ.\n\nನೀವು ಪ್ರಸಾರದ ಮೂಲಕ ಆಂಟೆನಾ ಬಳಸುತ್ತಿದ್ದರೆ, ಉತ್ತಮ ಫಲಿತಾಂಶಗಳಿಗೆ, ಇದನ್ನು ಎತ್ತರದಲ್ಲಿ ಮತ್ತು ಕಿಟಕಿಯ ಬಳಿ ಇರಿಸಿ ಹಾಗೂ ಮತ್ತೆ ಸ್ಕ್ಯಾನ್ ಮಾಡಿ." "ಸ್ಕ್ಯಾನ್ ಯಾವುದೇ ಚಾನಲ್‌ಗಳನ್ನು ಪತ್ತೆ ಮಾಡಿಲ್ಲ. USB ಟ್ಯೂನಲ್ ಅನ್ನು ಪ್ಲಗ್ ಇನ್ ಮಾಡಲಾಗಿದೆಯೇ ಮತ್ತು ಟಿವಿ ಸಿಗ್ನಲ್ ಮೂಲಕ್ಕೆ ಸಂಪರ್ಕಪಡಿಸಲಾಗಿದೆಯೆ ಎಂದು ಪರಿಶೀಲಿಸಿ.\n\nಪ್ರಸಾರದ ಮೂಲಕ ಆಂಟೆನಾ ಬಳಸುತ್ತಿದ್ದರೆ, ಅದರ ಸ್ಥಾನ ಅಥವಾ ದಿಕ್ಕನ್ನು ಸರಿಹೊಂದಿಸಿ. ಉತ್ತಮ ಫಲಿತಾಂಶಗಳಿಗೆ, ಎತ್ತರದಲ್ಲಿ ಮತ್ತು ಕಿಟಕಿಯ ಬಳಿ ಇರಿಸಿ ಹಾಗೂ ಮತ್ತೆ ಸ್ಕ್ಯಾನ್ ಮಾಡಿ." + "ಸ್ಕ್ಯಾನ್ ನಂತರ ಯಾವುದೇ ಚಾನಲ್ ಕಂಡುಬಂದಿಲ್ಲ. ನೆಟ್‌ವರ್ಕ್ ಟ್ಯೂನರ್ ಆನ್ ಮಾಡಲಾಗಿದೆಯೇ ಮತ್ತು ಟಿವಿ ಸಿಗ್ನಲ್ ಮೂಲಕ್ಕೆ ಸಂಪರ್ಕಪಡಿಸಲಾಗಿದೆಯೇ ಎಂದು ಪರಿಶೀಲಿಸಿ.\n\nನೀವು ನೆಟ್‌ವರ್ಕ್ ಸಂಪರ್ಕದ ಮೂಲಕ ಆಂಟೆನಾ ಬಳಸುತ್ತಿದ್ದರೆ, ಅದರ ಸ್ಥಾನ ಅಥವಾ ದಿಕ್ಕನ್ನು ಹೊಂದಿಸಿ. ಉತ್ತಮ ಫಲಿತಾಂಶಗಳಿಗೆ, ಅದನ್ನು ಎತ್ತರದಲ್ಲಿ ಮತ್ತು ಕಿಟಕಿಯ ಬಳಿ ಇರಿಸಿ ಹಾಗೂ ಮತ್ತೊಮ್ಮೆ ಸ್ಕ್ಯಾನ್ ಮಾಡಿ." "ಮತ್ತೆ ಸ್ಕ್ಯಾನ್ ಮಾಡು" "ಮುಗಿದಿದೆ" @@ -83,5 +84,7 @@ "ಟಿವಿ ಚಾನಲ್‌ಗಳನ್ನು ಸ್ಕ್ಯಾನ್ ಮಾಡಿ" "ಟಿವಿ ಟ್ಯೂನರ್ ಸೆಟಪ್" "USB ಟಿವಿ ಟ್ಯೂನರ್ ಸೆಟಪ್" - "USB ಟಿವಿ ಟ್ಯೂನರ್ ಸಂಪರ್ಕ ಕಡಿತಗೊಂಡಿದೆ." + "ನೆಟ್‌ವರ್ಕ್ ಟಿವಿ ಟ್ಯೂನರ್ ಸೆಟಪ್" + "USB ಟಿವಿ ಟ್ಯೂನರ್ ಸಂಪರ್ಕ ಕಡಿತಗೊಂಡಿದೆ." + "ನೆಟ್‌ವರ್ಕ್ ಟ್ಯೂನರ್ ಸಂಪರ್ಕ ಕಡಿತಗೊಂಡಿದೆ." diff --git a/usbtuner-res/values-ko/strings.xml b/usbtuner-res/values-ko/strings.xml index 2c67ab6f..0b354739 100644 --- a/usbtuner-res/values-ko/strings.xml +++ b/usbtuner-res/values-ko/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "TV 튜너" "USB TV 튜너" - "사용" - "사용 안함" + "네트워크 TV 튜너(BETA)" "처리가 완료될 때까지 기다려 주세요." - "채널 소스 선택" - "신호 없음" - "%s에 맞추지 못함" - "조정 실패" "튜너 소프트웨어가 최근 업데이트되었습니다. 채널을 다시 스캔하세요." "오디오를 사용하려면 시스템 사운드 설정에서 서라운드 사운드를 사용 설정하세요." + "오디오를 재생할 수 없습니다. 다른 TV를 사용해 보세요." "채널 튜너 설정" "TV 튜너 설정" "USB 채널 튜너 설정" + "네트워크 튜너 설정" "TV가 TV 신호 소스에 연결되어 있는지 확인하세요.\n\n무선 안테나를 사용하는 경우 대부분의 채널을 수신하려면 위치나 방향을 조정해야 할 수도 있습니다. 안테나를 창가 가까이에 높게 설치하면 가장 좋습니다." "USB 튜너가 전원 및 TV 신호 소스에 연결되어 있는지 확인하세요.\n\n무선 안테나를 사용 중인 경우 안테나 위치나 방향을 조정하세요. 창가 가까이에 높게 설치하면 가장 좋습니다." + "네트워크 튜너의 전원이 켜져 있고 TV 신호 소스에 연결되어 있는지 확인하세요.\n\n무선 안테나를 사용하는 경우 안테나 위치나 방향을 조정하세요. 최상의 결과를 얻으려면 안테나를 창가 가까이에 높게 설치하세요." "계속" "나중에" @@ -40,6 +38,7 @@ "채널 설정을 다시 실행하시겠습니까?" "이렇게 하면 TV 튜너에서 찾은 채널을 삭제하고 새로운 채널을 다시 스캔하게 됩니다.\n\nTV가 TV 신호 소스에 연결되어 있는지 확인하세요.\n\n무선 안테나를 사용하는 경우 대부분의 채널을 수신하려면 위치나 방향을 조정해야 할 수 있습니다. 안테나를 창가 가까이에 높게 설치하면 가장 좋습니다." "이 작업을 수행하면 USB 튜너에서 찾은 채널이 삭제되며 새로운 채널을 다시 스캔합니다.\n\nUSB 튜너가 전원 및 TV 신호 소스에 연결되어 있는지 확인하세요.\n\n무선 안테나를 사용 중인 경우 안테나 위치나 방향을 조정하세요. 창가 가까이에 높게 설치하면 가장 좋습니다." + "네트워크 튜너에서 찾은 채널이 삭제되며 새로운 채널을 다시 스캔합니다.\n\n네트워크 튜너의 전원이 켜져 있고 TV 신호 소스에 연결되어 있는지 확인하세요.\n\n무선 안테나를 사용하는 경우 안테나 위치나 방향을 조정하세요. 최상의 결과를 얻으려면 안테나를 창가 가까이에 높게 설치하고 다시 스캔해보세요." "계속" "취소" @@ -54,8 +53,9 @@ "TV 튜너 설정" "USB 채널 튜너 설정" + "네트워크 채널 튜너 설정" "이 작업은 몇 분 정도 걸릴 수 있습니다." - "일시적으로 튜너를 사용할 수 없거나 녹화에서 이미 사용하고 있습니다." + "일시적으로 튜너를 사용할 수 없거나 이미 녹화에 사용하고 있습니다." 채널 %1$d개 발견 채널 %1$d개 발견 @@ -76,6 +76,7 @@ "채널 없음" "스캔 결과 채널을 찾지 못했습니다. TV가 TV 신호 소스에 연결되어 있는지 확인하세요.\n\n무선 안테나를 사용하는 경우 위치나 방향을 조정하세요. 안테나를 창가 가까이에 높게 설치하면 가장 좋습니다." "스캔하여 채널을 찾을 수 없습니다. USB 튜너가 전원 및 TV 신호 소스에 연결되어 있는지 확인하세요.\n\n무선 안테나를 사용 중인 경우 안테나 위치나 방향을 조정하세요. 창가 가까이에 높게 설치하면 가장 좋습니다. 그런 다음 다시 스캔해 보세요." + "스캔 결과 채널을 찾지 못했습니다. 네트워크 튜너의 전원이 켜져 있고 TV 신호 소스에 연결되어 있는지 확인하세요.\n\n무선 안테나를 사용하는 경우 안테나 위치나 방향을 조정하세요. 최상의 결과를 얻으려면 안테나를 창가 가까이에 높게 설치하고 다시 스캔해보세요." "다시 스캔" "완료" @@ -83,5 +84,7 @@ "TV 채널 스캔" "TV 튜너 설정" "USB TV 튜너 설정" - "USB TV 튜너 연결 끊어짐" + "네트워크 TV 튜너 설정" + "USB TV 튜너의 연결이 끊겼습니다." + "네트워크 튜너의 연결이 끊겼습니다." diff --git a/usbtuner-res/values-ky-rKG/strings.xml b/usbtuner-res/values-ky-rKG/strings.xml index 39e9d1df..3b33821d 100644 --- a/usbtuner-res/values-ky-rKG/strings.xml +++ b/usbtuner-res/values-ky-rKG/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "Сыналгы күүлөгүчү" "USB сыналгы күүлөгүчү" - "Күйүк" - "Өчүк" + "Тармактык ТВ-тюнер (БЕТА)" "Иштетүүнү бүтүрүү үчүн күтө туруңуз" - "Каналыңыздын булагын тандаңыз" - "Сигнал жок" - "%s каналы кармалган жок" - "Канал кармалбай койду" "Күүлөгүчтүн программасы жакында жаңыртылды. Каналдарды кайрадан издеңиз." "Каналдын үнүн чыгаруу үчүн тутумдун үн жөндөөлөрүнө өтүп, көлөмдүү добушту иштетүү керек" + "Аудио ойнотулбай жатат. Башка каналды байкап көрүңүз" "Канал күүлөгүчтү жөндөө" "Сыналгынын күүлөгүчүн жөндөө" "USB канал күүлөгүчүнүн жөндөөсү" + "Тармактык тюнерди жөндөө" "Сыналдыңыз сыналгынын сигнал булагына туташтырылганын текшериңиз. \n\nЭгер телеантенна колдонулуп жатса, анын ордун же багытын тууралаңыз. Мыкты кармашы үчүн аны бийик жана терезеге жакын жерге коюп, кайра издеңиз." "USB күүлөгүч сайылып турганын жана сыналгынын сигнал булагына туташтырылганын текшериңиз.\n\nЭгер телеантенна колдонулуп жатса, көпчүлүк каналдарды табуу үчүн анын ордун же багытын тууралашыңыз керек болот. Мыкты кармашы үчүн аны бийик жана терезеге жакын жерге коюп, кайра издеңиз." + "Тармактык тюнер сайылып турганын жана сыналгынын сигнал булагына туташтырылганын текшериңиз.\n\nЭгер телеантенна колдонулуп жатса, каналдарды табуу үчүн анын ордун же багытын тууралашыңыз керек болот. Антенна жакшы кармашы үчүн аны бийик жана терезеге жакын жерге коюп, кайра издеп көрүңүз." "Улантуу" "Азыр эмес" @@ -40,6 +38,7 @@ "Канал кайра жөндөлсүнбү?" "Ушуну менен сыналгынын күүлөгүчүнөн табылган каналдар алынып салынып, жаңы каналдар кайра изделет.\n\nСыналдыңыз сыналгынын сигнал булагына туташтырылганын текшериңиз.\n\nIЭгер телеантенна колдонулуп жатса, көпчүлүк каналдарды табуу үчүн анын ордун же багытын тууралашыңыз керек болот. Мыкты кармашы үчүн аны бийик жана терезеге жакын жерге коюп, кайра издеңиз." "Ушуну менен USB күүлөгүчтөн табылган каналдар алынып салынып, жаңы каналдар кайра изделет.\n\nUSB күүлөгүч сайылып турганын жана сыналгынын сигнал булагына туташтырылганын текшериңиз.\n\nЭгер телеантенна колдонулуп жатса, көпчүлүк каналдарды табуу үчүн анын ордун же багытын тууралашыңыз керек болот. Мыкты кармашы үчүн аны бийик жана терезеге жакын жерге коюп, кайра издеңиз." + "Ушуну менен тармактык тюнер аркылуу табылган каналдар алынып салынып, жаңы каналдар кайра изделет.\n\nТармактык тюнер сайылып турганын жана сыналгынын сигнал булагына туташтырылганын текшериңиз.\n\nЭгер телеантенна колдонулуп жатса, каналдарды табуу үчүн анын ордун же багытын тууралашыңыз керек болот. Антенна жакшы кармашы үчүн аны бийик жана терезеге жакын жерге коюп, кайра издеп көрүңүз." "Улантуу" "Токтотуу" @@ -54,6 +53,7 @@ "Сыналгы күүлөгүчүн жөндөө" "USB канал күүлөгүчүн жөндөө" + "Тармактык каналдын тюнерин жөндөө" "Бир нече мүнөт созулушу мүмкүн" "Тюнер убактылуу жеткиликсиз же жаздыруу үчүн колдонулууда." @@ -76,6 +76,7 @@ "Бир да канал табылган жок" "Издөөдөн эч бир канал табылган жок. Сыналдыңыз сыналгынын сигнал булагына туташтырылганын текшериңиз.\n\nЭгер телеантенна колдонулуп жатса, анын ордун же багытын тууралаңыз. Мыкты кармашы үчүн аны бийик жана терезеге жакын жерге коюп, кайра издеңиз." "Издөөдөн эч бир канал табылган жок. USB күүлөгүч сайылып турганын жана сыналгынын сигнал булагына туташтырылганын текшериңиз.\n\nЭгер телеантенна колдонулуп жатса, анын ордун же багытын тууралаңыз. Мыкты кармашы үчүн аны бийик жана терезеге жакын жерге коюп, кайра издеңиз." + "Каналдар табылган жок. Тармактык тюнер сайылып турганын жана сыналгынын сигнал булагына туташтырылганын текшериңиз.\n\nЭгер телеантенна колдонулуп жатса, анын ордун же багытын тууралаңыз. Антенна жакшы кармашы үчүн аны бийик жана терезеге жакын жерге коюп, кайра издеп көрүңүз." "Кайра издөө" "Бүттү" @@ -83,5 +84,7 @@ "Сыналгы каналдарын издөө" "Сыналгы күүлөгүчүн жөндөө" "USB күүлөгүчүнүн жөндөөсү" - "USB TV күүлөгүчү ажыратылды." + "Тармактык ТВ-тюнерди жөндөө" + "USB TV күүлөгүчү ажыратылды." + "Тармак күүлөгүчү ажыратылды." diff --git a/usbtuner-res/values-lo-rLA/strings.xml b/usbtuner-res/values-lo-rLA/strings.xml index 954baabe..a7f5ba61 100644 --- a/usbtuner-res/values-lo-rLA/strings.xml +++ b/usbtuner-res/values-lo-rLA/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "ຕົວຮັບສັນຍານໂທລະພາບ" "ຕົວຮັບສັນຍານໂທລະພາບ USB" - "ເປີດ" - "ປິດ" + "Network TV Tuner (ເບຕ້າ)" "ກະລຸນາລໍຖ້າເພື່ອປະມວນຜົນໃຫ້ສຳເລັດ" - "ເລືອກແຫຼ່ງສັນຍານຊ່ອງຂອງທ່ານ" - "ບໍ່ມີສັນຍານ" - "ປັບຫາ %s ບໍ່ສຳເລັດ" - "ປັບຊ່ອງບໍ່ສຳເລັດ" "ຊອບແວຕົວປັບສັນຍານໄດ້ຮັບການອັບເດດເມື່ອບໍ່ດົນມານີ້ແລ້ວ. ກະລຸນາສະແກນຫາຊ່ອງຄືນໃໝ່." "ເປີດໃຊ້ສຽງຮອບທິດທາງໃນການຕັ້ງຄ່າລະບົບສຽງເພື່ອເປີດໃຊ້ສຽງ." + "Cannot play audio. Please try another TV" "ຕັ້ງເຄື່ອງຮັບສັນຍານຊ່ອງ" "ຕັ້ງຄ່າຕົວຮັບສັນຍານໂທລະພາບ" "ຕັ້ງເຄື່ອງຮັບສັນຍານຊ່ອງ USB" + "Network tuner setup" "ໃຫ້ກວດສອບວ່າທ່ານເຊື່ອມຕໍ່ໂທລະພາບຂອງທ່ານຫາແຫລ່ງສັນຍານໂທລະພາບແລ້ວ.\n\nຫາກໃຊ້ເສົາອາກາດ, ໃຫ້ຍ້າຍບ້ອນວາງ ຫຼື ທິດທາງຂອງມັນ. ເພື່ອໃຫ້ໄດ້ຜົນທີ່ດີທີ່ສຸດ, ໃຫ້ວາງໄວ້ບ່ອນສູງໃກ້ໆປ່ອງຢ້ຽມ ແລ້ວລອງສະແກນໃໝ່." "ກວດສອບເບິ່ງເຄື່ອງຮັບສັນຍານ USB ວ່າ ໄດ້ສຽບສາຍ ແລະ ເຊື່ອມຕໍ່ກັບແຫຼ່ງສັນຍານໂທລະພາບແລ້ວຫຼືຍັງ.\n\nຫາກໃຊ້ເສົາອາກາດ, ໃຫ້ຍ້າຍບ້ອນວາງ ຫຼື ທິດທາງຂອງມັນ. ເພື່ອໃຫ້ໄດ້ຜົນທີ່ດີທີ່ສຸດ, ໃຫ້ວາງໄວ້ບ່ອນສູງໃກ້ໆປ່ອງຢ້ຽມ ແລ້ວລອງສະແກນໃໝ່." + "ກວດສອບເບິ່ງເຄື່ອງຮັບສັນຍານເຄືອຂ່າຍວ່າ ໄດ້ສຽບສາຍ ແລະ ເຊື່ອມຕໍ່ກັບແຫຼ່ງສັນຍານໂທລະພາບແລ້ວຫຼືຍັງ.\n\nຫຼຫາກໃຊ້ເສົາອາກາດ, ໃຫ້ຍ້າຍບ່ອນວາງ ຫຼື ທິດທາງຂອງມັນ. ເພື່ອໃຫ້ໄດ້ຜົນທີ່ດີທີ່ສຸດ, ໃຫ້ວາງໄວ້ບ່ອນສູງໃກ້ໆປ່ອງຢ້ຽມ ແລ້ວລອງສະແກນໃໝ່." "ສືບຕໍ່" "ບໍ່ແມ່ນຕອນນີ້" @@ -40,6 +38,7 @@ "ຕັ້ງຄ່າຊ່ອງຄືນໃໝ່ບໍ່?" "ນີ້ເປັນການລຶບຊ່ອງທີ່ພົບແລ້ວອອກໄປຈາກຕົວຮັບສັນຍານໂທລະພາບ ແລະ ສະແກນຫາຊ່ອງໃໝ່ອີກຄັ້ງ.\n\nກວດສອບເບິ່ງຕົວຮັບສັນຍານໂທລະພາບວ່າໄດ້ສຽບສາຍ ແລະ ເຊື່ອມຕໍ່ກັບແຫຼ່ງສັນຍານໂທລະພາບແລ້ວຫຼືຍັງ.\n\nຫາກໃຊ້ເສົາອາກາດ, ໃຫ້ຍ້າຍບ້ອນວາງ ຫຼື ທິດທາງຂອງມັນ. ເພື່ອໃຫ້ໄດ້ຜົນທີ່ດີທີ່ສຸດ, ໃຫ້ວາງໄວ້ບ່ອນສູງໃກ້ໆປ່ອງຢ້ຽມ ແລ້ວລອງສະແກນໃໝ່." "ນີ້ຈະເປັນການລຶບຊ່ອງທີ່ພົບແລ້ວອອກໄປຈາກເຄື່ອງຮັບສັນຍານ USB ແລະ ສະແກນຫາຊ່ອງໃໝ່ອີກ.\n\nໃຫ້ກວດສອບເບິ່ງເຄື່ອງຮັບສັນຍານ USB ວ່າ ໄດ້ສຽບສາຍ ແລະ ເຊື່ອມຕໍ່ກັບແຫຼ່ງສັນຍານໂທລະພາບແລ້ວຫຼືຍັງ.\n\nຫາກໃຊ້ເສົາອາກາດ, ໃຫ້ຍ້າຍບ້ອນວາງ ຫຼື ທິດທາງຂອງມັນ. ເພື່ອໃຫ້ໄດ້ຜົນທີ່ດີທີ່ສຸດ, ໃຫ້ວາງໄວ້ບ່ອນສູງໃກ້ໆປ່ອງຢ້ຽມ ແລ້ວລອງສະແກນໃໝ່." + "ນີ້ຈະເປັນການລຶບຊ່ອງທີ່ພົບແລ້ວອອກໄປຈາກເຄື່ອງຮັບສັນຍານເຄືອຂ່າຍ ແລະ ສະແກນຫາຊ່ອງໃໝ່ອີກຄັ້ງ.\n\nໃຫ້ກວດສອບເບິ່ງເຄື່ອງຮັບສັນຍານເຄືອຂ່າຍວ່າໄດ້ສຽບສາຍ ແລະ ເຊື່ອມຕໍ່ກັບແຫຼ່ງສັນຍານໂທລະພາບແລ້ວຫຼືຍັງ.\n\nຫາກໃຊ້ເສົາອາກາດ, ທ່ານອາດຈະຕ້ອງໄດ້ປັບການຕັ້ງ ຫຼື ທິດທາງຂອງມັນ. ເພື່ອໃຫ້ໄດ້ຜົນດີຂຶ້ນ, ວາງມັນໄວ້ສູງ ແລະ ໃກ້ກັບປ່ອງຢ້ຽມແລ້ວສະແກນໃໝ່ອີກຄັ້ງ." "ສືບຕໍ່" "ຍົກເລີກ" @@ -54,6 +53,7 @@ "ຕັ້ງຄ່າຕົວຮັບສັນຍານໂທລະພາບ" "ການຕັ້ງເຄື່ອງຮັບສັນຍານຊ່ອງ USB" + "ຕັ້ງຄ່າຕົວຈູນສັນຍານຊ່ອງເຄືອຂ່າຍ" "ຂັ້ນຕອນນີ້ອາດຈະໃຊ້ເວລາຫຼາຍນາທີ" "ຈູນເນີບໍ່ສາມາດໃຊ້ໄດ້ຊົ່ວຄາວ ຫຼື ຖືກໃຊ້ໂດຍການບັນທຶກໃດໜຶ່ງຢູ່ກ່ອນແລ້ວ." @@ -76,6 +76,7 @@ "ບໍ່ພົບຊ່ອງໃດ" "ການສະແກນບໍ່ພົບຊ່ອງໃດໆເລີຍ. ໃຫ້ຢັ້ງຢືນວ່າທ່ານເຊື່ອມຕໍ່ໂທລະພາບຂອງທ່ານຫາແຫລ່ງສັນຍານໂທລະພາບແລ້ວ.\n\nຫາກໃຊ້ເສົາອາກາດ, ໃຫ້ຍ້າຍບ້ອນວາງ ຫຼື ທິດທາງຂອງມັນ. ເພື່ອໃຫ້ໄດ້ຜົນທີ່ດີທີ່ສຸດ, ໃຫ້ວາງໄວ້ບ່ອນສູງໃກ້ໆປ່ອງຢ້ຽມ ແລ້ວລອງສະແກນໃໝ່." "ການສະແກນບໍ່ພົບຊ່ອງໃດ. ໃຫ້ກວດສອບເບິ່ງເຄື່ອງຮັບສັນຍານ USB ວ່າໄດ້ສຽບສາຍ ແລະ ເຊື່ອມຕໍ່ກັບແຫຼ່ງສັນຍານໂທລະພາບແລ້ວຫຼືຍັງ.\n\nຫາກໃຊ້ເສົາອາກາດ, ໃຫ້ຍ້າຍບ້ອນວາງ ຫຼື ທິດທາງຂອງມັນ. ເພື່ອໃຫ້ໄດ້ຜົນທີ່ດີທີ່ສຸດ, ໃຫ້ວາງໄວ້ບ່ອນສູງໃກ້ໆປ່ອງຢ້ຽມ ແລ້ວລອງສະແກນໃໝ່." + "ການສະແກນບໍ່ພົບຊ່ອງໃດ. ໃຫ້ກວດສອບເບິ່ງເຄື່ອງຮັບສັນຍານເຄືອຂ່າຍວ່າໄດ້ສຽບສາຍ ແລະ ເຊື່ອມຕໍ່ກັບແຫຼ່ງສັນຍານໂທລະພາບແລ້ວຫຼືຍັງ.\n\nຫາກໃຊ້ເສົາອາກາດ, ໃຫ້ຍ້າຍບ້ອນວາງ ຫຼື ທິດທາງຂອງມັນ. ເພື່ອໃຫ້ໄດ້ຜົນທີ່ດີທີ່ສຸດ, ໃຫ້ວາງໄວ້ບ່ອນສູງໃກ້ໆປ່ອງຢ້ຽມ ແລ້ວລອງສະແກນໃໝ່." "ສະແກນອີກ" "ແລ້ວໆ" @@ -83,5 +84,7 @@ "ສະແກນຫາຊ່ອງໂທລະພາບ" "ຕັ້ງຄ່າຕົວຮັບສັນຍານໂທລະພາບ" "ຕັ້ງຄ່າຕົວຮັບສັນຍານໂທລະພາບແບບ USB" - "ການເຊື່ອມຕໍ່ USB TV tuner ຖືກຕັດແລ້ວ." + "ຕັ້ງຄ່າ Network TV Tuner" + "ຕັດການເຊື່ອມຕໍ່ USB TV tuner ແລ້ວ." + "ຕັດການເຊື່ອມຕໍ່ຕົວຈູນສັນຍານເຄືອຂ່າຍແລ້ວ." diff --git a/usbtuner-res/values-lt/strings.xml b/usbtuner-res/values-lt/strings.xml index 3ce2efc7..cc4ff197 100644 --- a/usbtuner-res/values-lt/strings.xml +++ b/usbtuner-res/values-lt/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "TV imtuvas" "USB TV imtuvas" - "Įjungta" - "Išjungti" + "Tinklo TV imtuvas (BETA)" "Palaukite, kol baigsis apdorojimas" - "Pasirinkite kanalo šaltinį" - "Nėra signalo" - "Nepavyko įjungti kanalo „%s“" - "Nepavyko suderinti" "Imtuvo programinė įranga buvo neseniai atnaujinta. Iš naujo nuskaitykite kanalus." "Įgalinkite erdvinį garsą sistemos garso nustatymuose, kad galėtumėte įgalinti garsą" + "Negalima paleisti garso įrašo. Bandykite naudoti kitą TV" "Kanalų imtuvo sąranka" "TV imtuvo sąranka" "USB kanalų imtuvo sąranka" + "Tinklo imtuvo sąranka" "Įsitikinkite, kad jūsų TV prijungtas prie TV signalo šaltinio.\n\nJei naudojate belaidę anteną, koreguokite jos vietą arba kryptį. Kad pasiektumėte geriausių rezultatų, ją padėkite aukštai prie lango." "Įsitikinkite, kad USB imtuvas prijungtas prie maitinimo ir TV signalo šaltinio.\n\nJei naudojate belaidę anteną, koreguokite jos vietą arba kryptį, kad būtų rodoma kuo daugiau kanalų. Kad pasiektumėte geriausių rezultatų, ją padėkite aukštai prie lango." + "Įsitikinkite, kad tinklo imtuvas yra įjungtas ir prijungtas prie TV signalo šaltinio.\n\nJei naudojate belaidę anteną, koreguokite jos vietą arba kryptį. Kad pasiektumėte geriausių rezultatų, ją padėkite aukštai prie lango ir nuskaitykite dar kartą." "Tęsti" "Ne dabar" @@ -40,6 +38,7 @@ "Iš naujo vykdyti kanalų sąranką?" "Tai atlikus bus pašalinti kanalai, kuriuos aptiko TV imtuvas, ir bus vėl ieškoma naujų kanalų.\n\nNuskaičius nerasta jokių kanalų. Įsitikinkite, kad jūsų TV prijungtas prie TV signalo šaltinio.\n\nJei naudojate belaidę anteną, koreguokite jos vietą arba kryptį. Kad pasiektumėte geriausių rezultatų, ją padėkite aukštai prie lango." "Taip bus pašalinti rasti kanalai iš USB imtuvo ir nauji kanalai nuskaityti dar kartą.\n\nĮsitikinkite, kad USB imtuvas prijungtas prie maitinimo ir TV signalo šaltinio.\n\nJei naudojate belaidę anteną, koreguokite jos vietą arba kryptį, kad būtų rodoma kuo daugiau kanalų. Kad pasiektumėte geriausių rezultatų, ją padėkite aukštai prie lango." + "Atlikus šį veiksmą iš tinklo imtuvo bus pašalinti rasti kanalai ir bus dar kartą nuskaitoma ieškant naujų kanalų.\n\nĮsitikinkite, kad tinklo imtuvas yra įjungtas ir prijungtas prie TV signalo šaltinio.\n\nJei naudojate belaidę anteną, koreguokite jos vietą arba kryptį. Kad pasiektumėte geriausių rezultatų, ją padėkite aukštai prie lango ir nuskaitykite dar kartą." "Tęsti" "Atšaukti" @@ -54,6 +53,7 @@ "TV imtuvo sąranka" "USB kanalų imtuvo sąranka" + "Tinklo kanalų imtuvo sąranka" "Tai gali užtrukti kelias minutes" "Derintuvas laikinai nepasiekiamas arba jau yra naudojamas įrašant." @@ -82,6 +82,7 @@ "Nerasta kanalų" "Nuskaičius nerasta jokių kanalų. Įsitikinkite, kad jūsų TV prijungtas prie TV signalo šaltinio.\n\nJei naudojate belaidę anteną, koreguokite jos vietą arba kryptį. Kad pasiektumėte geriausių rezultatų, ją padėkite aukštai prie lango ir nuskaitykite dar kartą." "Nuskaitant nerasta kanalų. Patvirtinkite, ar USB imtuvas prijungtas prie TV signalo šaltinio.\n\nJei naudojate belaidę anteną, koreguokite jos vietą arba kryptį. Kad pasiektumėte geriausių rezultatų, ją padėkite aukštai prie lango ir nuskaitykite dar kartą." + "Nuskaičius nerasta jokių kanalų. Įsitikinkite, kad tinklo imtuvas yra įjungtas ir prijungtas prie TV signalo šaltinio.\n\nJei naudojate belaidę anteną, koreguokite jos vietą arba kryptį. Kad pasiektumėte geriausių rezultatų, ją padėkite aukštai prie lango ir nuskaitykite dar kartą." "Nuskaityti dar kartą" "Atlikta" @@ -89,5 +90,7 @@ "Vykdykite TV kanalų nuskaitymą" "TV imtuvo sąranka" "USB TV imtuvo sąranka" - "USB TV imtuvas atjungtas." + "Tinklo TV imtuvo sąranka" + "USB TV imtuvas atjungtas." + "Tinklo imtuvas atjungtas." diff --git a/usbtuner-res/values-lv/strings.xml b/usbtuner-res/values-lv/strings.xml index 9337e7b3..f59276c4 100644 --- a/usbtuner-res/values-lv/strings.xml +++ b/usbtuner-res/values-lv/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "TV kanālu meklētājs" "USB TV kanālu meklētājs" - "Ieslēgta" - "Izslēgta" + "Interneta TV kanālu meklētājs (BETA)" "Lūdzu, uzgaidiet, līdz tiks pabeigta apstrāde!" - "Atlasiet kanāla avotu" - "Nav signāla" - "Neizdevās atrast kanālu %s." - "Neizdevās atrast" "Nesen tika atjaunināta kanālu meklētāja programmatūra. Lūdzu, atkārtoti meklējiet kanālus." "Lai ieslēgtu audio, sistēmas skaņas iestatījumos iespējojiet ieskaujošo skaņu." + "Nevar atskaņot audio. Lūdzu, izmantojiet citu televizoru." "Kanālu meklētāja iestatīšana" "TV kanālu meklētāja iestatīšana" "USB kanālu meklētāja iestatīšana" + "Interneta kanālu meklētāja iestatīšana" "Pārbaudiet, vai televizors ir pievienots TV signāla avotam.\n\nJa izmantojat bezvadu antenu, iespējams, būs jāmaina tās novietojums vai virziens, lai uztvertu lielāko daļu kanālu. Lai iegūtu labākos rezultātus, novietojiet antenu augstu pie loga." "Pārbaudiet, vai USB kanālu meklētājs ir pievienots strāvas avotam un TV signāla avotam.\n\nJa izmantojat bezvadu antenu, mainiet tās novietojumu un virzienu. Lai iegūtu labākos rezultātus, novietojiet antenu augstu pie loga un atkārtojiet kanālu meklēšanu." + "Pārbaudiet, vai interneta kanālu meklētājs ir ieslēgts un pievienots TV signāla avotam.\n\nJa izmantojat bezvadu antenu, iespējams, jāmaina tās novietojums vai virziens, lai uztvertu lielāko daļu kanālu. Lai iegūtu labākos rezultātus, novietojiet antenu augstu pie loga." "Turpināt" "Vēlāk" @@ -40,6 +38,7 @@ "Vai atkārtot kanālu iestatīšanu?" "Šādi no TV kanālu meklētāja tiks noņemti atrastie kanāli un tiks vēlreiz meklēti jauni kanāli.\n\nPārbaudiet, vai televizors ir pievienots TV signāla avotam.\n\nJa izmantojat bezvadu antenu, iespējams, būs jāmaina tās novietojums vai virziens, lai uztvertu lielāko daļu kanālu. Lai iegūtu labākos rezultātus, novietojiet antenu augstu pie loga." "Tādējādi tiks noņemti kanāli, kas tika atrasti ar USB kanālu meklētāju, un tiks atkārtota kanālu meklēšana.\n\nPārbaudiet, vai USB kanālu meklētājs ir pievienots strāvas avotam un TV signāla avotam.\n\nJa izmantojat bezvadu antenu, mainiet tās novietojumu un virzienu, lai uztvertu lielāko daļu kanālu. Lai iegūtu labākos rezultātus, novietojiet antenu augstu pie loga." + "Šādi no interneta kanālu meklētāja tiks noņemti atrastie kanāli un tiks vēlreiz meklēti jauni kanāli.\n\nPārbaudiet, vai interneta kanālu meklētājs ir ieslēgts un pievienots TV signāla avotam.\n\nJa izmantojat bezvadu antenu, iespējams, jāmaina tās novietojums vai virziens, lai uztvertu lielāko daļu kanālu. Lai iegūtu labākos rezultātus, novietojiet antenu augstu pie loga." "Turpināt" "Atcelt" @@ -54,6 +53,7 @@ "TV kanālu meklētāja iestatīšana" "USB kanālu meklētāja iestatīšana" + "Interneta kanālu meklētāja iestatīšana" "Tas var ilgt vairākas minūtes." "Kanālu meklētājs īslaicīgi nav pieejams, vai arī tas jau tiek izmantots ierakstīšanai." @@ -79,6 +79,7 @@ "Netika atrasts neviens kanāls" "Veicot meklēšanu, netika atrasts neviens kanāls. Pārbaudiet, vai televizors pievienots TV signāla avotam.\n\nJa izmantojat bezvadu antenu, mainiet tās novietojumu vai virzienu. Lai iegūtu labākos rezultātus, novietojiet antenu augstu pie loga, bet pēc tam vēlreiz veiciet meklēšanu." "Netika atrasts neviens kanāls. Pārbaudiet, vai USB kanālu meklētājs ir pievienots strāvas avotam un TV signāla avotam.\n\nJa izmantojat bezvadu antenu, mainiet tās novietojumu un virzienu. Lai iegūtu labākos rezultātus, novietojiet antenu augstu pie loga un atkārtojiet kanālu meklēšanu." + "Netika atrasts neviens kanāls. Pārbaudiet, vai interneta kanālu meklētājs ir ieslēgts un pievienots TV signāla avotam.\n\nJa izmantojat bezvadu antenu, mainiet tās novietojumu un virzienu. Lai iegūtu labākos rezultātus, novietojiet antenu augstu pie loga un atkārtojiet kanālu meklēšanu." "Meklēt vēlreiz" "Gatavs" @@ -86,5 +87,7 @@ "TV kanālu meklēšana" "TV kanālu meklētāja iestatīšana" "USB TV kanālu meklētāja iestatīšana" - "USB TV kanālu meklētājs ir atvienots." + "Interneta TV kanālu meklētāja iestatīšana" + "USB TV kanālu meklētājs ir atvienots." + "Interneta kanālu meklētājs ir atvienots." diff --git a/usbtuner-res/values-mk-rMK/strings.xml b/usbtuner-res/values-mk-rMK/strings.xml index fb2e71e7..8dfbc306 100644 --- a/usbtuner-res/values-mk-rMK/strings.xml +++ b/usbtuner-res/values-mk-rMK/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "ТВ приемник" "ТВ приемник со USB" - "Вклучено" - "Исклучено" + "Мрежен ТВ приемник (БЕТА)" "Почекајте да заврши обработувањето" - "Изберете го изворот на каналот" - "Нема сигнал" - "Не успеа да се избере %s" - "Не успеа да се избере" "Софтверот на приемникот неодамна е ажуриран. Скенирајте ги каналите повторно." "Овозможете опкружувачки звук во поставките за звуци на системот за да овозможите аудио" + "Не може да се пушти аудио. Обидете се со друг ТВ" "Поставување приемник на канали" "Поставување ТВ приемник" "Поставување на USB-приемникот за канали" + "Поставување мрежен приемник" "Потврдете дека телевизорот е приклучен и поврзан со изворот на ТВ сигналот.\n\nАко користите безжична антена, можеби ќе треба да ја приспособите поставеноста или насоката за да примате најмногу канали. За најдобри резултати, поставете ја високо и во близина на прозорец." "Потврдете дека USB-приемникот е приклучен и поврзан со изворот на ТВ сигналот.\n\nАко користите безжична антена, можеби ќе треба да ја приспособите поставеноста или насоката за да примате најмногу канали. За најдобри резултати, поставете ја високо и во близина на прозорец." + "Потврдете дека мрежниот приемник е вклучен и поврзан со изворот на ТВ сигналот.\n\nАко користите безжична антена, може ќе треба да ја приспособите поставеноста или насоката за да примате најмногу канали. За најдобри резултати, поставете ја високо и во близина на прозорец." "Продолжи" "Не сега" @@ -40,6 +38,7 @@ "Да се изврши поставувањето на каналите повторно?" "Ова ќе ги отстрани каналите од ТВ приемникот и ќе скенира за нови канали повторно.\n\nПотврдете дека телевизорот е поврзан со изворот на ТВ сигналот.\n\nАко користите безжична антена, приспособете ја поставеноста или насоката. За најдобри резултати, поставете ја високо и во близина на прозорец." "Ова ќе ги отстрани каналите од USB-приемникот и ќе скенира за нови канали повторно.\n\nПотврдете дека USB-приемникот е приклучен и поврзан со изворот на ТВ сигналот.\n\nАко користите безжична антена, можеби ќе треба да ја приспособите поставеноста или насоката за да примате најмногу канали. За најдобри резултати, поставете ја високо и во близина на прозорец." + "Ова ќе ги отстрани каналите од мрежниот приемник и ќе скенира за нови канали повторно.\n\nПотврдете дека мрежниот приемник е вклучен и поврзан со изворот на ТВ сигналот.\n\nАко користите безжична антена, може ќе треба да ја приспособите поставеноста или насоката. За најдобри резултати, поставете ја високо и во близина на прозорец." "Продолжи" "Откажи" @@ -54,6 +53,7 @@ "Поставување ТВ приемник" "Поставување на USB-приемникот за канали" + "Поставување мрежен приемник на канали" "Ова може да трае неколку минути" "Приемникот е привремено недостапен или веќе се користи за снимање." @@ -76,6 +76,7 @@ "Не се пронајдени канали" "Скенирањето не пронајде ниеден канал. Потврдете дека телевизорот е поврзан со изворот на ТВ сигналот.\n\nАко користите безжична антена, приспособете ја поставеноста или насоката. За најдобри резултати, поставете ја високо и во близина на прозорец и скенирајте повторно." "Скенирањето не пронајде ниеден канал. Потврдете дека USB-приемникот е приклучен и поврзан со изворот на ТВ сигналот.\n\nАко користите безжична антена, приспособете ја поставеноста или насоката. За најдобри резултати, поставете ја високо и во близина на прозорец и скенирајте повторно." + "Скенирањето не пронајде ниеден канал. Потврдете дека мрежниот приемник е вклучен и поврзан со изворот на ТВ сигналот.\n\nАко користите безжична антена, приспособете ја поставеноста или насоката. За најдобри резултати, поставете ја високо и во близина на прозорец, па скенирајте повторно." "Скенирај пак" "Готово" @@ -83,5 +84,7 @@ "Скенирај ТВ-канали" "Поставување ТВ приемник" "Поставување ТВ приемник со USB" - "TV приемникот преку USB е исклучен." + "Поставување мрежен ТВ приемник" + "USB-приемникот за TV е исклучен." + "Мрежниот приемник е исклучен." diff --git a/usbtuner-res/values-ml-rIN/strings.xml b/usbtuner-res/values-ml-rIN/strings.xml index d025ec7b..b68953d2 100644 --- a/usbtuner-res/values-ml-rIN/strings.xml +++ b/usbtuner-res/values-ml-rIN/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "ടിവി ട്യൂണർ" "USB ടിവി ട്യൂണർ" - "ഓണാക്കുക" - "ഓഫാക്കുക" + "നെറ്റ്‌വർക്ക് ടിവി ട്യൂണർ (ബീറ്റ)" "പ്രോസസ്സുചെയ്യൽ പൂർത്തിയാകുന്നത് വരെ കാത്തിരിക്കുക" - "നിങ്ങളുടെ ചാനൽ ഉറവിടം തിരഞ്ഞെടുക്കുക" - "സിഗ്‌നൽ ഇല്ല" - "%s എന്നതിലേക്ക് ട്യൂൺ ചെയ്യുന്നത് പരാജയപ്പെട്ടു" - "ട്യൂൺ ചെയ്യുന്നത് പരാജയപ്പെട്ടു" "ട്യൂണർ സോഫ്‌റ്റ്‌വെയർ അടുത്തിടെ അപ്‌ഡേറ്റുചെയ്‌തു. ചാനലുകൾ വീണ്ടും സ്‌കാൻ ചെയ്യുക." "ഓഡിയോ പ്രവർത്തനക്ഷമമാക്കുന്നതിന് സിസ്റ്റം ശബ്ദ ക്രമീകരണത്തിൽ സറൗണ്ട് ശബ്‌ദം പ്രവർത്തനക്ഷമമാക്കുക" + "ഓഡിയോ പ്ലേ ചെയ്യാൻ കഴിയുന്നില്ല. മറ്റൊരു ടിവിയിൽ ശ്രമിച്ചുനോക്കൂ" "ചാനൽ ട്യൂണർ സജ്ജമാക്കല്‍‌" "ടിവി ട്യൂണർ സജ്ജമാക്കല്‍‌" "USB ചാനൽ ട്യൂണർ സജ്ജമാക്കല്‍‌" + "നെറ്റ്‌വർക്ക് ട്യൂണർ സജ്ജമാക്കല്‍‌" "ഒരു ടിവി സിഗ്നൽ ഉറവിടത്തിലേക്ക് നിങ്ങളുടെ ടിവി കണക്റ്റുചെയ്തിട്ടുണ്ടെന്ന് ഉറപ്പാക്കുക.\n\nഉയരത്തിൽ വയ്ക്കേണ്ട തരത്തിലുള്ള ആന്റിനയാണ് നിങ്ങൾ ഉപയോഗിക്കുന്നതെങ്കിൽ, പരമാവധി ചാനലുകൾ ലഭിക്കുന്നതിന്, അതിന്റെ ഇരിപ്പോ ദിശയോ ക്രമീകരിക്കേണ്ടി വരാം. മികച്ച ഫലം ലഭിക്കാൻ, കുറച്ചുകൂടി ഉയരത്തിലും ജാലകത്തിന് അരികിലായും അത് സ്ഥാപിക്കുക." "USB ട്യൂണർ പ്ലഗിൻ ചെയ്തിട്ടുണ്ടെന്നും ഒരു ടിവി സിഗ്നൽ ഉറവിടത്തിലേക്ക് കണക്റ്റുചെയ്തിട്ടുണ്ടെന്നും ഉറപ്പാക്കുക.\n\nഉയരത്തിൽ വയ്ക്കേണ്ട തരത്തിലുള്ള ആന്റിനയാണ് നിങ്ങൾ ഉപയോഗിക്കുന്നതെങ്കിൽ, പരമാവധി ചാനലുകൾ ലഭിക്കുന്നതിന്, അതിന്റെ ഇരിപ്പോ ദിശയോ ക്രമീകരിക്കേണ്ടി വരാം. മികച്ച ഫലം ലഭിക്കാൻ, കുറച്ചുകൂടി ഉയരത്തിലും ജാലകത്തിന് അരികിലായും അത് സ്ഥാപിക്കുക." + "നെറ്റ്‌വർക്ക് ട്യൂണർ ഓണാക്കിയിട്ടുണ്ടെന്നും ഒരു ടിവി സിഗ്നൽ ഉറവിടത്തിലേക്ക് കണക്റ്റുചെയ്തിട്ടുണ്ടെന്നും ഉറപ്പാക്കുക.\n\nഉയരത്തിൽ വയ്ക്കേണ്ട തരത്തിലുള്ള ആന്റിനയാണ് നിങ്ങൾ ഉപയോഗിക്കുന്നതെങ്കിൽ, പരമാവധി ചാനലുകൾ ലഭിക്കുന്നതിന്, അതിന്റെ ഇരിപ്പോ ദിശയോ ക്രമീകരിക്കേണ്ടി വരാം. മികച്ച ഫലം ലഭിക്കാൻ, കുറച്ചുകൂടി ഉയരത്തിലും ജാലകത്തിന് അരികിലായും അത് സ്ഥാപിക്കുക." "തുടരുക" "ഇപ്പോൾ വേണ്ട" @@ -40,6 +38,7 @@ "ചാനൽ സജ്ജമാക്കൽ വീണ്ടും റൺ ചെയ്യണോ?" "ടിവി ട്യൂണറിൽ നിന്ന് കണ്ടെത്തിയ ചാനലുകളെ ഇത് നീക്കംചെയ്യും, പുതിയ ചാനലുകൾക്കായി വീണ്ടും സ്കാൻ ചെയ്യും.\n\nഒരു ടിവി സിഗ്നൽ ഉറവിടത്തിലേക്ക് നിങ്ങളുടെ ടിവി കണക്റ്റുചെയ്തിട്ടുണ്ടെന്ന് ഉറപ്പാക്കുക.\n\nഉയരത്തിൽ വയ്ക്കേണ്ട തരത്തിലുള്ള ആന്റിനയാണ് നിങ്ങൾ ഉപയോഗിക്കുന്നതെങ്കിൽ, പരമാവധി ചാനലുകൾ ലഭിക്കുന്നതിന്, അതിന്റെ ഇരിപ്പോ ദിശയോ ക്രമീകരിക്കേണ്ടി വരാം. മികച്ച ഫലം ലഭിക്കാൻ, കുറച്ചുകൂടി ഉയരത്തിലും ജാലകത്തിന് അരികിലായും അത് സ്ഥാപിക്കുക." "USB ട്യൂണറിൽ നിന്ന് കണ്ടെത്തിയ ചാനലുകളെ ഇത് നീക്കംചെയ്യും, പുതിയ ചാനലുകൾക്കായി വീണ്ടും സ്കാൻ ചെയ്യും.\n\nസ്കാൻ ചെയ്തപ്പോൾ ചാനലുകളൊന്നും കണ്ടെത്തിയില്ല. USB ട്യൂണർ പ്ലഗിൻ ചെയ്തിട്ടുണ്ടെന്നും ഒരു ടിവി സിഗ്നൽ ഉറവിടത്തിലേക്ക് കണക്റ്റുചെയ്തിട്ടുണ്ടെന്നും ഉറപ്പാക്കുക.\n\nഉയരത്തിൽ വയ്ക്കേണ്ട തരത്തിലുള്ള ആന്റിനയാണ് നിങ്ങൾ ഉപയോഗിക്കുന്നതെങ്കിൽ, പരമാവധി ചാനലുകൾ ലഭിക്കുന്നതിന്, അതിന്റെ ഇരിപ്പോ ദിശയോ ക്രമീകരിക്കേണ്ടി വരാം. മികച്ച ഫലം ലഭിക്കാൻ, കുറച്ചുകൂടി ഉയരത്തിലും ജാലകത്തിന് അരികിലായും അത് സ്ഥാപിക്കുക." + "നെറ്റ്‌വർക്ക് ട്യൂണറിൽ നിന്ന് കണ്ടെത്തിയ ചാനലുകളെ ഇത് നീക്കംചെയ്യും, പുതിയ ചാനലുകൾക്കായി വീണ്ടും സ്കാൻ ചെയ്യും.\n\nനെറ്റ്‌വർക്ക് ട്യൂണർ ഓണാക്കിയിട്ടുണ്ടെന്നും ഒരു ടിവി സിഗ്നൽ ഉറവിടത്തിലേക്ക് കണക്റ്റുചെയ്തിട്ടുണ്ടെന്നും ഉറപ്പാക്കുക.\n\nഉയരത്തിൽ വയ്ക്കേണ്ട തരത്തിലുള്ള ആന്റിനയാണ് നിങ്ങൾ ഉപയോഗിക്കുന്നതെങ്കിൽ, പരമാവധി ചാനലുകൾ ലഭിക്കുന്നതിന്, അതിന്റെ ഇരിപ്പോ ദിശയോ ക്രമീകരിക്കേണ്ടി വരാം മികച്ച ഫലം ലഭിക്കാൻ, കുറച്ചുകൂടി ഉയരത്തിലും ജാലകത്തിന് അരികിലായും അത് സ്ഥാപിക്കുക." "തുടരുക" "റദ്ദാക്കൂ" @@ -54,6 +53,7 @@ "ടിവി ട്യൂണർ സജ്ജമാക്കല്‍‌" "USB ചാനൽ ട്യൂണർ സജ്ജമാക്കല്‍‌" + "നെറ്റ്‌വർക്ക് ചാനൽ ട്യൂണർ സജ്ജമാക്കല്‍‌" "ഇതിന് കുറച്ച് സമയം എടുത്തേക്കാം" "ട്യൂണർ നിലവിൽ ലഭ്യമല്ല അല്ലെങ്കിൽ ഇതിനകം തന്നെ റെക്കോർഡിംഗ് ഉപയോഗിച്ചുകൊണ്ടിരിക്കുന്നു." @@ -76,6 +76,7 @@ "ചാനലുകളൊന്നും കണ്ടെത്തിയില്ല" "സ്കാൻ ചെയ്തപ്പോൾ ചാനലുകളൊന്നും കണ്ടെത്തിയില്ല. ഒരു ടിവി സിഗ്നൽ ഉറവിടത്തിലേക്ക് നിങ്ങളുടെ ടിവി കണക്റ്റുചെയ്തിട്ടുണ്ടെന്ന് ഉറപ്പാക്കുക.\n\nഉയരത്തിൽ വയ്ക്കേണ്ട തരത്തിലുള്ള ആന്റിനയാണ് നിങ്ങൾ ഉപയോഗിക്കുന്നതെങ്കിൽ അതിന്റെ ഇരിപ്പോ ദിശയോ ക്രമീകരിക്കുക. മികച്ച ഫലം ലഭിക്കാൻ, കുറച്ചുകൂടി ഉയരത്തിലും ജാലകത്തിന് അരികിലായും അത് സ്ഥാപിച്ച് വീണ്ടും സ്കാൻ ചെയ്യുക." "സ്കാൻ ചെയ്തപ്പോൾ ചാനലുകളൊന്നും കണ്ടെത്തിയില്ല. USB ട്യൂണർ പ്ലഗിൻ ചെയ്തിട്ടുണ്ടെന്നും ഒരു ടിവി സിഗ്നൽ ഉറവിടത്തിലേക്ക് കണക്റ്റുചെയ്തിട്ടുണ്ടെന്നും ഉറപ്പാക്കുക.\n\nഉയരത്തിൽ വയ്ക്കേണ്ട തരത്തിലുള്ള ആന്റിനയാണ് നിങ്ങൾ ഉപയോഗിക്കുന്നതെങ്കിൽ അതിന്റെ ഇരിപ്പോ ദിശയോ ക്രമീകരിക്കുക. മികച്ച ഫലം ലഭിക്കാൻ, കുറച്ചുകൂടി ഉയരത്തിലും ജാലകത്തിന് അരികിലായും അത് സ്ഥാപിച്ച് വീണ്ടും സ്കാൻ ചെയ്യുക." + "സ്കാൻ ചെയ്തപ്പോൾ ചാനലുകളൊന്നും കണ്ടെത്തിയില്ല. നെറ്റ്‌വർക്ക് ട്യൂണർ ഓണാക്കിയിട്ടുണ്ടെന്നും ഒരു ടിവി സിഗ്നൽ ഉറവിടത്തിലേക്ക് കണക്റ്റുചെയ്തിട്ടുണ്ടെന്നും ഉറപ്പാക്കുക.\n\nഉയരത്തിൽ വയ്ക്കേണ്ട തരത്തിലുള്ള ആന്റിനയാണ് നിങ്ങൾ ഉപയോഗിക്കുന്നതെങ്കിൽ അതിന്റെ ഇരിപ്പോ ദിശയോ ക്രമീകരിക്കുക. മികച്ച ഫലം ലഭിക്കാൻ, കുറച്ചുകൂടി ഉയരത്തിലും ജാലകത്തിന് അരികിലായും അത് സ്ഥാപിച്ച് വീണ്ടും സ്കാൻ ചെയ്യുക." "വീണ്ടും സ്കാൻ ചെയ്യുക" "പൂർത്തിയായി" @@ -83,5 +84,7 @@ "ടിവി ചാനലുകൾക്കായി സ്കാൻ ചെയ്യുക" "ടിവി ട്യൂണർ സജ്ജമാക്കല്‍‌" "USB ടിവി ട്യൂണർ സജ്ജമാക്കല്‍‌" - "USB ടിവി ട്യൂണർ വിച്ഛേദിച്ചു." + "നെറ്റ്‌വർക്ക് ടിവി ട്യൂണർ സജ്ജമാക്കല്‍‌" + "USB ടിവി ട്യൂണർ വിച്ഛേദിച്ചു." + "നെറ്റ്‌വർക്ക് ട്യൂണർ വിച്ഛേദിച്ചു." diff --git a/usbtuner-res/values-mn-rMN/strings.xml b/usbtuner-res/values-mn-rMN/strings.xml index 51f7a8dd..232ebac9 100644 --- a/usbtuner-res/values-mn-rMN/strings.xml +++ b/usbtuner-res/values-mn-rMN/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "ТВ тохируулагч" "USB ТВ тохируулагч" - "Идэвхтэй" - "Идэвхгүй" + "Сүлжээний ТВ Тохируулагч (БЭТА)" "Боловсруулж дуусах хүртэл хүлээнэ үү" - "Сувгийн эх үүсвэрээ сонгоно уу" - "Дохио алга" - "%s-д тохируулж чадсангүй" - "Тохируулж чадсангүй" "Тохируулагчийн програмыг саяхан шинэчилсэн байна. Дахин суваг хайна уу." "Аудиог идэвхжүүлэхийн тулд орчны дууны системийг дууны тохиргоонд идэвхжүүлнэ үү" + "Аудиог тоглуулах боломжгүй байна. Өөр ТВ дээр оролдож үзнэ үү" "Суваг тохируулагчийн тохиргоо" "ТВ тохируулагчийн тохиргоо" "USB суваг тохируулагчийн тохиргоо" + "Сүлжээ тохируулагчийн тохиргоо" "ТВ-ээ ТВ дохионы үүсвэртэй холбосон эсэхээ батална уу.\n\nХэрэв агаарын антен хэрэглэж байгаа бол олон суваг авахын тулд антены байршил эсвэл чиглэлийг өөрчилнө үү. Антеныг өндөрт, цонхны дэргэд байрлуулах нь илүү үр дүнтэй." "USB тохируулагчийг залгасан ба ТВ дохионы үүсвэртэй холбосон эсэхээ шалгана уу.\n\nХэрэв агаарын антен хэрэглэж байгаа бол олон суваг авахын тулд антены байршлыг өөрчилнө үү. Антеныг өндөрт, цонхны дэргэд байрлуулах нь илүү үр дүнтэй байдаг." + "Сүлжээ тохируулагчийг асааж, ТВ дохионы үүсвэртэй холбосон эсэхээ баталгаажуулна уу.\n\nХэрэв агаарын антен хэрэглэж байгаа бол олон суваг авахын тулд антены байршлыг өөрчилнө үү. Антеныг өндөрт, цонхны дэргэд байрлуулна уу." "Үргэлжлүүлэх" "Одоо биш" @@ -40,6 +38,7 @@ "Сувгийн тохируулгыг дахин ажиллуулах уу?" "Энэ нь ТВ тохируулагчаас олсон сувгийг устгаад, шинэ суваг хайх болно.\n\nТВ тохируулагчаа ТВ дохионы үүсвэртэй холбосон эсэхээ шалгана уу.\n\nХэрэв агаарын антен хэрэглэж байгаа бол олон суваг авахын тулд антены байршил эсвэл чиглэлийг өөрчилнө үү. Антеныг өндөрт, цонхны дэргэд байрлуулах нь илүү үр дүнтэй." "Энэ нь USB тохируулагчаас олдсон сувгийг устгаад, шинэ суваг хайх болно.\n\nUSB тохируулагчийг залгасан, ТВ дохионы үүсвэртэй холбосон эсэхээ шалгана уу.\n\nХэрэв агаарын антен хэрэглэж байгаа бол олон суваг авахын тулд антены байршлыг өөрчилнө үү. Антеныг өндөрт, цонхны дэргэд байрлуулах нь илүү үр дүнтэй." + "Энэ нь сүлжээ тохируулагчаас олдсон сувгийг устгаж, шинэ суваг хайх болно.\n\nСүлжээ тохируулагчийг асааж, ТВ дохио үүсвэртэй холбосон эсэхээ шалгана уу.\n\nХэрэв антен хэрэглэж байгаа бол антены байршлыг өөрчилнө үү. Антеныг өндөрт, цонхны дэргэд байрлуулаад, дахин хайна уу." "Үргэлжлүүлэх" "Цуцлах" @@ -54,6 +53,7 @@ "ТВ тохируулагчийн тохиргоо" "USB суваг тохируулагчийн тохиргоо" + "Сүлжээний суваг тохируулагчийн тохиргоо" "Хэдэн минут шаардлагатай" "Суваг солигч одоогоор боломжгүй, эсвэл үүнийг өөр бичлэгт ашиглаж байна." @@ -76,6 +76,7 @@ "Суваг олсонгүй" "Хайлтын явцад суваг олсонгүй. ТВ-ээ ТВ дохионы үүсвэрт холбосон эсэхээ шалгана уу.\n\nХэрэв агаарын антен хэрэглэж байгаа бол антены байршлыг эсвэл чиглэлийг өөрчилнө үү. Антеныг өндөрт, цонхны дэргэд байрлуулаад, дахин хайх нь илүү үр дүнтэй." "Хайлтын явцад суваг олдсонгүй. USB тохируулагчийг залгасан бөгөөд ТВ дохионы үүсвэртэй холбосон эсэхээ шалгана уу.\n\nХэрэв агаарын антен хэрэглэж байгаа бол антены байршил эсвэл чиглэлийг өөрчилнө үү. Антеныг өндөрт, цонхны дэргэд байрлуулахаад, дахин хайх нь илүү үр дүнтэй." + "Хайлтын явцад суваг олдсонгүй. Сүлжээ тохируулагчийг асааж, ТВ дохионы үүсвэртэй холбосон эсэхээ шалгана уу.\n\nХэрэв агаарын антен хэрэглэж байгаа бол антены байршил, эсвэл чиглэлийг өөрчилнө үү. Антеныг өндөрт, цонхны дэргэд байрлуулаад, дахин хайна уу." "Дахин хайх" "Дууссан" @@ -83,5 +84,7 @@ "TВ-н суваг хайх" "ТВ тохируулагчийн тохиргоо" "USB ТВ тохируулагчийн тохиргоо" - "USB ТВ тохируулагч салсан байна." + "Сүлжээний ТВ тохируулагчийн тохиргоо" + "USB ТВ тохируулагч салсан байна." + "Сүлжээ тааруулагч салсан байна." diff --git a/usbtuner-res/values-mr-rIN/strings.xml b/usbtuner-res/values-mr-rIN/strings.xml index 2ea242f1..af7676fe 100644 --- a/usbtuner-res/values-mr-rIN/strings.xml +++ b/usbtuner-res/values-mr-rIN/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "टीव्ही ट्यूनर" "USB टीव्ही ट्यूनर" - "चालू" - "बंद" + "नेटवर्क टीव्ही ट्यूनर (बीटा)" "कृपया प्रक्रिया पूर्ण होण्‍याची प्रतीक्षा करा" - "आपला चॅनेल स्रोत निवडा" - "सिग्नल नाही" - "%s वर ट्यून करण्‍यात अयशस्वी झाले" - "ट्यून करण्यात अयशस्वी झाले" "ट्यूनर सॉफ्टवेअर अलीकडे अद्यतनित केले आहे. कृपया चॅनेल पुन्हा स्कॅन करा." "ऑडिओ सक्षम करण्यासाठी सिस्टीम ध्वनी सेटिंग्ज मध्ये सराउंड ध्वनी सक्षम करा" + "ऑडिओ प्ले करू शकत नाही. कृपया दुसरा टीव्ही वापरून पहा" "चॅनेल ट्यूनर सेटअप" "टीव्ही ट्यूनर सेटअप" "USB चॅनेल ट्यूनर सेटअप" + "नेटवर्क ट्यूनर सेटअप" "टीव्ही सिग्नल स्रोताशी आपला टीव्ही कनेक्ट केला असल्याची खात्री करा. \n\n बिनतारी अँटेना वापरत असल्‍यास, आपल्‍याला कदाचित सर्वाधिक चॅनेल मिळविण्‍यासाठी त्याचे स्थान किंवा दिशा समायोजित करावी लागेल. उत्कृष्‍ट परिणामांसाठी, त्‍यास उंचावर आणि खिडकी जवळ ठेवा." "USB ट्यूनर प्लगिन केले आणि TV सिग्नल स्रोताशी कनेक्‍ट केले आहे हे सत्यापित करा.\n\nबिनतारी अँटेना वापरत असल्‍यास, आपल्‍याला कदाचित सर्वाधिक चॅनेल मिळविण्‍यासाठी त्याचे स्थान किंवा दिशा समायोजित करावी लागेल. उत्कृष्‍ट परिणामांसाठी, त्‍यास उंचावर आणि खिडकी जवळ ठेवा." + "नेटवर्क ट्यूनर प्लगिन केले आणि TV सिग्नल स्रोताशी कनेक्‍ट केले आहे हे सत्यापित करा.\n\nबिनतारी अँटेना वापरत असल्‍यास, आपल्‍याला कदाचित सर्वाधिक चॅनेल मिळविण्‍यासाठी त्याचे स्थान किंवा दिशा समायोजित करावी लागेल. उत्कृष्‍ट परिणामांसाठी, त्‍यास उंचावर आणि खिडकी जवळ ठेवा." "सुरू ठेवा" "सध्या नाही" @@ -40,6 +38,7 @@ "चॅनेल सेटअप वर परत यायचे?" "हे टीव्ही ट्यूनर वरून शोधलेले चॅनेल काढेल आणि नवीन चॅनेलसाठी पुन्हा स्कॅन करेल.\n\n टीव्ही सिग्नल स्रोताशी आपला टीव्ही कनेक्ट केला असल्याचे सत्यापित करा.\n\nबिनतारी अँटेना वापरत असल्‍यास, आपल्‍याला कदाचित सर्वाधिक चॅनेल मिळविण्‍यासाठी त्याचे स्थान किंवा दिशा समायोजित करावी लागेल. उत्कृष्‍ट परिणामांसाठी, त्‍यास उंचावर आणि खिडकी जवळ ठेवा." "हे USB ट्यूनर वरून शोधलेले चॅनेल काढेल आणि नवीन चॅनेलसाठी पुन्हा स्कॅन करेल.\n\n USB ट्यूनर प्लगिन केले आणि TV सिग्नल स्त्रोताशी कनेक्‍ट केले आहे हे सत्यापित करा.\n\nबिनतारी अँटेना वापरत असल्‍यास, आपल्‍याला कदाचित सर्वाधिक चॅनेल मिळविण्‍यासाठी त्याचे स्थान किंवा दिशा समायोजित करावी लागेल. उत्कृष्‍ट परिणामांसाठी, त्‍यास उंचावर आणि खिडकी जवळ ठेवा." + "हे नेटवर्क ट्यूनर वरून शोधलेले चॅनेल काढेल आणि नवीन चॅनेलसाठी पुन्हा स्कॅन करेल.\n\n नेटवर्क ट्यूनर प्लगिन केले आणि टीव्ही सिग्नल स्रोताशी कनेक्‍ट केले आहे हे सत्यापित करा.\n\nबिनतारी अँटेना वापरत असल्‍यास, आपल्याला कदाचित सर्वाधिक चॅनेल मिळविण्‍यासाठी त्याचे स्थान किंवा दिशा समायोजित करावी लागेल. उत्कृष्‍ट परिणामांसाठी, त्‍यास उंचावर आणि खिडकी जवळ ठेवा." "सुरू ठेवा" "रद्द करा" @@ -54,6 +53,7 @@ "टीव्ही ट्यूनर सेटअप" "USB चॅनेल ट्यूनर सेटअप" + "नेटवर्क चॅनेल ट्यूनर सेटअप" "यास काही मिनिटे लागू शकतात" "ट्यूनर तात्पुरते उपलब्ध नाही किंवा रेकॉर्डिंगद्वारे आधीपासून वापरले गेले आहे." @@ -76,6 +76,7 @@ "कोणतेही चॅनेल आढळले नाही" "स्कॅन करताना कोणतेही चॅनेल आढळले नाही. टीव्ही सिग्नल स्रोताशी आपला टीव्ही कनेक्ट केला असल्याचे सत्यापित करा.\n\nबिनतारी अँटेना वापरत असल्‍यास, त्याचे स्थान किंवा दिशा समायोजित करा. उत्कृष्‍ट परिणामांसाठी, त्‍यास उंचावर आणि खिडकी जवळ ठेवा आणि पुन्हा स्कॅन करा." "स्कॅन करताना कोणतेही चॅनेल आढळले नाही. USB ट्यूनर प्लगिन केले आणि TV सिग्नल स्रोताशी कनेक्‍ट केले आहे हे सत्यापित करा.\n\nबिनतारी अँटेना वापरत असल्‍यास, त्याचे स्थान किंवा दिशा समायोजित करा. उत्कृष्‍ट परिणामांसाठी, त्‍यास उंचावर आणि खिडकी जवळ ठेवा आणि पुन्हा स्कॅन करा." + "स्कॅन करताना कोणतेही चॅनेल आढळले नाहीत. नेटवर्क ट्यूनर प्लगिन केले आणि टीव्ही सिग्नल स्रोताशी कनेक्‍ट केले आहे हे सत्यापित करा.\n\nबिनतारी अँटेना वापरत असल्‍यास, त्याचे स्थान किंवा दिशा समायोजित करा. उत्कृष्‍ट परिणामांसाठी, त्‍यास उंचावर आणि खिडकी जवळ ठेवा आणि पुन्हा स्कॅन करा." "पुन्हा स्कॅन करा" "पूर्ण झाले" @@ -83,5 +84,7 @@ "टीव्ही चॅनेलसाठी स्कॅन करा" "टीव्ही ट्यूनर सेटअप" "USB टीव्ही ट्यूनर सेटअप" - "USB TV ट्यूनर डिस्कनेक्ट केला." + "नेटवर्क टीव्ही ट्यूनर सेटअप" + "USB टीव्ही ट्यूनर डिस्कनेक्ट केला." + "नेटवर्क ट्यूनर डिस्कनेक्ट केले." diff --git a/usbtuner-res/values-ms-rMY/strings.xml b/usbtuner-res/values-ms-rMY/strings.xml index 578e8ae5..a3ca255c 100644 --- a/usbtuner-res/values-ms-rMY/strings.xml +++ b/usbtuner-res/values-ms-rMY/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "Penala TV" "Penala TV USB" - "Hidupkan" - "Matikan" + "Penala TV Rangkaian (BETA)" "Sila tunggu sehingga proses selesai" - "Pilih sumber saluran anda" - "Tiada Isyarat" - "Gagal menala ke %s" - "Gagal menala" "Perisian penala telah dikemas kini baru-baru ini. Sila imbas semula saluran." "Dayakan bunyi keliling dalam tetapan bunyi sistem untuk mendayakan audio" + "Tidak dapat memainkan audio. Sila cuba TV lain" "Persediaan penala saluran" "Persediaan Penala TV" "Persediaan penala saluran USB" + "Persediaan penala rangkaian" "Sahkan bahawa TV anda disambungkan pada sumber isyarat TV.\n\nJika menggunakan antena siaran, anda mungkin perlu melaraskan peletakan atau arahnya untuk menerima saluran terbanyak. Untuk mendapatkan hasil yang terbaik, letakkan antena itu di tempat yang tinggi dan berdekatan dengan tingkap." "Sahkan bahawa penala USB dipalamkan dan disambungkan pada sumber isyarat TV.\n\nJika menggunakan antena siaran, anda mungkin perlu melaraskan peletakan atau arahnya untuk menerima saluran terbanyak. Untuk mendapatkan hasil yang terbaik, letakkan antena itu di tempat yang tinggi dan berdekatan dengan tingkap." + "Sahkan bahawa penala rangkaian telah dihidupkan dan disambungkan pada sumber isyarat TV\n\nJika menggunakan antena siaran, anda mungkin perlu melaraskan peletakan atau arahnya untuk menerima saluran terbanyak. Untuk mendapatkan hasil yang terbaik, letakkan antena itu di tempat yang tinggi dan berdekatan dengan tingkap." "Teruskan" "Bukan sekarang" @@ -40,6 +38,7 @@ "Jalankan semula persediaan saluran?" "Tindakan ini akan mengalih keluar saluran yang ditemui daripada penala TV dan mengimbas saluran baharu sekali lagi.\n\nSahkan bahawa penala TV telah disambungkan pada sumber isyarat TV.\n\nJika menggunakan antena siaran, anda mungkin perlu melaraskan peletakan atau arahnya untuk menerima saluran terbanyak. Untuk mendapatkan hasil yang terbaik, letakkan antena itu di tempat yang tinggi dan berdekatan dengan tingkap." "Tindakan ini akan mengalih keluar saluran yang ditemui daripada penala USB dan mengimbas saluran baharu sekali lagi.\n\nSahkan bahawa penala USB telah dipalamkan dan disambungkan pada sumber isyarat TV.\n\nJika menggunakan antena siaran, anda mungkin perlu melaraskan peletakan atau arahnya untuk menerima saluran terbanyak. Untuk mendapatkan hasil yang terbaik, letakkan antena itu di tempat yang tinggi dan berdekatan dengan tingkap." + "Tindakan ini akan mengalih keluar saluran yang ditemui daripada penala rangkaian dan mengimbas saluran baharu sekali lagi.\n\nSahkan bahawa penala rangkaian telah dihidupkan dan disambungkan pada sumber isyarat TV.\n\nJika menggunakan antena siaran, anda mungkin perlu melaraskan peletakan atau arahnya untuk menerima saluran terbanyak. Untuk mendapatkan hasil yang terbaik, letakkan antena itu di tempat yang tinggi dan berdekatan dengan tingkap." "Teruskan" "Batal" @@ -54,6 +53,7 @@ "Persediaan penala TV" "Persediaan penala saluran USB" + "Persediaan penala saluran rangkaian" "Proses ini mungkin mengambil masa beberapa minit" "Penala tidak tersedia buat sementara waktu atau sudah pun digunakan oleh rakaman." @@ -76,6 +76,7 @@ "Tiada Saluran ditemui" "Pengimbasan ini tidak menemui sebarang saluran. Sahkan bahawa TV anda disambungkan pada sumber isyarat TV.\n\nJika menggunakan antena siaran, laraskan peletakan atau arahnya. Untuk mendapatkan hasil yang terbaik, letakkan antena itu di tempat yang tinggi dan berdekatan dengan tingkap, kemudian imbas sekali lagi." "Pengimbasan tidak menemui sebarang saluran. Sahkan bahawa penala USB dipalamkan dan disambungkan pada sumber isyarat TV.\n\nJika menggunakan antena siaran, laraskan peletakan atau arahnya. Untuk mendapatkan hasil yang terbaik, letakkan antena itu di tempat yang tinggi dan berdekatan dengan tingkap, kemudian imbas sekali lagi." + "Pengimbasan ini tidak menemui sebarang saluran. Sahkan bahawa penala rangkaian telah dihidupkan dan disambungkan pada sumber isyarat TV.\n\nJika menggunakan antena siaran, laraskan peletakan atau arahnya. Untuk mendapatkan hasil yang terbaik, letakkan antena itu di tempat yang tinggi dan berdekatan dengan tingkap, kemudian imbas sekali lagi." "Imbas lagi" "Selesai" @@ -83,5 +84,7 @@ "Imbas saluran TV" "Persediaan Penala TV" "Persediaan Penala TV USB" - "Penala TV USB diputuskan sambungan." + "Persediaan Penala TV Rangkaian" + "Penala TV USB diputuskan sambungan." + "Penala rangkaian diputuskan sambungan." diff --git a/usbtuner-res/values-my-rMM/strings.xml b/usbtuner-res/values-my-rMM/strings.xml index 56a29e51..0914e8ec 100644 --- a/usbtuner-res/values-my-rMM/strings.xml +++ b/usbtuner-res/values-my-rMM/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "တီဗီချန်နယ်ချိန်ကိရိယာ" "USB တီဗီချန်နယ်ချိန်ကိရိယာ" - "ဖွင့်ပါ" - "ပိတ်ပါ" + "ကွန်ရက်တီဗီ လိုင်းချိန်စနစ် (စမ်းသပ်ဆော့ဖ်ဝဲ)" "စီမံဆောင်ရွက်မှု အဆုံးသတ်ရန် ခဏစောင့်ပါ" - "သင့်ချန်နယ်လိုင်းထုတ်လွှင့်ရာ အရင်းအမြစ်ကို ရွေးပါ" - "လိုင်းမမိပါ" - "%s သို့ ချိန်ညှိခြင်း မအောင်မြင်ပါ" - "ချိန်ညှိ၍ မရခဲ့ပါ" "ချန်နယ်ချိန်ဆော့ဖ်ဝဲကို မကြာသေးမီက အပ်ဒိတ်လုပ်ခဲ့သည်။ ချန်နယ်လိုင်းများကို ပြန်ရှာပါ။" "အသံဖွင့်ရန် ပတ်ပတ်လည်အသံစနစ်ဆက်တင်များကို ဖွင့်ပါ" + "အသံဖွင့်၍မရပါ။ အခြားတီဗီတွင် စမ်းကြည့်ပါ" "ချန်နယ်ချိန်ကိရိယာ ထည့်သွင်းတပ်ဆင်မှု" "တီဗီချန်နယ်ချိန်ကိရိယာ ပြင်ဆင်သတ်မှတ်မှု" "USB ချန်နယ်ချိန်ကိရိယာ ပြင်ဆင်သတ်မှတ်မှု" + "တီဗီလိုင်းဖမ်းကိရိယာ စနစ်ထည့်သွင်းမှု" "သင့်တီဗီသည် တီဗီချန်နယ်ထုတ်လွှင့်ရာ အရင်းအမြစ်တစ်ခုနှင့် ချိတ်ဆက်ထားကြောင်း အတည်ပြုပါ။\n\nအကယ်၍ ကောင်းကင်အန်တန်နာတိုင်ကို အသုံးပြုနေပါက ချန်နယ်အများစုကို ဖမ်းယူနိုင်ရန် ၎င်း၏အနေအထား (သို့) ဦးတည်ဘက်ကို ချိန်ညှိရန် လိုပါသည်။ ရလဒ်များအကောင်းဆုံး ဖြစ်စေရန် ၎င်းကို ပြတင်းပေါက်နားတွင် မြင့်မြင့်ထားပါ။" "USB ချန်နယ်ချိန်ကိရိယာကို တပ်ဆင်ထားပြီး တီဗီချန်နယ်ထုတ်လွှင့်ရာ အရင်းအမြစ်တစ်ခုနှင့် ချိတ်ဆက်ထားကြောင်း အတည်ပြုပါ။\n\nအကယ်၍ ကောင်းကင်အန်တန်နာတိုင်ကို အသုံးပြုနေပါက ချန်နယ်အများစုကို ဖမ်းယူနိုင်ရန် ၎င်း၏အနေအထား (သို့) ဦးတည်ဘက်ကို ချိန်ညှိရန် လိုပါသည်။ ရလဒ်များအကောင်းဆုံး ဖြစ်စေရန် ၎င်းကို ပြတင်းပေါက်နားတွင် မြင့်မြင့်ထားပါ။" + "တီဗီလိုင်းဖမ်းကိရိယာကို ပါဝါဖွင့်ထားခြင်း ရှိ မရှိနှင့် တီဗီစလောင်းသို့ ချိတ်ဆက်ထားခြင်းရှိမရှိ စစ်ဆေးပါ။\n\nအင်တင်နာကို အသုံးပြုလျှင် ၎င်း၏အနေအထား သို့မဟုတ် ဦးတည်ချက်တို့ကို ချိန်ညှိရန် လိုအပ်ပါသည်။ အကောင်းဆုံးရလဒ်အတွက် ပြတင်းပေါက်အနီး အမြင့်ပိုင်းတွင် ထားပါ။" "ရှေ့ဆက်ရန်" "မလုပ်သေးပါ" @@ -40,6 +38,7 @@ "ချန်နယ်စနစ်ထည့်သွင်းမှု ပြန်စမလား။" "ဤလုပ်ဆောင်ချက်သည် တီဗီချန်နယ်ချိန်ကိရိယာက တွေ့ရှိထားသော ချန်နယ်လိုင်းများကို ဖယ်ရှားလိုက်ပြီး ချန်နယ်အသစ်များကို ထပ်မံရှာဖွေလိမ့်မည်။\n\nသင့်တီဗီသည် တီဗီချန်နယ်ထုတ်လွှင့်ရာ အရင်းအမြစ်တစ်ခုနှင့် ချိတ်ဆက်ထားကြောင်း အတည်ပြုပါ။\n\nအကယ်၍ ကောင်းကင်အန်တန်နာတိုင်ကို အသုံးပြုနေပါက ချန်နယ်အများစုကို ဖမ်းယူနိုင်ရန် ၎င်း၏အနေအထား (သို့) ဦးတည်ဘက်ကို ချိန်ညှိရန် လိုပါသည်။ ရလဒ်များအကောင်းဆုံး ဖြစ်စေရန် ၎င်းကို ပြတင်းပေါက်နားတွင် မြင့်မြင့်ထားပါ။" "ဤလုပ်ဆောင်ချက်သည် USB ချန်နယ်ချိန်ကိရိယာက တွေ့ရှိထားသော ချန်နယ်လိုင်းများကို ဖယ်ရှားလိုက်ပြီး ချန်နယ်အသစ်များကို ထပ်မံရှာဖွေလိမ့်မည်။\n\nUSB ချန်နယ်ချိန်ကိရိယာကို တပ်ဆင်ထားပြီး တီဗီချန်နယ်ထုတ်လွှင့်ရာ အရင်းအမြစ်တစ်ခုနှင့် ချိတ်ဆက်ထားကြောင်း အတည်ပြုပါ။\n\nအကယ်၍ ကောင်းကင်အန်တန်နာတိုင်ကို အသုံးပြုနေပါက ချန်နယ်အများစုကို ဖမ်းယူနိုင်ရန် ၎င်း၏အနေအထား (သို့) ဦးတည်ဘက်ကို ချိန်ညှိရန် လိုပါသည်။ ရလဒ်များအကောင်းဆုံး ဖြစ်စေရန် ၎င်းကို ပြတင်းပေါက်နားတွင် မြင့်မြင့်ထားပါ။" + "၎င်းသည် တီဗီလိုင်းဖမ်းကိရိယာက ရှာဖွေတွေ့ရှိခဲ့သည့် ချန်နယ်များကို ဖယ်ရှားလိုက်မည်ဖြစ်ပြီး ချန်နယ်အသစ်များကို ထပ်မံရှာဖွေသွားပါမည်။\n\nတီဗီလိုင်းဖမ်းကိရိယာကို ပါဝါဖွင့်ထားခြင်း ရှိ မရှိနှင့် တီဗီစလောင်းသို့ ချိတ်ဆက်ထားခြင်းရှိမရှိ စစ်ဆေးပါ။\n\nအင်တင်နာကို အသုံးပြုလျှင် ၎င်း၏အနေအထား သို့မဟုတ် ဦးတည်ချက်တို့ကို ချိန်ညှိရန် လိုအပ်ပါသည်။ အကောင်းဆုံးရလဒ်အတွက် ပြတင်းပေါက်အနီး အမြင့်ပိုင်းတွင် ထားပါ။" "ရှေ့ဆက်ရန်" "မလုပ်တော့" @@ -54,6 +53,7 @@ "တီဗီချန်နယ်ချိန်ကိရိယာ ပြင်ဆင်သတ်မှတ်မှု" "USB ချန်နယ်ချိန်ကိရိယာ ပြင်ဆင်သတ်မှတ်မှု" + "တီဗီလိုင်းဖမ်းကိရိယာ စနစ်ထည့်သွင်းမှု" "မိနစ်အနည်းငယ် ကြာနိုင်ပါသည်" "လိုင်းချိန်စက်သည် ယာယီမရနိုင်သေးပါ သို့မဟုတ် ဖမ်းယူခြင်းအတွက် အသုံးပြုနေပြီး ဖြစ်ပါသည်။" @@ -76,6 +76,7 @@ "ချန်နယ်တစ်လိုင်းမျှ မတွေ့ပါ" "ချန်နယ်များရှာဖွေရာတွင် တစ်လိုင်းမျှ ရှာမတွေ့ပါ။ သင့်တီဗီသည် တီဗီချန်နယ်ထုတ်လွှင့်ရာ အရင်းအမြစ်တစ်ခုနှင့် ချိတ်ဆက်ထားကြောင်း အတည်ပြုပါ။\n\n အကယ်၍ ကောင်းကင်အန်တန်နာတိုင်ကို အသုံးပြုနေပါက ၎င်း၏ အနေအထား (သို့) ဦးတည်ဘက်ကို ချိန်ညှိပါ။ ရလဒ်များအကောင်းဆုံးဖြစ်စေရန် ၎င်းကို ပြတင်းပေါက်နားတွင် မြင့်မြင့်ထားပြီး ထပ်ရှာကြည့်ပါ။" "ချန်နယ်များရှာဖွေရာတွင် တစ်လိုင်းမျှ ရှာမတွေ့ပါ။ USB ချန်နယ်ချိန်ကိရိယာကို တပ်ဆင်ထား၍ တီဗီချန်နယ်ထုတ်လွှင့်ရာ အရင်းအမြစ်တစ်ခုနှင့် ချိတ်ဆက်ထားကြောင်း အတည်ပြုပါ။\n\n အကယ်၍ ကောင်းကင်အန်တန်နာတိုင်ကို အသုံးပြုနေပါက ၎င်း၏ အနေအထား (သို့) ဦးတည်ဘက်ကို ချိန်ညှိပါ။ ရလဒ်များအကောင်းဆုံးဖြစ်စေရန် ၎င်းကို ပြတင်းပေါက်နားတွင် မြင့်မြင့်ထားပြီး ထပ်ရှာကြည့်ပါ။" + "မည်သည့် ချန်နယ်မျှ ရှာမတွေ့ပါ။ တီဗီလိုင်းဖမ်းကိရိယာကို ပါဝါဖွင့်ထားခြင်းရှိ မရှိနှင့် တီဗီစလောင်းသို့ ချိတ်ဆက်ထားခြင်းရှိမရှိ စစ်ဆေးပါ။\n\nအင်တင်နာကို အသုံးပြုလျှင် ၎င်း၏အနေအထား သို့မဟုတ် ဦးတည်ချက်တို့ကို ချိန်ညှိရန် လိုအပ်ပါသည်။ အကောင်းဆုံးရလဒ်အတွက် ပြတင်းပေါက်အနီး အမြင့်ပိုင်းတွင် ထားပါ။" "ထပ်ရှာရန်" "ပြီးသွားပြီ" @@ -83,5 +84,7 @@ "တီဗီချန်နယ်များကို ရှာပါ" "တီဗီချန်နယ်ချိန် ကိရိယာအက်ပ် ထည့်သွင်းမှု" "USB တီဗီချန်နယ်ချိန်အက်ပ် ထည့်သွင်းမှု" - "USB တီဗီချိန်စက်ကို ဖြုတ်လိုက်ပါပြီ" + "တီဗီလိုင်းဖမ်းကိရိယာ စနစ်ထည့်သွင်းမှု" + "USB တီဗီလိုင်းချိန်ကိရိယာကို ဖြုတ်လိုက်ပါပြီ။" + "ကွန်ရက်လိုင်းချိန်ကိရိယာကို ဖြုတ်လိုက်ပါပြီ။" diff --git a/usbtuner-res/values-nb/strings.xml b/usbtuner-res/values-nb/strings.xml index bce9e176..60bf7e1a 100644 --- a/usbtuner-res/values-nb/strings.xml +++ b/usbtuner-res/values-nb/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "TV-tuner" "USB-tuneren for TV" - "På" - "Av" + "Nettverkstuner for TV (betaversjon)" "Vent til behandlingen er fullført" - "Velg en kanalkilde" - "Ikke noe signal" - "Kunne ikke bytte kanal til %s" - "Kunne ikke bytte kanal" "Programvaren for tuneren er nylig blitt oppdatert. Du må skanne kanalene på nytt." "Slå på surroundlyd i innstillingene for systemlyd for å slå på lyd" + "Kan ikke spille av lyd. Prøv en annen TV" "Konfigurasjon av kanaler via tuneren" "Konfigurasjon av TV-tuneren" "Konfigurasjon av kanaler via USB-tuneren" + "Konfigurasjon av nettverkstuner" "Bekreft at TV-en din er koblet til en TV-signalkilde.\n\nHvis du bruker en trådløs antenne, kan det hende du må justere posisjonen eller retningen for å motta flest mulig kanaler. For å få de beste resultatene bør du plassere antennen høyt og i nærheten av et vindu." "Bekreft at USB-tuneren er plugget i og koblet til en TV-signalkilde.\n\nHvis du bruker en trådløs antenne, kan det hende du må justere posisjonen eller retningen for å motta flest mulig kanaler. For å få de beste resultatene bør du plassere antennen høyt og i nærheten av et vindu." + "Bekreft at nettverkstuneren er slått på og koblet til en TV-signalkilde.\n\nHvis du bruker en trådløs antenne, må du kanskje justere posisjonen eller retningen for å motta flere kanaler. For å få de beste resultatene bør du plassere antennen høyt og i nærheten av et vindu." "Fortsett" "Ikke nå" @@ -40,6 +38,7 @@ "Vil du kjøre kanalkonfigureringen på nytt?" "Dette fjerner kanalene som ble funnet med TV-tuneren, og skanner etter nye kanaler igjen.\n\nBekreft at TV-en din er koblet til en TV-signalkilde.\n\nHvis du bruker en trådløs antenne, kan det hende du må justere posisjonen eller retningen for å motta flest mulig kanaler. For å få de beste resultatene bør du plassere antennen høyt og i nærheten av et vindu." "Dette fjerner kanaler som er funnet via USB-tuneren, og skanner på nytt etter nye kanaler.\n\nBekreft at USB-tuneren er plugget i og koblet til en TV-signalkilde.\n\nHvis du bruker en trådløs antenne, kan det hende du må justere posisjonen eller retningen for å motta flest mulig kanaler. For å få de beste resultatene bør du plassere antennen høyt og i nærheten av et vindu." + "Dette fjerner kanalene som ble funnet av nettverkstuneren, og skanner på nytt etter nye kanaler.\n\nBekreft at nettverkstuneren er slått på og koblet til en TV-signalkilde.\n\nHvis du bruker en trådløs antenne, må du kanskje justere posisjonen eller retningen for å motta flere kanaler. For å få de beste resultatene bør du plassere antennen høyt og i nærheten av et vindu." "Fortsett" "Avbryt" @@ -54,6 +53,7 @@ "Konfigurasjon av TV-tuneren" "Konfigurasjon av kanaler via USB-tuneren" + "Konfigurasjon av tuner for nettverkskanaler" "Dette kan ta flere minutter" "Tuneren er midlertidig utilgjengelig eller brukes allerede av opptak." @@ -76,6 +76,7 @@ "Fant ingen kanaler" "Ingen kanaler ble funnet under skanningen. Bekreft at TV-en din er koblet til en TV-signalkilde.\n\nHvis du bruker en trådløs antenne, kan du justere posisjonen eller retningen. For å få de beste resultatene bør du plassere antennen høyt og i nærheten av et vindu. Deretter skanner du på nytt." "Fant ingen kanaler under skanningen. Bekreft at USB-tuneren er plugget i og koblet til en TV-signalkilde.\n\nHvis du bruker en trådløs antenne, kan du justere posisjonen eller retningen. For å få de beste resultatene bør du plassere antennen høyt og i nærheten av et vindu. Deretter skanner du på nytt." + "Skanningen fant ingen kanaler. Bekreft at nettverkstuneren er slått på og koblet til en TV-signalkilde.\n\nHvis du bruker en trådløs antenne, må du justere posisjonen eller retningen. For å få de beste resultatene bør du plassere antennen høyt og i nærheten av et vindu. Deretter skanner du på nytt." "Skann på nytt" "Ferdig" @@ -83,5 +84,7 @@ "Skann etter TV-kanaler" "Konfigurasjon av TV-tuneren" "Konfigurasjon av USB-tuner for TV" - "USB-tuneren for TV er frakoblet." + "Konfigurasjon av nettverkstuner for TV" + "USB-tuneren for TV er frakoblet." + "Nettverkstuneren er frakoblet." diff --git a/usbtuner-res/values-ne-rNP/strings.xml b/usbtuner-res/values-ne-rNP/strings.xml index 07d68e2c..387ebbf6 100644 --- a/usbtuner-res/values-ne-rNP/strings.xml +++ b/usbtuner-res/values-ne-rNP/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "TV ट्युनर" "USB TV ट्युनर" - "सक्रिय" - "निष्क्रिय" + "नेटवर्कको TV ट्युनर (बिटा)" "कृपया प्रक्रिया सम्पन्न हुने प्रतीक्षा गर्नुहोस्" - "आफ्नो च्यानलको स्रोत चयन गर्नुहोस्" - "कुनै सिग्‍नल छैन" - "%s मा ट्युन गर्न सकिएन" - "ट्युन गर्न सकिएन" "ट्युनरको सफ्टवेयरलाई हालसालै अद्यावधिक गरिएको छ। कृपया च्यानलहरू पुन:स्क्यान गर्नुहोस्।" "अडियोलाई सक्षम पार्न प्रणालीको ध्वनि सम्बन्धी सेटिङहरूमा गई सराउन्ड साउन्डलाई सक्षम पार्नुहोस्" + "अडियो बजाउन सकिँदैन। कृपया अर्को TV को प्रयोग गरी हेर्नुहोस्" "च्यानल ट्युनरको सेटअप" "TV ट्युनरको सेटअप" "USB च्यानल ट्युनरको सेटअप" + "नेटवर्क ट्युनरको सेटअप" "तपाईँको TV कुनै TV सिग्नलको स्रोतमा जडान गरिएको छ भनी पुष्टि गर्नुहोस्।\n\nयदि कुनै ओभर-दि-एयर एन्टेनाको प्रयोग भइरहेको छ भने धेरै च्यानलहरू प्राप्त गर्न तपाईँले त्यसको स्थान वा दिशा समायोजन गर्नुपर्ने हुन सक्छ। उत्कृष्ट परिणामहरूका लागि त्यसलाई उच्च स्थानमा र कुनै झ्यालको नजिक राख्नुहोस्।" "USB ट्युनर प्लगइन रहेको र कुनै TV सिग्नलको स्रोतमा जडान गरिएको छ भनी पुष्टि गर्नुहोस्।\n\nयदि कुनै ओभर-दि-एयर एन्टेनाको प्रयोग भइरहेको छ भने धेरै च्यानलहरू प्राप्त गर्न तपाईँले त्यसको स्थान वा दिशा समायोजन गर्नुपर्ने हुन सक्छ। उत्कृष्ट परिणामहरूका लागि त्यसलाई उच्च स्थानमा र कुनै झ्यालको नजिक राख्नुहोस्।" + "नेटवर्क ट्युनर सक्रिय रहेको र कुनै TV सिग्नलको स्रोतमा जडान गरिएको छ भनी पुष्टि गर्नुहोस्।\n\nयदि कुनै ओभर-दि-एयर एन्टेनाको प्रयोग भइरहेको छ भने धेरै च्यानलहरू प्राप्त गर्न तपाईंले त्यसको स्थान वा दिशा समायोजन गर्नुपर्ने हुन सक्छ। उत्कृष्ट परिणामहरूका लागि त्यसलाई उच्च स्थानमा र कुनै झ्यालको नजिक राख्नुहोस्।" "जारी राख्नुहोस्" "अहिले होइन" @@ -40,12 +38,13 @@ "च्यानलको सेटअप पुनःसञ्चालन गर्ने हो?" "यसले USB ट्युनरबाट भेट्टिएका च्यानलहरूलाई हटाउनेछ र नयाँ च्यानलहरू भेट्टाउन फेरि स्क्यान गर्नेछ।\n\nतपाईँको TV कुनै TV सिग्नलको स्रोतमा जडान गरिएको छ भनी पुष्टि गर्नुहोस्।\n\nयदि कुनै ओभर-दि-एयर एन्टेनाको प्रयोग भइरहेको छ भने धेरै च्यानलहरू प्राप्त गर्न तपाईँले त्यसको स्थान वा दिशा समायोजन गर्नुपर्ने हुन सक्छ। उत्कृष्ट परिणामहरूका लागि त्यसलाई उच्च स्थानमा र कुनै झ्यालको नजिक राख्नुहोस्।" "यसले USB ट्युनरबाट भेट्टिएका च्यानलहरूलाई हटाउनेछ र नयाँ च्यानलहरू भेट्टाउन फेरि स्क्यान गर्नेछ।\n\nUSB ट्युनर प्लगइन गरिएको र कुनै TV सिग्नलको स्रोतमा जडान गरिएको छ भनी पुष्टि गर्नुहोस्।\n\nयदि कुनै ओभर-दि-एयर एन्टेनाको प्रयोग भइरहेको छ भने धेरै च्यानलहरू प्राप्त गर्न तपाईँले त्यसको स्थान वा दिशा समायोजन गर्नुपर्ने हुन सक्छ। उत्कृष्ट परिणामहरूका लागि त्यसलाई उच्च स्थानमा र कुनै झ्यालको नजिक राख्नुहोस्।" + "यसले नेटवर्क ट्युनर मार्फत भेट्टिएका च्यानलहरूलाई हटाउनेछ र नयाँ च्यानलहरू भेट्टाउन फेरि स्क्यान गर्नेछ।\n\nनेटवर्क ट्युनरलाई सक्रिय गरिएको र कुनै TV सिग्नलको स्रोतमा जडान गरिएको छ भनी पुष्टि गर्नुहोस्।\n\nयदि कुनै ओभर-दि-एयर एन्टेनाको प्रयोग भइरहेको छ भने धेरै च्यानलहरू प्राप्त गर्न तपाईंले त्यसको स्थान वा दिशा समायोजन गर्नुपर्ने हुन सक्छ। उत्कृष्ट परिणामहरूका लागि त्यसलाई उच्च स्थानमा र कुनै झ्यालको नजिक राख्नुहोस्।" "जारी राख्नुहोस्" "रद्द गर्नुहोस्" "जडानको प्रकार चयन गर्नुहोस्" - "यदि उक्त ट्युनरमा कुनै बाह्य एन्टेना जडान गरिएको छ भने एन्टेना छनौट गर्नुहोस्। यदि तपाईँका च्यानलहरू केबल सेवा प्रदायक मार्फत आउँछन् भने केबल छनौट गर्नुहोस्। यदि तपाईँ निश्चित हुनुहुन्न भने दुवै प्रकारहरू स्क्यान गरिने छन् तर यसमा लामो समय लाग्न सक्छ।" + "यदि उक्त ट्युनरमा कुनै बाह्य एन्टेना जडान गरिएको छ भने एन्टेना छनौट गर्नुहोस्। यदि तपाईंका च्यानलहरू केबल सेवा प्रदायक मार्फत आउँछन् भने केबल छनौट गर्नुहोस्। यदि तपाईं निश्चित हुनुहुन्न भने दुवै प्रकारहरू स्क्यान गरिने छन् तर यसमा लामो समय लाग्न सक्छ।" "एन्टेना" "केबल" @@ -54,6 +53,7 @@ "TV ट्युनरको सेटअप" "USB च्यानल ट्युनरको सेटअप" + "नेटवर्कको च्यानल ट्युनरको सेटअप" "यसमा धेरै मिनेट लाग्न सक्छ" "ट्युनर अस्थायी रूपले अनुपलब्ध छ वा रेकर्डिङद्वारा पहिले नै प्रयोग गरिएको छ।" @@ -76,6 +76,7 @@ "कुनै च्यानल भेट्टिएन" "उक्त स्क्यानले कुनै पनि च्यानल भेट्टाएन। तपाईँको TV कुनै TV सिग्नलको स्रोतमा जडान गरिएको छ भनी पुष्टि गर्नुहोस्।\n\nयदि कुनै ओभर-दि-एयर एन्टेनाको प्रयोग भइरहेको छ भने त्यसको स्थान वा दिशा समायोजन गर्नुहोस्। उत्कृष्ट परिणामहरूका लागि त्यसलाई उच्च स्थानमा र कुनै झ्यालको नजिक राखी फेरि स्क्यान गर्नुहोस्।" "उक्त स्क्यानले कुनै पनि च्यानल भेट्टाएन। USB ट्युनर प्लगइन गरिएको र कुनै TV सिग्नलको स्रोतमा जडान गरिएको छ भनी पुष्टि गर्नुहोस्।\n\nयदि कुनै ओभर-दि-एयर एन्टेनाको प्रयोग भइरहेको छ भने त्यसको स्थान वा दिशा समायोजन गर्नुहोस्। उत्कृष्ट परिणामहरूका लागि त्यसलाई उच्च स्थानमा र कुनै झ्यालको नजिक राखी फेरि स्क्यान गर्नुहोस्।" + "उक्त स्क्यानले कुनै पनि च्यानल भेट्टाएन। नेटवर्क ट्युनरलाई सक्रिय गरिएको र कुनै TV सिग्नलको स्रोतमा जडान गरिएको छ भनी पुष्टि गर्नुहोस्।\n\nयदि कुनै ओभर-दि-एयर एन्टेनाको प्रयोग भइरहेको छ भने त्यसको स्थान वा दिशा समायोजन गर्नुहोस्। उत्कृष्ट परिणामहरूका लागि त्यसलाई उच्च स्थानमा र कुनै झ्यालको नजिक राखी फेरि स्क्यान गर्नुहोस्।" "फेरि स्क्यान गर्नुहोस्" "सम्पन्न भयो" @@ -83,5 +84,7 @@ "TV च्यानलहरू भेट्टाउन स्क्यान गर्नुहोस्" "TV ट्युनरको सेटअप" "USB TV ट्युनरको सेटअप" - "USB TV ट्युनरलाई विच्छेद गरियो।" + "नेटवर्कको TV ट्युनरको सेटअप" + "USB TV ट्युनरलाई विच्छेद गरियो।" + "नेटवर्क ट्युनरलाई विच्छेद गरियो।" diff --git a/usbtuner-res/values-nl/strings.xml b/usbtuner-res/values-nl/strings.xml index b9e18682..53852ce3 100644 --- a/usbtuner-res/values-nl/strings.xml +++ b/usbtuner-res/values-nl/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "Tv-tuner" "USB-tv-tuner" - "Aan" - "Uit" + "Netwerk-tv-tuner (BÈTA)" "Wacht tot het proces is voltooid" - "Selecteer je kanaalbron" - "Geen signaal" - "Kan niet afstemmen op %s" - "Kan niet afstemmen" "De software van de tuner is recent geüpdatet. Scan de kanalen opnieuw." "Schakel surrond sound in via de geluidsinstellingen van het systeem om audio in te schakelen" + "Kan audio niet afspelen. Probeer een andere tv." "Configuratie van kanaaltuner" "Tv-tuner instellen" "Kanaalconfiguratie van USB-tuner" + "Netwerktuner instellen" "Controleer of je tv is aangesloten op een tv-signaalbron.\n\nAls je een over-the-air-antenne gebruikt, moet je de positie of richting daarvan mogelijk aanpassen om zo veel mogelijk kanalen te ontvangen. Voor de beste resultaten plaats je de antenne op een hoge plek in de buurt van een raam." "Controleer of de USB-tuner is ingeschakeld en aangesloten op een tv-signaalbron.\n\nAls je een over-the-air-antenne gebruikt, moet je de positie of richting daarvan mogelijk aanpassen om zo veel mogelijk kanalen te ontvangen. Voor de beste resultaten plaats je de antenne op een hoge plek in de buurt van een raam." + "Controleer of de netwerktuner is ingeschakeld en aangesloten op een tv-signaalbron.\n\nAls je een over-the-air-antenne gebruikt, moet je de positie of richting daarvan mogelijk aanpassen om zo veel mogelijk kanalen te ontvangen. Voor de beste resultaten plaats je de antenne op een hoge plek in de buurt van een raam." "Doorgaan" "Niet nu" @@ -40,6 +38,7 @@ "Kanaalconfiguratie opnieuw uitvoeren?" "Hiermee worden de gevonden kanalen verwijderd van de tv-tuner en wordt opnieuw gezocht naar nieuwe kanalen.\n\nControleer of je tv is aangesloten op een tv-signaalbron.\n\nAls je een over-the-air-antenne gebruikt, moet je de positie of richting daarvan mogelijk aanpassen om zo veel mogelijk kanalen te ontvangen. Voor de beste resultaten plaats je de antenne op een hoge plek in de buurt van een raam." "Hiermee worden de gevonden kanalen verwijderd van de USB-tuner en wordt opnieuw gescand naar nieuwe kanalen.\n\nControleer of de USB-tuner is ingeschakeld en aangesloten op een tv-signaalbron.\n\nAls je een over-the-air-antenne gebruikt, moet je de positie of richting daarvan mogelijk aanpassen om zo veel mogelijk kanalen te ontvangen. Voor de beste resultaten plaats je de antenne op een hoge plek in de buurt van een raam." + "Hiermee worden de gevonden kanalen verwijderd van de netwerktuner en wordt opnieuw gezocht naar nieuwe kanalen.\n\nControleer of de netwerktuner is ingeschakeld en aangesloten op een tv-signaalbron.\n\nAls je een over-the-air-antenne gebruikt, moet je de positie of richting daarvan mogelijk aanpassen om zo veel mogelijk kanalen te ontvangen. Voor de beste resultaten plaats je de antenne op een hoge plek in de buurt van een raam." "Doorgaan" "Annuleren" @@ -54,6 +53,7 @@ "Tv-tuner instellen" "Kanaalconfiguratie van USB-tuner" + "Netwerkkanaaltuner instellen" "Dit kan enkele minuten duren" "De tuner is tijdelijk niet beschikbaar of wordt al gebruikt voor een opname." @@ -76,6 +76,7 @@ "Geen kanalen gevonden" "Er zijn geen kanalen gevonden tijdens de scan. Controleer of je tv is aangesloten op een tv-signaalbron.\n\nAls je een over-the-air-antenne gebruikt, pas je de positie of richting daarvan aan. Voor de beste resultaten plaats je de antenne op een hoge plek in de buurt van een raam en voer je de scan opnieuw uit." "Er zijn geen kanalen gevonden tijdens de scan. Controleer of de USB-tuner is ingeschakeld en aangesloten op een tv-signaalbron.\n\nAls je een over-the-air-antenne gebruikt, pas je de positie of richting daarvan aan. Voor de beste resultaten plaats je de antenne op een hoge plek in de buurt van een raam en voer je de scan opnieuw uit." + "De scan heeft geen kanalen gevonden. Controleer of de netwerktuner is ingeschakeld en aangesloten op een tv-signaalbron.\n\nAls je een over-the-air-antenne gebruikt, moet je de positie of richting daarvan mogelijk aanpassen. Voor de beste resultaten plaats je de antenne op een hoge plek in de buurt van een raam en voer je de scan opnieuw uit." "Opnieuw scannen" "Gereed" @@ -83,5 +84,7 @@ "Scannen naar tv-kanalen" "Tv-tuner instellen" "USB-tv-tuner instellen" - "USB-tv-tuner ontkoppeld." + "Netwerk-tv-tuner instellen" + "USB-tv-tuner losgekoppeld." + "Netwerktuner losgekoppeld." diff --git a/usbtuner-res/values-pl/strings.xml b/usbtuner-res/values-pl/strings.xml index 0bc7e734..33287ea9 100644 --- a/usbtuner-res/values-pl/strings.xml +++ b/usbtuner-res/values-pl/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "Tuner TV" "Tuner TV USB" - "Włącz" - "Wyłącz" + "Sieciowy tuner TV (BETA)" "Poczekaj na zakończenie przetwarzania" - "Wybierz źródło kanału" - "Brak sygnału" - "Nie udało się dostroić kanału %s" - "Nie udało się dostroić kanału" "Oprogramowanie tunera zostało niedawno zaktualizowane. Przeskanuj ponownie kanały." "Aby włączyć dźwięk, włącz dźwięk przestrzenny w ustawieniach systemowych dźwięku" + "Nie można odtworzyć dźwięku. Spróbuj użyć innego telewizora" "Konfiguracja kanałów w tunerze" "Konfiguracja tunera TV" "Konfiguracja kanałów w tunerze USB" + "Konfiguracja tunera sieciowego" "Upewnij się, że telewizor jest podłączony do źródła sygnału telewizyjnego.\n\nJeśli używasz anteny telewizyjnej, może być konieczne wyregulowanie jej położenia lub kierunku, by można było odbierać jak najwięcej kanałów. Aby uzyskać najlepszy sygnał, umieść antenę na podwyższeniu, w pobliżu okna." "Upewnij się, że tuner USB jest podłączony do telewizora oraz do źródła sygnału telewizyjnego.\n\nJeśli używasz anteny telewizyjnej, może być konieczne wyregulowanie jej położenia lub kierunku, by można było odbierać jak najwięcej kanałów. Aby uzyskać najlepszy sygnał, umieść antenę na podwyższeniu, w pobliżu okna." + "Upewnij się, że tuner sieciowy jest włączony i został podłączony do źródła sygnału telewizyjnego.\n\nJeśli używasz anteny telewizyjnej, może być konieczne wyregulowanie jej położenia lub kierunku, by można było odbierać jak najwięcej kanałów. Aby uzyskać najlepszy sygnał, umieść antenę na podwyższeniu, w pobliżu okna." "Kontynuuj" "Nie teraz" @@ -40,6 +38,7 @@ "Skonfigurować ponownie kanały?" "Spowoduje to usunięcie kanałów znalezionych przez tuner TV i ponowne wykonanie skanowania.\n\nUpewnij się, że telewizor jest podłączony do źródła sygnału telewizyjnego.\n\nJeśli używasz anteny telewizyjnej, może być konieczne wyregulowanie jej położenia lub kierunku, by można było odbierać jak najwięcej kanałów. Aby uzyskać najlepszy sygnał, umieść antenę na podwyższeniu, w pobliżu okna." "Spowoduje to usunięcie kanałów znalezionych przez tuner USB i ponowne wykonanie skanowania.\n\nUpewnij się, że tuner USB jest podłączony do telewizora oraz do źródła sygnału telewizyjnego.\n\nJeśli używasz anteny telewizyjnej, może być konieczne wyregulowanie jej położenia lub kierunku, by można było odbierać jak najwięcej kanałów. Aby uzyskać najlepszy sygnał, umieść antenę na podwyższeniu, w pobliżu okna." + "Spowoduje to usunięcie kanałów znalezionych przez tuner sieciowy i ponowne wykonanie skanowania.\n\nUpewnij się, że tuner sieciowy jest włączony i został podłączony do źródła sygnału telewizyjnego.\n\nJeśli używasz anteny telewizyjnej, może być konieczne wyregulowanie jej położenia lub kierunku, by można było odbierać jak najwięcej kanałów. Aby uzyskać najlepszy sygnał, umieść antenę na podwyższeniu, w pobliżu okna." "Kontynuuj" "Anuluj" @@ -54,6 +53,7 @@ "Konfiguracja tunera TV" "Konfiguracja kanałów w tunerze USB" + "Konfiguracja kanałów w tunerze sieciowym" "Może to potrwać kilka minut" "Tuner jest czasowo niedostępny lub właśnie nagrywa." @@ -82,6 +82,7 @@ "Nie znaleziono kanałów" "Podczas skanowania nie znaleziono żadnych kanałów. Upewnij się, że telewizor jest podłączony do źródła sygnału telewizyjnego.\n\nJeśli używasz anteny telewizyjnej, wyreguluj jej położenie lub kierunek. Aby uzyskać najlepszy sygnał, umieść antenę na podwyższeniu, w pobliżu okna, a następnie ponownie wykonaj skanowanie." "Podczas skanowania nie znaleziono żadnych kanałów. Upewnij się, że tuner USB jest podłączony do telewizora oraz do źródła sygnału telewizyjnego.\n\nJeśli używasz anteny telewizyjnej, wyreguluj jej położenie lub kierunek. Aby uzyskać najlepszy sygnał, umieść antenę na podwyższeniu, w pobliżu okna, a następnie ponownie wykonaj skanowanie." + "Podczas skanowania nie znaleziono żadnych kanałów. Upewnij się, że tuner sieciowy jest włączony i został podłączony do źródła sygnału telewizyjnego.\n\nJeśli używasz anteny telewizyjnej, wyreguluj jej położenie lub kierunek. Aby uzyskać najlepszy sygnał, umieść antenę na podwyższeniu, w pobliżu okna, a następnie ponownie wykonaj skanowanie." "Skanuj ponownie" "Gotowe" @@ -89,5 +90,7 @@ "Wyszukaj kanały TV" "Konfiguracja tunera TV" "Konfiguracja tunera TV USB" - "Tuner TV USB odłączony." + "Konfiguracja sieciowego tunera TV" + "Telewizyjny tuner USB rozłączony." + "Tuner sieciowy rozłączony." diff --git a/usbtuner-res/values-pt-rPT/strings.xml b/usbtuner-res/values-pt-rPT/strings.xml index c78c92c1..b7586f13 100644 --- a/usbtuner-res/values-pt-rPT/strings.xml +++ b/usbtuner-res/values-pt-rPT/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "Sintonizador de TV" "Sintonizador de TV USB" - "Ativar" - "Desativar" + "Sintonizador de televisão (BETA)" "Aguarde enquanto o processamento é terminado" - "Selecione a origem do canal" - "Sem sinal" - "Falha ao sintonizar %s" - "Falha ao sintonizar" "O software do sintonizador foi atualizado recentemente. Procure novamente os canais." "Ative o som surround nas definições de som do sistema para ativar o áudio" + "Não é possível reproduzir áudio. Experimente outra TV." "Configuração do sintonizador de canais" "Configuração do sintonizador de TV" "Configuração do sintonizador de canais USB" + "Configuração do sintonizador de rede" "Confirme se a sua TV está ligada a uma fonte de sinal da TV.\n\nSe estiver a utilizar uma antena via rede sem fios, pode ter de ajustar a respetiva posição ou a direção para receber a maioria dos canais. Para obter os melhores resultados, coloque-a num ponto alto e junto a uma janela." "Confirme se o sintonizador USB está ativado e ligado a uma fonte de sinal da TV.\n\nSe estiver a utilizar uma antena via rede sem fios, pode ter de ajustar a respetiva posição ou a direção para receber a maioria dos canais. Para obter os melhores resultados, coloque-a num ponto alto e junto a uma janela." + "Confirme se o sintonizador de rede está ligado à alimentação e ligado a uma fonte de sinal de TV.\n\nSe estiver a utilizar uma antena via rede sem fios, pode ter de ajustar a respetiva posição ou direção para receber a maioria dos canais. Para obter os melhores resultados, coloque-a num ponto alto e junto a uma janela." "Continuar" "Agora não" @@ -40,6 +38,7 @@ "Pretende executar novamente a configuração do canal?" "Esta ação remove os canais encontrados pelo sintonizador de TV e procura novamente canais novos.\n\nConfirme se a sua TV está ligada a uma fonte de sinal da TV.\n\nSe estiver a utilizar uma antena via rede sem fios, pode ter de ajustar a respetiva posição ou a direção para receber a maioria dos canais. Para obter os melhores resultados, coloque-a num ponto alto e junto a uma janela." "Esta ação remove os canais encontrados do sintonizador USB e procura novamente canais novos.\n\nConfirme se o sintonizador USB está ativado e ligado a uma fonte de sinal da TV.\n\nSe estiver a utilizar uma antena via rede sem fios, pode ter de ajustar a respetiva posição ou a direção para receber a maioria dos canais. Para obter os melhores resultados, coloque-a num ponto alto e junto a uma janela." + "Esta ação vai remover os canais encontrados do sintonizador de rede e procurar novos canais novamente.\n\nConfirme se o sintonizador de rede está ligado à alimentação e ligado a uma fonte de sinal de TV.\n\nSe estiver a utilizar uma antena via rede sem fios, pode ter de ajustar a respetiva posição ou direção para receber a maioria dos canais. Para obter os melhores resultados, coloque-a num ponto alto e junto a uma janela." "Continuar" "Cancelar" @@ -54,6 +53,7 @@ "Configuração do sintonizador de TV" "Configuração do sintonizador de canais USB" + "Configuração do sintonizador de canais" "Esta operação pode demorar vários minutos" "O sintonizador está temporariamente indisponível ou já está a ser utilizado pela gravação." @@ -76,6 +76,7 @@ "Não foram encontrados canais" "A procura não encontrou quaisquer canais. Confirme se a TV está ligada a uma fonte de sinal da TV.\n\nSe estiver a utilizar uma antena via rede sem fios, ajuste a respetiva posição ou a direção. Para obter os melhores resultados, coloque-a num ponto alto e junto a uma janela e procure novamente." "Não foram encontrados quaisquer canais. Verifique se o sintonizador USB está ativado e ligado a uma fonte de sinal da TV.\n\nSe estiver a utilizar uma antena via rede sem fios, ajuste a respetiva posição ou a direção. Para obter os melhores resultados, coloque-a num ponto alto e junto a uma janela e procure novamente." + "A procura não encontrou quaisquer canais. Confirme se o sintonizador de rede está ligado à alimentação e ligado a uma fonte de sinal de TV.\n\nSe estiver a utilizar uma antena via rede sem fios, ajuste a respetiva posição ou direção. Para obter os melhores resultados, coloque-a num ponto alto e junto a uma janela. Em seguida, procure novamente." "Procurar novamente" "Concluído" @@ -83,5 +84,7 @@ "Procurar canais de TV" "Configuração do sintonizador de TV" "Configuração do sintonizador de TV USB" - "Sintonizador de TV USB desligado." + "Configuração do sintonizador de televisão" + "Sintonizador de TV USB desligado." + "Sintonizador de rede desligado." diff --git a/usbtuner-res/values-pt/strings.xml b/usbtuner-res/values-pt/strings.xml index 3dfd30c0..862b1dbe 100644 --- a/usbtuner-res/values-pt/strings.xml +++ b/usbtuner-res/values-pt/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "Sintonizador de TV" "Sintonizador de TV USB" - "Ativar" - "Desativar" + "Sintonizador de rede de TV (BETA)" "Aguarde a conclusão do processamento" - "Selecione a fonte do canal" - "Sem sinal" - "Falha ao sintonizar %s" - "Falha ao sintonizar" "O software do sintonizador foi atualizado recentemente. Procure os canais mais uma vez." "Ative o som surround nas configurações de som do sistema para ativar o áudio" + "Não foi possível reproduzir o áudio. Tente em outra TV" "Configuração do sintonizador de canais" "Configuração do Sintonizador de TV" "Configuração do sintonizador de canais USB" + "Configuração do sintonizador de rede" "Verifique se sua TV está conecta a uma fonte de sinal de TV.\n\nSe você estiver usando uma antena Over-the-air (OTA), talvez seja necessário ajustar o posicionamento ou a direção dela para receber o máximo de canais. Para ter resultados melhores, coloque-a em um lugar alto e perto de uma janela." "Verifique se o sintonizador USB está conectado a uma fonte de sinal de TV.\n\nSe você usa uma antena Over the air, talvez seja necessário ajustar o posicionamento ou a direção dela para receber o maior número de canais. Para ter resultados melhores, coloque-a em um lugar alto e perto de uma janela." + "Verifique se o sintonizador de rede está ligado e conectado a uma fonte de sinal de TV.\n\nSe você estiver usando uma antena OTA (over-the-air), talvez seja necessário ajustar o posicionamento ou a direção dela para receber o máximo de canais. Para ter resultados melhores, coloque-a em um lugar alto e perto de uma janela." "Continuar" "Agora não" @@ -40,6 +38,7 @@ "Executar novamente a configuração de canais?" "Isso removerá os canais encontrados do Sintonizador de TV e procurará novos canais mais uma vez.\n\nVerifique se sua TV está conectada a uma fonte de sinal de TV.\n\nSe você estiver usando uma antena Over-the-air (OTA), talvez seja necessário ajustar o posicionamento ou a direção dela para receber o máximo de canais. Para ter resultados melhores, coloque-a em um lugar alto e perto de uma janela." "Essa ação remove os canais encontrados pelo sintonizador USB e procura canais novamente.\n\nVerifique se o sintonizador USB está conectado a uma fonte de sinal de TV.\n\n.Se você usa uma antena Over the air, talvez seja necessário ajustar o posicionamento ou a direção dela para receber o maior número de canais. Para ter resultados melhores, coloque-a em um lugar alto e perto de uma janela." + "Essa ação removerá os canais encontrados do sintonizador de rede e procurará novos canais mais uma vez.\n\nVerifique se o sintonizador de rede está ligado e conectado a uma fonte de sinal de TV.\n\nSe você estiver usando uma antena OTA (over-the-air), talvez seja necessário ajustar o posicionamento ou a direção dela para receber o máximo de canais. Para ter resultados melhores, coloque-a em um lugar alto e perto de uma janela." "Continuar" "Cancelar" @@ -54,6 +53,7 @@ "Configuração do Sintonizador de TV" "Configuração do sintonizador de canais USB" + "Configuração do sintonizador de canais de rede" "Isso pode demorar alguns minutos" "O sintonizador está temporariamente indisponível ou já está sendo usado pela gravação." @@ -76,6 +76,7 @@ "Nenhum canal encontrado" "A procura não encontrou nenhum canal. Verifique se sua TV está conectada a uma fonte de sinal de TV.\n\nSe você estiver usando uma antena Over-the-air (OTA), ajuste o posicionamento ou a direção dela. Para ter resultados melhores, coloque-a em um lugar alto e perto de uma janela e procure novamente." "Nenhum canal foi encontrado pela procura. Verifique se o sintonizador USB está conectado a uma fonte de sinal de TV.\n\nSe você estiver usando uma antena Over the air, ajuste o posicionamento ou a direção dela. Para ter resultados melhores, coloque-a em um lugar alto e perto de uma janela e procure novamente." + "Nenhum canal foi encontrado pela procura. Verifique se o sintonizador de rede está ligado e conectado a uma fonte de sinal de TV.\n\nSe você estiver usando uma antena OTA (over-the-air), ajuste o posicionamento ou a direção dela. Para ter resultados melhores, coloque-a em um lugar alto e perto de uma janela, depois procure novamente." "Procurar novamente" "Concluído" @@ -83,5 +84,7 @@ "Procurar canais de TV" "Configuração do Sintonizador de TV" "Configuração do Sintonizador de TV USB" - "Sintonizador de TV USB desconectado." + "Configuração do sintonizador de rede de TV" + "Sintonizador de TV USB desconectado." + "Sintonizador de rede desconectado." diff --git a/usbtuner-res/values-ro/strings.xml b/usbtuner-res/values-ro/strings.xml index 57f1ca48..f87c5494 100644 --- a/usbtuner-res/values-ro/strings.xml +++ b/usbtuner-res/values-ro/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "Tuner TV" "Tuner TV USB" - "Activați" - "Dezactivați" + "Tuner de rețea TV (BETA)" "Așteptați ca procesarea să fie finalizată" - "Selectați sursa canalului" - "Fără semnal" - "Canalul %s nu a putut fi selectat" - "Canalul nu a putut fi selectat" "Software-ul tunerului a fost actualizat recent. Scanați din nou canalele." "Pentru a activa conținutul audio, activați sunetul surround din setările de sunet ale sistemului" + "Conținutul audio nu poate fi redat. Încercați pe un alt televizor." "Configurarea tunerului de canale" "Configurarea tunerului TV" "Configurarea tunerului de canale USB" + "Configurarea tunerului de rețea" "Asigurați-vă că televizorul este conectat la o sursă de semnal TV.\n\nDacă folosiți o antenă over the air, poate fi necesar să-i ajustați amplasarea sau direcția astfel încât să capteze majoritatea canalelor. Pentru cele mai bune rezultate, amplasați-o la înălțime și în apropierea unei ferestre." "Asigurați-vă că tunerul USB este conectat la o sursă de alimentare și la o sursă de semnal TV.\n\nDacă folosiți o antenă over the air, poate fi necesar să-i ajustați amplasarea sau direcția astfel încât să capteze majoritatea canalelor. Pentru cele mai bune rezultate, amplasați-o la înălțime și în apropierea unei ferestre." + "Verificați dacă tunerul de rețea este pornit și conectat la o sursă de semnal TV.\n\nDacă folosiți o antenă over-the-air, ar putea fi necesar să-i ajustați poziția sau direcția pentru a recepționa cât mai multe canale. Pentru rezultate optime, plasați-o sus și lângă o fereastră." "Continuați" "Nu acum" @@ -40,6 +38,7 @@ "Configurați din nou canalul?" "Astfel, vor fi eliminate canalele găsite de tunerul TV și se vor căuta din nou canale.\n\nAsigurați-vă că televizorul este conectat la o sursă de semnal TV.\n\nDacă folosiți o antenă over the air, poate fi necesar să-i ajustați amplasarea sau direcția astfel încât să capteze majoritatea canalelor. Pentru cele mai bune rezultate, amplasați-o la înălțime și în apropierea unei ferestre." "Astfel, vor fi eliminate canalele găsite de pe tunerul USB și se vor căuta din nou canale.\n\nAsigurați-vă că tunerul USB este conectat la o sursă de alimentare și la o sursă de semnal TV.\n\nDacă folosiți o antenă over the air, poate fi necesar să-i ajustați amplasarea sau direcția pentru a capta majoritatea canalelor. Pentru cele mai bune rezultate, amplasați-o la înălțime și în apropierea unei ferestre." + "Astfel veți elimina canalele găsite din tunerul de rețea și veți scana iar pentru a găsi canale noi.\n\nVerificați dacă tunerul de rețea este pornit și conectat la o sursă de semnal TV.\n\nDacă folosiți o antenă over-the-air, ar putea fi necesar să-i ajustați poziția sau direcția pentru a recepționa cât mai multe canale. Pentru rezultate optime, plasați-o sus și lângă o fereastră." "Continuați" "Anulați" @@ -54,6 +53,7 @@ "Configurarea tunerului TV" "Configurarea tunerului de canale USB" + "Configurarea tunerului de canale de rețea" "Poate dura câteva minute" "Tunerul nu este disponibil temporar sau este folosit deja de înregistrare." @@ -79,6 +79,7 @@ "Nu s-au găsit canale" "Scanarea nu a găsit niciun canal. Asigurați-vă că televizorul este conectat la o sursă de semnal TV.\n\nDacă folosiți o antenă over the air, ajustați-i amplasarea sau direcția. Pentru cele mai bune rezultate, amplasați-o la înălțime și în apropierea unei ferestre, apoi repetați scanarea." "Nu s-a găsit niciun canal la scanare. Asigurați-vă că tunerul USB este conectat la o sursă de alimentare și la o sursă de semnal TV. \n\nDacă folosiți o antenă over the air, ajustați-i amplasarea sau direcția. Pentru cele mai bune rezultate, amplasați-o la înălțime și în apropierea unei ferestre, apoi repetați scanarea." + "Scanarea nu a găsit niciun canal. Verificați dacă tunerul de rețea este pornit și conectat la o sursă de semnal TV.\n\nDacă folosiți o antenă over-the-air, ajustați poziția sau direcția. Pentru rezultate optime, plasați-o sus și lângă o fereastră și scanați din nou." "Scanați din nou" "Terminat" @@ -86,5 +87,7 @@ "Căutați canale TV" "Configurarea tunerului TV" "Configurarea tunerului TV USB" - "Tunerul TV USB a fost deconectat." + "Configurarea tunerului de rețea TV" + "Tunerul TV USB este deconectat." + "Tunerul de rețea este deconectat." diff --git a/usbtuner-res/values-ru/strings.xml b/usbtuner-res/values-ru/strings.xml index 7509ff19..4ddf0c58 100644 --- a/usbtuner-res/values-ru/strings.xml +++ b/usbtuner-res/values-ru/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "ТВ-тюнер" "USB-тюнер" - "Вкл." - "Выкл." + "Сетевой ТВ-тюнер (бета)" "Дождитесь окончания обработки" - "Выберите источник каналов" - "Нет сигнала" - "Не удалось настроиться на канал \"%s\"" - "Не удалось настроиться на канал" "Программное обеспечение тюнера было обновлено. Повторите сканирование." "Включите объемный звук в системных настройках" + "Не удается воспроизвести аудио. Выберите другой канал." "Настройка тюнера" "Настройка ТВ-тюнера" "Настройка USB-тюнера" + "Настройка сетевого тюнера" "Убедитесь, что телевизор подключен к источнику сигнала.\n\nЕсли вы используете телеантенну, переместите или перенаправьте ее. Рекомендуем расположить ее высоко и рядом с окном." "Убедитесь, что USB-тюнер включен в сеть и подсоединен к источнику сигнала.\n\nЕсли вы используете телеантенну, переместите или перенаправьте ее. Рекомендуем расположить ее высоко и рядом с окном." + "Убедитесь, что сетевой тюнер включен в сеть и подсоединен к источнику ТВ-сигнала.\n\nЕсли вы используете телеантенну, поместите и направьте ее так, чтобы она принимала большинство каналов. Рекомендуем расположить антенну высоко и рядом с окном." "Продолжить" "Не сейчас" @@ -40,6 +38,7 @@ "Настроить каналы заново?" "Все каналы, найденные с помощью ТВ-тюнера, будут удалены, и поиск начнется заново.\n\nУбедитесь, что телевизор подключен к источнику сигнала.\n\nЕсли вы используете телеантенну, переместите или перенаправьте ее. Рекомендуем расположить ее высоко и рядом с окном." "Все каналы, найденные с помощью USB-тюнера, будут удалены, и сканирование начнется заново.\n\nУбедитесь, что USB-тюнер включен в сеть и подсоединен к источнику сигнала.\n\nЕсли вы используете телеантенну, переместите или перенаправьте ее. Рекомендуем расположить ее высоко и рядом с окном." + "Все каналы, найденные с помощью сетевого тюнера, будут удалены, и сканирование начнется заново.\n\nУбедитесь, что сетевой тюнер включен в сеть и подсоединен к источнику ТВ-сигнала.\n\nЕсли вы используете телеантенну, поместите и направьте ее так, чтобы она принимала большинство каналов. Рекомендуем расположить антенну высоко и рядом с окном." "Продолжить" "Отмена" @@ -54,6 +53,7 @@ "Настройка ТВ-тюнера" "Настройка USB-тюнера" + "Настройка каналов сетевого тюнера" "Это может занять несколько минут" "Тюнер временно недоступен или уже используется для записи." @@ -78,6 +78,7 @@ "Каналы не найдены" "Каналы не найдены. Убедитесь, что телевизор подключен к источнику сигнала.\n\nЕсли вы используете телеантенну, переместите или перенаправьте ее. Рекомендуем расположить ее высоко и рядом с окном." "Каналы не найдены. Убедитесь, что USB-тюнер включен в сеть и подсоединен к источнику сигнала.\n\nЕсли вы используете телеантенну, переместите или перенаправьте ее. Рекомендуем расположить ее высоко и рядом с окном." + "Каналы не найдены. Убедитесь, что сетевой тюнер включен в сеть и подсоединен к источнику ТВ-сигнала.\n\nЕсли вы используете телеантенну, переместите или перенаправьте ее (рекомендуем расположить антенну высоко и рядом с окном). Затем повторите сканирование." "Искать повторно" "Готово" @@ -85,5 +86,7 @@ "Сканирование телеканалов" "Настройка ТВ-тюнера" "Настройка USB-тюнера" - "USB-тюнер отключен." + "Настройка сетевого ТВ-тюнера" + "USB-тюнер отключен." + "Сетевой тюнер отключен." diff --git a/usbtuner-res/values-si-rLK/strings.xml b/usbtuner-res/values-si-rLK/strings.xml index a792b2c0..5f195311 100644 --- a/usbtuner-res/values-si-rLK/strings.xml +++ b/usbtuner-res/values-si-rLK/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "TV සුසරකය" "USB TV සුසරකය" - "ක්‍රියාත්මක කරන්න" - "ක්‍රියාවිරහිත කරන්න" + "Network TV Tuner (BETA)" "සැකසීම අවසන් කිරීමට කරුණාකර රැඳී සිටින්න" - "ඔබගේ නාලිකා මූලාශ්‍රය තෝරන්න" - "සංඥාවක් නැත" - "%s වෙත සුසර කිරීම අසාර්ථක විය" - "සුසර කිරීම අසාර්ථක විය" "සුසරක මෘදුකාංගය පසුගියදා යාවත්කාලීන කර ඇත. කරුණාකර නාලිකා නැවත ස්කෑන් කරන්න." "ශ්‍රව්‍ය සබල කිරීමට හඬ සැකසීම් තුළ අවට හඬ සබල කරන්න" + "ශ්‍රව්‍යය ධාවනය කළ නොහැකිය. කරුණාකර වෙනත් TV එකක් උත්සාහ කරන්න" "නාලිකා සුසරක පිහිටුවීම" "TV සුසරක පිහිටුවීම" "USB නාලිකා සුසරක පිහිටුවීම" + "ජාල සුසරකය පිහිටුවීම" "ඔබේ TV, TV සංඥා මූලාශ්‍රයකට සම්බන්ධ කර ඇති බව තහවුරු කර ගන්න.\n\nගුවන-ඔස්සේ ඇන්ටනාවක් භාවිත කරන්නේ නම්, ඔබට බොහොමයක් නාලිකා ලබා ගැනීමට පිහිටීම හෝ දිශාව සීරුමාරු කිරීමට අවශ්‍ය විය හැකිය. හොඳම ප්‍රතිඵල සඳහා, එය ඉහළින් සහ කවුළුවක් ආසන්නයේ තබන්න." "සුසරකය පේනුගත කර ඇති බව සහ TV සංඥා මූලාශ්‍රයකට සම්බන්ධ කර ඇති බව තහවුරු කර ගන්න.\n\nගුවන-ඔස්සේ ඇන්ටනාවක් භාවිත කරන්නේ නම්, ඔබට බොහොමයක් නාලිකා ලබා ගැනීමට පිහිටීම හෝ දිශාව සීරුමාරු කිරීමට අවශ්‍ය විය හැකිය. හොඳම ප්‍රතිඵල සඳහා, එය ඉහළින් සහ කවුළුවක් ආසන්නයේ තබන්න." + "ජාල සුසරකය බලයට සම්බන්ධ කර ඇති බව සහ TV සංඥා මූලාශ්‍රයකට සම්බන්ධ කර ඇති බව තහවුරු කර ගන්න.\n\nගුවන-ඔස්සේ ඇන්ටනාවක් භාවිත කරන්නේ නම්, ඔබට බොහොමයක් නාලිකා ලබා ගැනීමට පිහිටීම හෝ දිශාව සීරුමාරු කිරීමට අවශ්‍ය විය හැකිය. හොඳම ප්‍රතිඵල සඳහා, එය ඉහළින් සහ කවුළුවක් ආසන්නයේ තබන්න." "දිගටම කර ගෙන යන්න" "දැන් නොවේ" @@ -40,6 +38,7 @@ "නාලිකා පිහිටුවීම නැවත ධාවනය කරන්නද?" "මෙය TV සුසරකය වෙතින් සොයා ගත් නාලිකා ඉවත් කරනු ඇති අතර නව නාලිකා සඳහා නැවත ස්කෑන් කරනු ඇත.\n\nඔබේ TV, TV සංඥා මූලාශ්‍රයකට සම්බන්ධ කර ඇති බව තහවුරු කර ගන්න.\n\nගුවන-ඔස්සේ ඇන්ටනාවක් භාවිත කරන්නේ නම්, ඔබට බොහොමයක් නාලිකා ලබා ගැනීමට පිහිටීම හෝ දිශාව සීරුමාරු කිරීමට අවශ්‍ය විය හැකිය. හොඳම ප්‍රතිඵල සඳහා, එය ඉහළින් සහ කවුළුවක් ආසන්නයේ තබන්න." "මෙය USB සුසරකය වෙතින් සොයා ගත් නාලිකා ඉවත් කරනු ඇති අතර නව නාලිකා සඳහා නැවත ස්කෑන් කරනු ඇත.\n\nUSB සුසරකය පේනුගත කර ඇති බව සහ TV සංඥා මූලාශ්‍රයකට සම්බන්ධ කර ඇති බව තහවුරු කර ගන්න.\n\nගුවන-ඔස්සේ ඇන්ටනාවක් භාවිත කරන්නේ නම්, ඔබට බොහොමයක් නාලිකා ලබා ගැනීමට පිහිටීම හෝ දිශාව සීරුමාරු කිරීමට අවශ්‍ය විය හැකිය. හොඳම ප්‍රතිඵල සඳහා, එය ඉහළින් සහ කවුළුවක් ආසන්නයේ තබන්න." + "මෙය ජාල සුසරකය වෙතින් සොයා ගත් නාලිකා ඉවත් කරනු ඇති අතර නව නාලිකා සඳහා නැවත ස්කෑන් කරනු ඇත.\n\nජාල සුසරකය බලයට සම්බන්ධ කර ඇති බව සහ TV සංඥා මූලාශ්‍රයකට සම්බන්ධ කර ඇති බව තහවුරු කර ගන්න.\n\nගුවන-ඔස්සේ ඇන්ටනාවක් භාවිත කරන්නේ නම්, ඔබට බොහොමයක් නාලිකා ලබා ගැනීමට පිහිටීම හෝ දිශාව සීරුමාරු කිරීමට අවශ්‍ය විය හැකිය. හොඳම ප්‍රතිඵල සඳහා, එය ඉහළින් සහ කවුළුවක් ආසන්නයේ තබන්න." "දිගටම කර ගෙන යන්න" "අවලංගු කරන්න" @@ -54,6 +53,7 @@ "TV සුසරක පිහිටුවීම" "USB නාලිකා සුසරක පිහිටුවීම" + "ජාල නාලිකා සුසරකය පිහිටුවීම" "මෙය මිනිත්තු කිහිපයක් ගත හැකිය" "සුසරකය තාවකාලිකව ලබා ගත නොහැකිය නැතහොත් දැනටමත් පටිගත කිරීම මගින් භාවිත කරනු ලැබේ." @@ -76,6 +76,7 @@ "නාලිකා හමු නොවීය" "ස්කෑන් කිරීමට නාලිකා කිසිවක් හමු නොවීය. ඔබේ TV, TV සංඥා මූලාශ්‍රයකට සම්බන්ධ කර ඇති බව තහවුරු කර ගන්න.\n\nගුවන-ඔස්සේ ඇන්ටනාවක් භාවිත කරන්නේ නම්, එහි පිහිටීම හෝ දිශාව සීරුමාරු කරන්න. හොඳම ප්‍රතිඵල සඳහා, එය ඉහළින් සහ කවුළුවක් ආසන්නයේ තබා නැවත ස්කෑන් කරන්න." "ස්කෑන් කිරීමට නාලිකා කිසිවක් හමු නොවීය. USB සුසරකය පේනුගත කර ඇති බව සහ TV සංඥා මූලාශ්‍රයකට සම්බන්ධ කර ඇති බව තහවුරු කර ගන්න.\n\nගුවන-ඔස්සේ ඇන්ටනාවක් භාවිත කරන්නේ නම්, පිහිටීම හෝ දිශාව සීරුමාරු කරන්න. හොඳම ප්‍රතිඵල සඳහා, එය ඉහළින් සහ කවුළුවක් ආසන්නයේ තබා නැවත ස්කෑන් කරන්න." + "ස්කෑන් කිරීමට නාලිකා කිසිවක් හමු නොවීය. ජාල සුසරකය බලයට සම්බන්ධ කර ඇති බව සහ TV සංඥා මූලාශ්‍රයකට සම්බන්ධ කර ඇති බව තහවුරු කර ගන්න.\n\nගුවන-ඔස්සේ ඇන්ටනාවක් භාවිත කරන්නේ නම්, පිහිටීම හෝ දිශාව සීරුමාරු කරන්න. හොඳම ප්‍රතිඵල සඳහා, එය ඉහළින් සහ කවුළුවක් ආසන්නයේ තබා නැවත ස්කෑන් කරන්න." "නැවත ස්කෑන් කරන්න" "නිමයි" @@ -83,5 +84,7 @@ "TV නාලිකා සඳහා ස්කෑන් කරන්න" "TV සුසරක පිහිටුවීම" "USB TV සුසරක පිහිටුවීම" - "USB TV සුසරකය විසන්ධි කරන ලදී." + "ජාල TV සුසරකය පිහිටුවීම" + "USB TV සුසරකය විසන්ධි වුණි." + "ජාල සුසරකය විසන්ධි වුණි." diff --git a/usbtuner-res/values-sk/strings.xml b/usbtuner-res/values-sk/strings.xml index 67f9829e..00b9ba6b 100644 --- a/usbtuner-res/values-sk/strings.xml +++ b/usbtuner-res/values-sk/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "Televízny tuner" "Televízny tuner s rozhraním USB" - "Zapnúť" - "Vypnúť" + "Televízny sieťový tuner (BETA)" "Počkajte, kým bude spracovanie dokončené" - "Vyberte zdroj kanála" - "Žiadny signál" - "Nepodarilo sa naladiť kanál %s" - "Ladenie zlyhalo" "Softvér tunera bol nedávno aktualizovaný. Znova vyhľadajte kanály." "Ak chcete zapnúť zvuk, v nastaveniach systémového zvuku povoľte priestorový zvuk" + "Zvuk nie je možné prehrať. Skúste použiť iný televízor." "Nastavenie tunera kanálov" "Nastavenie televízneho tunera" "Nastavenie kanálov tunera USB" + "Nastavenie sieťového tunera" "Skontrolujte, či je televízor pripojený k zdroju televízneho signálu.\n\nAk používate vzdušnú anténu, upravte jej umiestnenie alebo orientáciu, aby ste získali čo najviac kanálov. Najlepšie výsledky dosiahnete umiestnením antény dostatočne vysoko a do blízkosti okna." "Skontrolujte, či je tuner USB zapojený a pripojený k zdroju televízneho signálu.\n\nAk používate vzdušnú anténu, upravte jej umiestnenie alebo orientáciu, aby ste získali čo najviac kanálov. Najlepšie výsledky dosiahnete umiestnením antény dostatočne vysoko a do blízkosti okna." + "Skontrolujte, či je sieťový tuner zapnutý a pripojený k zdroju televízneho signálu.\n\nAk používate bezdrôtovú anténu, upravte jej umiestnenie alebo orientáciu tak, aby ste získali čo najviac kanálov. Najlepšie výsledky dosiahnete umiestnením antény dostatočne vysoko a do blízkosti okna." "Pokračovať" "Teraz nie" @@ -40,6 +38,7 @@ "Znova spustiť nastavenie kanálov?" "Táto akcia odstráni nájdené kanály z televízneho tunera a opätovne spustí vyhľadávanie nových kanálov.\n\nSkontrolujte, či je televízor pripojený k zdroju televízneho signálu.\n\nAk používate vzdušnú anténu, upravte jej umiestnenie alebo orientáciu, aby ste získali čo najviac kanálov. Najlepšie výsledky dosiahnete umiestnením antény dostatočne vysoko a do blízkosti okna." "Táto akcia odstráni nájdené kanály z tunera USB a opätovne spustí vyhľadávanie nových kanálov.\n\nSkontrolujte, či je tuner USB zapojený a pripojený k zdroju televízneho signálu.\n\nAk používate vzdušnú anténu, upravte jej umiestnenie alebo orientáciu, aby ste získali čo najviac kanálov. Najlepšie výsledky dosiahnete umiestnením antény dostatočne vysoko a do blízkosti okna." + "Táto akcia odstráni nájdené kanály zo sieťového tunera a opätovne spustí vyhľadávanie nových kanálov.\n\nSkontrolujte, či je sieťový tuner zapnutý a pripojený k zdroju televízneho signálu.\n\nAk používate bezdrôtovú anténu, upravte jej umiestnenie alebo orientáciu tak, aby ste získali čo najviac kanálov. Najlepšie výsledky dosiahnete umiestnením antény dostatočne vysoko a do blízkosti okna." "Pokračovať" "Zrušiť" @@ -54,6 +53,7 @@ "Nastavenie televízneho tunera" "Nastavenie kanálov tunera USB" + "Nastavenie kanálov sieťového tunera" "Môže to trvať niekoľko minút" "Tuner nie je dočasne kˆ dispozícii alebo sa práve používa na nahrávanie." @@ -82,6 +82,7 @@ "Nenašli sa žiadne kanály" "Nenašli sa žiadne kanály. Skontrolujte, či je televízor pripojený k zdroju televízneho signálu.\n\nAk používate vzdušnú anténu, upravte jej umiestnenie alebo orientáciu. Najlepšie výsledky dosiahnete umiestnením antény dostatočne vysoko a do blízkosti okna. Potom znova spustite hľadanie." "Nenašli sa žiadne kanály. Skontrolujte, či je tuner USB zapojený a pripojený k zdroju televízneho signálu.\n\nAk používate vzdušnú anténu, upravte jej umiestnenie alebo orientáciu. Najlepšie výsledky dosiahnete umiestnením antény dostatočne vysoko a do blízkosti okna. Potom znova spustite hľadanie." + "Nenašli sa žiadne kanály. Skontrolujte, či je sieťový tuner zapnutý a pripojený k zdroju televízneho signálu.\n\nAk používate bezdrôtovú anténu, upravte jej umiestnenie alebo orientáciu. Najlepšie výsledky dosiahnete umiestnením antény dostatočne vysoko a do blízkosti okna. Potom znova spustite hľadanie." "Hľadať znova" "Hotovo" @@ -89,5 +90,7 @@ "Vyhľadajte televízne kanály" "Nastavenie televízneho tunera" "Nastavenie televízneho tunera s rozhraním USB" - "TV tuner s rozhraním USB je odpojený." + "Nastavenie televízneho sieťového tunera" + "TV tuner s rozhraním USB bol odpojený." + "Sieťový tuner bol odpojený." diff --git a/usbtuner-res/values-sl/strings.xml b/usbtuner-res/values-sl/strings.xml index e454e971..fdde1e61 100644 --- a/usbtuner-res/values-sl/strings.xml +++ b/usbtuner-res/values-sl/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "Sprejemnik TV-kanalov" "Sprejemnik TV-kanalov USB" - "Vklop" - "Izklop" + "Omrežni sprejemnik TV-kanalov (BETA)" "Počakajte, da se dokonča obdelava" - "Izberite vir kanalov" - "Ni signala" - "Iskanje kanala %s ni uspelo" - "Iskanje kanala ni uspelo" "Programska oprema sprejemnika je bila nedavno posodobljena. Znova poiščite kanale." "V sistemskih nastavitvah zvoka omogočite prostorski zvok, če želite omogočiti zvok" + "Zvoka ni mogoče predvajati. Poskusite na drugem televizorju." "Nastavitev sprejemnika kanalov" "Nastavitev sprejemnika TV-kanalov" "Nastavitev sprejemnika kanalov USB" + "Nastavitev omrežnega sprejemnika" "Preverite, ali je televizor povezan z virom TV-signala.\n\nČe uporabljate anteno za prizemno televizijo, prilagodite njen položaj oziroma njeno usmerjenost. Če želite najboljši rezultat, jo postavite na visok položaj in blizu okna ter iščite kanale znova." "Preverite, ali je sprejemnik USB priključen in povezan z virom TV-signala.\n\nČe uporabljate anteno za prizemno televizijo, morate morda prilagoditi njen položaj oziroma njeno usmerjenost, če želite prejemati največ kanalov. Če želite najboljši rezultat, jo postavite na visok položaj in blizu okna." + "Preverite, ali je omrežni sprejemnik vklopljen in povezan z virom TV-signala.\n\nČe uporabljate anteno za prizemno televizijo, prilagodite njen položaj oziroma njeno usmerjenost. Če želite najboljši rezultat, jo postavite na visok položaj in blizu okna." "Naprej" "Ne zdaj" @@ -40,6 +38,7 @@ "Ali želite znova zagnati namestitev kanalov?" "S tem bodo odstranjeni kanali, najdeni prek sprejemnika TV-kanalov, in iskanje kanalov se bo začelo znova.\n\nPreverite, ali je televizor povezan z virom TV-signala.\n\nČe uporabljate anteno za prizemno televizijo, prilagodite njen položaj oziroma njeno usmerjenost. Če želite najboljši rezultat, jo postavite na visok položaj in blizu okna ter iščite kanale znova." "S tem bodo odstranjeni kanali, najdeni prek sprejemnika USB, in iskanje kanalov se bo začelo znova.\n\nPreverite, ali je sprejemnik USB priključen in povezan z virom TV-signala.\n\nČe uporabljate anteno za prizemno televizijo, morate morda prilagoditi njen položaj oziroma njeno usmerjenost, če želite prejemati največ kanalov. Če želite najboljši rezultat, jo postavite na visok položaj in blizu okna." + "S tem bodo odstranjeni kanali, najdeni z omrežnim sprejemnikom kanalov, in vnovič se bo začelo iskanje novih kanalov.\n\nPreverite, ali je omrežni sprejemnik vklopljen in povezan z virom TV-signala.\n\nČe uporabljate anteno za prizemno televizijo, prilagodite njen položaj oziroma njeno usmerjenost. Če želite najboljši rezultat, jo postavite na visok položaj in blizu okna." "Naprej" "Prekliči" @@ -54,6 +53,7 @@ "Nastavitev sprejemnika TV-kanalov" "Nastavitev sprejemnika kanalov USB" + "Nastavitev omrežnega sprejemnika kanalov" "To lahko traja nekaj minut" "Sprejemnik začasno ni na voljo ali se že uporablja za snemanje." @@ -82,6 +82,7 @@ "Ni najdenih kanalov" "Pri iskanju kanali niso bili najdeni. Preverite, ali je televizor povezan z virom TV-signala.\n\nČe uporabljate anteno za prizemno televizijo, prilagodite njen položaj oziroma njeno usmerjenost. Če želite najboljši rezultat, jo postavite na visok položaj in blizu okna ter iščite kanale znova." "Pri iskanju kanali niso bili najdeni. Preverite, ali je sprejemnik USB priključen in povezan z virom TV-signala.\n\nČe uporabljate anteno za prizemno televizijo, prilagodite njen položaj oziroma njeno usmerjenost. Če želite najboljši rezultat, jo postavite na visok položaj in blizu okna ter iščite kanale znova." + "Pri iskanju ni bil najden noben kanal. Preverite, ali je omrežni sprejemnik vklopljen in povezan z virom TV-signala.\n\nČe uporabljate anteno za prizemno televizijo, prilagodite njen položaj oziroma njeno usmerjenost. Če želite najboljši rezultat, jo postavite na visok položaj in blizu okna ter iščite kanale znova." "Znova išči" "Končano" @@ -89,5 +90,7 @@ "Iskanje TV-kanalov" "Nastavitev sprejemnika TV-kanalov" "Nastavitev sprejemnika TV-kanalov USB" - "Povezava s sprejemnikom za TV-kanale USB je prekinjena." + "Nastavitev omrežnega sprejemnika TV-kanalov" + "Povezava s sprejemnikom za TV-kanale USB je prekinjena." + "Povezava z omrežnim sprejemnikom je prekinjena." diff --git a/usbtuner-res/values-sr/strings.xml b/usbtuner-res/values-sr/strings.xml index 6c1fa1bf..12b8ae17 100644 --- a/usbtuner-res/values-sr/strings.xml +++ b/usbtuner-res/values-sr/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "Тјунер за ТВ" "USB тјунер за ТВ" - "Укључи" - "Искључи" + "Мрежни тјунер за ТВ (BETA)" "Сачекајте да се заврши обрада" - "Изаберите извор канала" - "Нема сигнала" - "Укључивање канала %s није успело" - "Укључивање канала није успело" "Софтвер тјунера је недавно ажуриран. Претражите канале поново." "Омогући звучни систем у подешавањима звука да бисте омогућили аудио" + "Звук не може да се пусти. Испробајте други ТВ уређај" "Подешавање тјунера за канале" "Подешавање тјунера за ТВ" "Подешавање USB тјунера за канале" + "Подешавање мрежног тјунера" "Проверите да ли је ТВ повезан са извором ТВ сигнала.\n\nАко користите антену за сигнал преко мреже, можда треба да прилагодите њен положај или смер да бисте имали највише канала. Ако желите најбоље резултате, поставите је високо и близу прозора." "Проверите да ли је USB тјунер прикључен и повезан са извором ТВ сигнала.\n\nАко користите антену за сигнал преко мреже, можда треба да прилагодите њен положај или смер да бисте имали највише канала. Ако желите најбоље резултате, поставите је високо и близу прозора." + "Проверите да ли је мрежни тјунер укључен и повезан са извором ТВ сигнала.\n\nАко користите антену за сигнал преко мреже, можда треба да прилагодите њен положај или смер да бисте имали највише канала. Ако желите најбоље резултате, поставите је високо и близу прозора." "Настави" "Не сада" @@ -40,6 +38,7 @@ "Желите ли да поново покренете подешавање канала?" "На овај начин уклањате канале пронађене помоћу тјунера за ТВ и поново тражите нове канале.\n\nПроверите да ли је ТВ повезан са извором ТВ сигнала.\n\nАко користите антену за сигнал преко мреже, можда треба да прилагодите њен положај или смер да бисте имали највише канала. Ако желите најбоље резултате, поставите је високо и близу прозора." "На овај начин уклањате канале пронађене помоћу USB тјунера и поново тражите нове канале.\n\nПроверите да ли је USB тјунер прикључен и повезан са извором ТВ сигнала.\n\nАко користите антену за сигнал преко мреже, можда треба да прилагодите њен положај или смер да бисте имали највише канала. Ако желите најбоље резултате, поставите је високо и близу прозора." + "На овај начин уклањате канале пронађене помоћу мрежног тјунера и поново скенирате нове канале.\n\nПроверите да ли је мрежни тјунер укључен и повезан са извором ТВ сигнала.\n\nАко користите антену за сигнал преко мреже, можда треба да прилагодите њен положај или смер да бисте имали највише канала. Ако желите најбоље резултате, поставите је високо и близу прозора." "Настави" "Откажи" @@ -54,6 +53,7 @@ "Подешавање тјунера за ТВ" "Подешавање USB тјунера за канале" + "Подешавање мрежног тјунера за канале" "Ово може да потраје неколико минута" "Тјунер привремено није доступан или се већ користи за снимање." @@ -79,6 +79,7 @@ "Није пронађен ниједан канал" "Претрагом није пронађен ниједан канал. Проверите да ли је ТВ повезан са извором ТВ сигнала.\n\nАко користите антену за сигнал преко мреже, прилагодите њен положај или смер. Ако желите најбоље резултате, поставите је високо и близу прозора, па поново обавите претрагу." "Претрагом није пронађен ниједан канал. Проверите да ли је USB тјунер прикључен и повезан са извором ТВ сигнала.\n\nАко користите антену за сигнал преко мреже, прилагодите њен положај или смер. Ако желите најбоље резултате, поставите је високо и близу прозора, па поново обавите претрагу." + "Скенирањем није пронађен ниједан канал. Проверите да ли је мрежни тјунер укључен и повезан са извором ТВ сигнала.\n\nАко користите антену за сигнал преко мреже, прилагодите њен положај или смер. Ако желите најбоље резултате, поставите је високо и близу прозора, па поново обавите скенирање." "Претражи поново" "Готово" @@ -86,5 +87,7 @@ "Потражите ТВ канале" "Подешавање тјунера за ТВ" "Подешавање USB тјунера за ТВ" - "USB тјунер за ТВ није прикључен." + "Подешавање мрежног тјунера за ТВ" + "USB тјунер за ТВ није прикључен." + "Мрежни тјунер није прикључен." diff --git a/usbtuner-res/values-sv/strings.xml b/usbtuner-res/values-sv/strings.xml index dea421bb..dfa02252 100644 --- a/usbtuner-res/values-sv/strings.xml +++ b/usbtuner-res/values-sv/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "TV-mottagare" "USB-TV-mottagare" - "På" - "Av" + "Mottagare för TV över nätverket (BETA)" "Vänta tills sökningen är klar" - "Välj kanalkälla" - "Ingen signal" - "Det gick inte att ställa in %s" - "Det gick inte att ställa in kanalen" "Programvaran för mottagaren har nyligen uppdaterats. Sök igenom kanalerna igen." "Aktivera surroundljud under inställningarna för systemljud om du vill aktivera ljud" + "Det går inte att spela upp ljud. Testa en annan TV" "Inställning av kanalmottagare" "Konfiguration av TV-mottagare" "Kanalinställning för USB-mottagare" + "Konfiguration av nätverksmottagare" "Verifiera att TV:n är ansluten till en signalkälla på TV:n.\n\nOm du använder en antenn för over the air-uppdateringar justerar du dess placering eller riktning. Placera den högt upp och nära ett fönster och sök på nytt för bästa resultat." "Verifiera att USB-mottagaren är ansluten till en signalkälla på TV:n.\n\nOm du använder en antenn för over the air-uppdateringar kan du behöva justera dess placering eller riktning för att hitta så många kanaler som möjligt. Placera den högt upp och nära ett fönster för bästa resultat." + "Verifiera att nätverksmottagaren är på och ansluten till en signalkälla på TV:n.\n\nOm du använder en antenn för over the air-uppdateringar justerar du dess placering eller riktning. Placera den högt upp och nära ett fönster och sök på nytt för bästa resultat." "Fortsätt" "Inte nu" @@ -40,6 +38,7 @@ "Vill du göra om kanalinställningen?" "Kanalerna som hittats via TV-mottagaren tas bort och en ny kanalsökning startas.\n\nVerifiera att TV:n är ansluten till en signalkälla på TV:n.\n\nOm du använder en antenn för over the air-uppdateringar justerar du dess placering eller riktning. Placera den högt upp och nära ett fönster och sök på nytt för bästa resultat." "Kanalerna som hittats via USB-mottagaren tas bort och en ny kanalsökning startas.\n\nVerifiera att USB-mottagaren är ansluten till en signalkälla på TV:n.\n\nOm du använder en antenn för over the air-uppdateringar kan du behöva justera dess placering eller riktning för att hitta så många kanaler som möjligt. Placera den högt upp och nära ett fönster för bästa resultat." + "Åtgärden tar bort alla kanaler som hittades av nätverksmottagaren och söker efter nya kanaler igen.\n\nVerifiera att nätverksmottagaren är på och ansluten till en signalkälla på TV:n.\n\nOm du använder en antenn för over the air-uppdateringar justerar du dess placering eller riktning. Placera den högt upp och nära ett fönster och sök på nytt för bästa resultat." "Fortsätt" "Avbryt" @@ -54,6 +53,7 @@ "Konfiguration av TV-mottagare" "Kanalinställning för USB-mottagare" + "Konfiguration av mottagare för nätverkskanaler" "Det här kan ta flera minuter" "Mottagaren är inte tillgänglig just nu eller så spelas andra program in med den." @@ -76,6 +76,7 @@ "Inga kanaler hittades" "Inga kanaler hittades vid sökningen. Verifiera att TV:n är ansluten till en signalkälla på TV:n.\n\nOm du använder en antenn för over the air-uppdateringar justerar du dess placering eller riktning. Placera den högt upp och nära ett fönster och sök på nytt för bästa resultat." "Inga kanaler hittades vid sökningen. Verifiera att USB-mottagaren är ansluten till en signalkälla på TV:n.\n\nOm du använder en antenn för over the air-uppdateringar justerar du dess placering eller riktning. Placera den högt upp och nära ett fönster och sök på nytt för bästa resultat." + "Inga kanaler hittades under genomsökningen. Verifiera att nätverksmottagaren är på och ansluten till en signalkälla på TV:n.\n\nOm du använder en antenn för over the air-uppdateringar justerar du dess placering eller riktning. Placera den högt upp och nära ett fönster och sök på nytt för bästa resultat." "Sök igen" "Klar" @@ -83,5 +84,7 @@ "Sök efter TV-kanaler" "Konfiguration av TV-mottagare" "Konfiguration av USB-TV-mottagare" - "USB-TV-mottagaren har kopplats från" + "Konfiguration av nätverksmottagare till TV" + "USB-TV-mottagaren har kopplats från." + "Nätverksmottagaren har kopplats från." diff --git a/usbtuner-res/values-sw/strings.xml b/usbtuner-res/values-sw/strings.xml index 1ade7516..d35e239f 100644 --- a/usbtuner-res/values-sw/strings.xml +++ b/usbtuner-res/values-sw/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "Kitafutaji cha vituo vya TV" "Kitafutaji cha Vituo vya TV cha USB" - "Washa" - "Zima" + "Kitafuta Vituo vya TV ya Mtandao (BETA)" "Tafadhali subiri ili shughuli imalizike" - "Chagua chanzo cha kituo" - "Hakuna Mawimbi" - "Imeshindwa kupata kituo cha %s" - "Imeshindwa kupata kituo" "Programu ya kitafutaji cha vituo cha USB ilisasishwa hivi majuzi. Tafadhali tafuta vituo tena." "Washa sauti ya mzunguko katika mipangilio ya mfumo wa sauti ili uruhusu sauti" + "Haiwezi kucheza sauti. Tafadhali jaribu TV nyingine" "Kuweka mipangilio ya kitafutaji cha vituo" "Kuweka mipangilio ya kitafutaji cha vituo vya TV" "Kuweka mipangilio ya kitafutaji cha vituo cha USB" + "Kuweka mipangilio ya kitafuta vituo vya TV" "Hakikisha TV yako imeunganishwa kwenye chanzo cha TV.\n\nIkiwa unatumia antena ya hewani, rekebisha jinsi ilivyowekwa au mwelekeo wake. Ili kupata matokeo bora zaidi, ipandishe juu na uiweke karibu na dirisha." "Thibitisha kuwa kitafutaji cha vituo cha USB kimechomekwa katika chanzo cha umeme na kuunganishwa katika chanzo cha mawimbi ya TV.\n\nKama unatumia antena ya hewani, rekebisha mkao wake au kule inakoelekea. Kwa matokeo bora zaidi, iweke juu , karibu na dirisha." + "Thibitisha kuwa kitafuta vituo kimewashwa na kuunganishwa kwenye chanzo cha mawimbi ya TV. \n\nIkiwa unatumia antena ya hewani, huenda ukahitaji kurekebisha jinsi ilivyowekwa au inakoelekea, ili upate vituo vingi. Kwa matokeo bora zaidi, ipandishe juu na iwe karibu na dirisha." "Endelea" "Si sasa" @@ -40,6 +38,7 @@ "Ungependa kuweka mipangilio ya vituo upya?" "Hatua hii itaondoa vituo vilivyopatikana kwenye kitafutaji cha vituo vya TV na kutafuta vituo vipya tena.\n\nHakikisha TV yako imeunganishwa kwenye chanzo cha mawimbi ya TV.\n\nIkiwa unatumia antena ya hewani, rekebisha jinsi ilivyowekwa au mwelekeo wake. Ili kupata matokeo bora zaidi, ipandishe juu na uiweke karibu na dirisha kisha utafute tena." "Hatua hii itaondoa vituo vilivyopatikana kwenye kitafutaji cha vituo cha USB na kutafuta vituo tena. \n\nThibitisha kuwa kitafutaji cha vituo cha USB kimechomekwa kwenye chanzo cha umeme na kuunganishwa katika chanzo cha mawimbi ya TV.\n\nKama unatumia antena ya hewani, rekebisha mkao wake au kule inakoelekea. Kwa matokeo bora zaidi, iweke juu, karibu na dirisha." + "Hatua hii itaondoa vituo ulivyopata kwenye kitafuta vituo na kutafuta vituo vipya tena.\n\nThibitisha kwamba kitafuta vituo kimewashwa na kuunganishwa kwenye chanzo cha mawimbi ya TV.\n\nIkiwa unatumia antena ya hewani, rekebisha jinsi ilivyowekwa au inakoelekea, ili upate vituo vingi. Kwa matokeo bora zaidi, ipandishe juu na iwe karibu na dirisha." "Endelea" "Ghairi" @@ -54,6 +53,7 @@ "Kuweka mipangilio ya kitafutaji cha vituo vya TV" "Kuweka mipangilio ya kitafutaji cha vituo cha USB" + "Kuweka mipangilio ya kitafuta vituo" "Shughuli hii inaweza kuchukua dakika kadhaa" "Kitafuta vituo hakipatikani kwa sasa au tayari kinatumiwa kurekodi." @@ -76,6 +76,7 @@ "Hakuna Vituo vilivyopatikana" "Hakuna vituo vilivyopatikana baada ya kutafuta. Hakikisha TV yako imeunganishwa kwenye chanzo cha mawimbi ya TV.\n\nIkiwa unatumia antena ya hewani, rekebisha jinsi ilivyowekwa au mwelekeo wake. Ili kupata matokeo bora zaidi, ipandishe juu na uiweke karibu na dirisha kisha utafute tena." "Utafutaji haukupata vituo vyovyote. Thibitisha kuwa kitafutaji cha vituo cha USB kimechomekwa kwenye chanzo cha umeme na kuunganishwa katika chanzo cha mawimbi ya TV.\n\nKama unatumia antena ya hewani, rekebisha mkao wake au kule inakoelekea. Kwa matokeo bora zaidi, iweke juu, karibu na dirisha na utafute tena." + "Haikupata vituo vyovyote. Thibitisha kwamba kitafuta vituo kimewashwa na kuunganishwa kwenye chanzo cha mawimbi ya TV.\n\nIkiwa unatumia antena ya hewani, rekebisha jinsi ilivyowekwa au inakoelekea. Ili upate matokeo bora zaidi, ipandishe juu na karibu na dirisha kisha utafute tena." "Tafuta tena" "Nimemaliza" @@ -83,5 +84,7 @@ "Tafuta vituo vya TV" "Kuweka mipangilio ya kitafutaji cha vituo vya TV" "Kuweka mipangilio ya Kitafutaji cha Vituo cha USB" - "Umeondoa kichagua programu cha USB cha TV." + "Kuweka mipangilio ya Kitafuta Vituo vya TV" + "Umeondoa kichagua programu cha USB cha TV." + "Umeondoa kitafuta mitandao." diff --git a/usbtuner-res/values-ta-rIN/strings.xml b/usbtuner-res/values-ta-rIN/strings.xml index dd09420d..f6a7cbe5 100644 --- a/usbtuner-res/values-ta-rIN/strings.xml +++ b/usbtuner-res/values-ta-rIN/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "டிவி ட்யூனர்" "USB டிவி ட்யூனர்" - "இயக்கு" - "முடக்கு" + "நெட்வொர்க் டிவி டியூனர் (பீட்டா)" "செயலாக்கம் முடியும் வரை காத்திருக்கவும்" - "சேனல் மூலத்தைத் தேர்ந்தெடுக்கவும்" - "சிக்னல் இல்லை" - "%sக்கு ட்யூன் செய்ய முடியவில்லை" - "ட்யூன் செய்ய முடியவில்லை" "ட்யூனர் மென்பொருள் சமீபத்தில் புதுப்பிக்கப்பட்டது. சேனல்களை மீண்டும் ஸ்கேன் செய்யவும்." "ஆடியோவை இயக்க, சாதன ஒலி அமைப்புகளில் \"சரவுண்ட் சவுண்ட்\" என்பதை இயக்கவும்" + "ஆடியோவை இயக்க முடியவில்லை. வேறு டிவியைப் பயன்படுத்தவும்" "சேனல் ட்யூனர் அமைவு" "டிவி ட்யூனர் அமைவு" "USB சேனல் ட்யூனர் அமைவு" + "நெட்வொர்க் டியூனர் அமைவு" "டிவி சிக்னல் மூலத்துடன் உங்கள் டிவி இணைக்கப்பட்டுள்ளதைச் சரிபார்க்கவும்.\n\nஆன்டெனாவைப் பயன்படுத்தினால், அதிகமான சேனல்களைப் பெறுவதற்காக அதன் இடம் அல்லது திசையைச் சரிசெய்ய வேண்டியிருக்கலாம். இன்னும் சிறப்பான முடிவுகளுக்கு, அதை உயரமான இடத்தில் ஜன்னலுக்கு அருகே வைக்கவும்." "USB ட்யூனர் செருகப்பட்டுள்ளதையும் டிவி சிக்னல் மூலத்துடன் இணைக்கப்பட்டுள்ளதையும் சரிபார்க்கவும்.\n\nஆன்டெனாவைப் பயன்படுத்தினால், அதிகமான சேனல்களைப் பெறுவதற்காக அதன் இடம் அல்லது திசையைச் சரிசெய்ய வேண்டியிருக்கலாம். இன்னும் சிறப்பான முடிவுகளுக்கு, அதை உயரமான இடத்தில் ஜன்னலுக்கு அருகே வைக்கவும்." + "நெட்வொர்க் டியூனர் ஆன் செய்யப்பட்டுள்ளதையும் டிவி சிக்னல் மூலத்துடன் இணைக்கப்பட்டுள்ளதையும் சரிபார்க்கவும்.\n\nஆன்டெனாவைப் பயன்படுத்தினால், அதிகமான சேனல்களைப் பெறுவதற்காக அதன் இடம் அல்லது திசையைச் சரிசெய்ய வேண்டியிருக்கலாம். இன்னும் சிறப்பான முடிவுகளுக்கு, அதை உயரமான இடத்தில் ஜன்னலுக்கு அருகே வைக்கவும்." "தொடர்க" "இப்போது வேண்டாம்" @@ -40,6 +38,7 @@ "சேனல் அமைவை மீண்டும் இயக்கவா?" "இவ்வாறு செய்வதால் டிவி ட்யூனர் மூலம் கண்டறிந்த சேனல்கள் அகற்றப்படுவதுடன், புதிய சேனல்களுக்காக மீண்டும் ஸ்கேன் செய்யும்.\n\nடிவி சிக்னல் மூலத்துடன் உங்கள் டிவி இணைக்கப்பட்டுள்ளதைச் சரிபார்க்கவும்.\n\nஆன்டெனாவைப் பயன்படுத்தினால், அதிகமான சேனல்களைப் பெறுவதற்காக அதன் இடம் அல்லது திசையைச் சரிசெய்ய வேண்டியிருக்கலாம். இன்னும் சிறப்பான முடிவுகளுக்கு, அதை உயரமான இடத்தில் ஜன்னலுக்கு அருகே வைக்கவும்." "இவ்வாறு செய்வதால் USB ட்யூனர் மூலம் கண்டறிந்த சேனல்கள் அகற்றப்படுவதுடன், புதிய சேனல்களுக்காக மீண்டும் ஸ்கேன் செய்யும்.\n\nUSB ட்யூனர் செருகப்பட்டுள்ளதையும் டிவி சிக்னல் மூலத்துடன் இணைக்கப்பட்டுள்ளதையும் சரிபார்க்கவும்.\n\nஆன்டெனாவைப் பயன்படுத்தினால், அதிகமான சேனல்களைப் பெறுவதற்காக அதன் இடம் அல்லது திசையைச் சரிசெய்ய வேண்டியிருக்கலாம். இன்னும் சிறப்பான முடிவுகளுக்கு, அதை உயரமான இடத்தில் ஜன்னலுக்கு அருகே வைக்கவும்." + "இது நெட்வொர்க் டியூனரிலிருந்து கண்டறிந்த சேனல்களை அகற்றும் மற்றும் புதிய சேனல்களுக்காக மீண்டும் ஸ்கேன் செய்யும்.\n\nநெட்வொர்க் டியூனர் ஆன் செய்யப்பட்டுள்ளதையும் டிவி சிக்னல் மூலத்துடன் இணைக்கப்பட்டுள்ளதையும் சரிபார்க்கவும்.\n\nஆன்டெனாவைப் பயன்படுத்தினால், அதிகமான சேனல்களைப் பெறுவதற்காக, அதன் இடம் அல்லது திசையைச் சரிசெய்ய வேண்டியிருக்கலாம். இன்னும் சிறப்பான முடிவுகளுக்கு, உயரமான இடத்தில், ஜன்னலுக்கு அருகில் வைக்கவும்." "தொடர்க" "ரத்துசெய்" @@ -54,6 +53,7 @@ "டிவி ட்யூனர் அமைவு" "USB சேனல் ட்யூனர் அமைவு" + "நெட்வொர்க் சேனல் டியூனர் அமைவு" "இதற்குச் சில நிமிடங்கள் ஆகலாம்" "ட்யூனர் தற்காலிகமாகக் கிடைக்கவில்லை அல்லது ரெக்கார்டு செய்வதற்காக ஏற்கனவே பயன்படுத்தப்படுகிறது." @@ -76,6 +76,7 @@ "சேனல்கள் இல்லை" "ஸ்கேன் செய்ததில் சேனல்கள் எவையும் கண்டறியப்படவில்லை. டிவி சிக்னல் மூலத்துடன் உங்கள் டிவி இணைக்கப்பட்டுள்ளதைச் சரிபார்க்கவும்.\n\nஆன்டெனாவைப் பயன்படுத்தினால், அதன் இடம் அல்லது திசையைச் சரிசெய்யவும். இன்னும் சிறப்பான முடிவுகளுக்கு, அதை உயரமான இடத்தில் ஜன்னலுக்கு அருகே வைத்து, மீண்டும் ஸ்கேன் செய்யவும்." "ஸ்கேன் செய்ததில் சேனல்கள் எவையும் கண்டறியப்படவில்லை. USB ட்யூனர் செருகப்பட்டுள்ளதையும் டிவி சிக்னல் மூலத்துடன் இணைக்கப்பட்டுள்ளதையும் சரிபார்க்கவும்.\n\nஆன்டெனாவைப் பயன்படுத்தினால், அதன் இடம் அல்லது திசையைச் சரிசெய்யவும். இன்னும் சிறப்பான முடிவுகளுக்கு, அதை உயரமான இடத்தில் ஜன்னலுக்கு அருகே வைத்து, மீண்டும் ஸ்கேன் செய்யவும்." + "ஸ்கேன் செய்ததில் சேனல்கள் எதுவும் கண்டறியப்படவில்லை. நெட்வொர்க் டியூனர் ஆன் செய்யப்பட்டுள்ளதையும் டிவி சிக்னல் மூலத்துடன் இணைக்கப்பட்டுள்ளதையும் சரிபார்க்கவும்.\n\nஆன்டெனாவைப் பயன்படுத்தினால், அதன் இடம் அல்லது திசையைச் சரிசெய்யவும். இன்னும் சிறப்பான முடிவுகளுக்கு, அதை உயரமான இடத்தில் ஜன்னலுக்கு அருகே வைத்து, மீண்டும் ஸ்கேன் செய்யவும்." "மீண்டும் ஸ்கேன் செய்" "முடிந்தது" @@ -83,5 +84,7 @@ "டிவி சேனல்களை ஸ்கேன் செய்" "டிவி ட்யூனர் அமைவு" "USB டிவி ட்யூனர் அமைவு" - "USB டிவி ட்யூனர் வெளியே எடுக்கப்பட்டது." + "நெட்வொர்க் டிவி டியூனர் அமைவு" + "USB டிவி ட்யூனர் துண்டிக்கப்பட்டது." + "நெட்வொர்க் ட்யூனர் துண்டிக்கப்பட்டது." diff --git a/usbtuner-res/values-te-rIN/strings.xml b/usbtuner-res/values-te-rIN/strings.xml index a80bd1c7..a0023d71 100644 --- a/usbtuner-res/values-te-rIN/strings.xml +++ b/usbtuner-res/values-te-rIN/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "టీవీ ట్యూనర్" "USB టీవీ ట్యూనర్" - "ఆన్ చేయి" - "ఆఫ్ చేయి" + "నెట్‌వర్క్ టీవీ ట్యూనర్ (బీటా)" "దయచేసి ప్రాసెస్ చేయడం పూర్తయ్యే వరకు వేచి ఉండండి" - "మీ ఛానెల్ మూలాన్ని ఎంచుకోండి" - "సిగ్నల్ లేదు" - "%sకి ట్యూన్ చేయడంలో విఫలమైంది" - "ట్యూన్ చేయడంలో విఫలమైంది" "ట్యూనర్ సాఫ్ట్‌వేర్ ఇటీవల నవీకరించబడింది. దయచేసి ఛానెల్‌లను మళ్లీ స్కాన్ చేయండి." "ఆడియోను ప్రారంభించడానికి సిస్టమ్ శబ్ద సెట్టింగ్‌ల్లో పరిసర వ్యాప్త శబ్దాన్ని ప్రారంభించండి" + "ఆడియోను ప్లే చేయడం సాధ్యపడదు. దయచేసి మరో టీవీలో ప్రయత్నించండి" "ఛానెల్ ట్యూనర్ సెటప్" "టీవీ ట్యూనర్ సెటప్" "USB ఛానెల్ ట్యూనర్ సెటప్" + "నెట్‍వర్క్ ట్యూనర్ సెటప్" "టీవీ.సిగ్నల్ సోర్స్‌కు మీ టీవీ కనెక్ట్ చేయబడిందని ధృవపరుచుకోండి.\n\nప్రసారాల కోసం యాంటెన్నాను ఉపయోగిస్తుంటే, మీరు మరిన్ని ఛానెల్‌లను స్వీకరించడానికి దాని స్థానాన్ని లేదా దిశను మార్చాల్సి రావచ్చు. ఉత్తమ ఫలితాల కోసం, దాన్ని ఎత్తులో కిటికీకి దగ్గరగా ఉంచండి." "USB ట్యూనర్ ప్లగిన్ చేయబడి, టీవీ సిగ్నల్ సోర్స్‌కు కనెక్ట్ చేయబడినట్లు ధృవపరుచుకోండి.\n\nప్రసారాల కోసం యాంటెన్నాను ఉపయోగిస్తుంటే, మీరు మరిన్ని ఛానెల్‌లను స్వీకరించడానికి దాని స్థానాన్ని లేదా దిశను మార్చాల్సి రావచ్చు. ఉత్తమ ఫలితాల కోసం, దాన్ని ఎత్తులో కిటికీకి దగ్గరగా ఉంచండి." + "నెట్‍వర్క్ ట్యూనర్ పవర్ ఆన్ చేయబడి, టీవీ సిగ్నల్ సోర్స్‌కు కనెక్ట్ చేయబడినట్లు ధృవపరుచుకోండి.\n\nప్రసారాల కోసం యాంటెన్నాను ఉపయోగిస్తుంటే, మీరు మరిన్ని ఛానెల్‌లను స్వీకరించడానికి దాని స్థానాన్ని లేదా దిశను మార్చాల్సి రావచ్చు. ఉత్తమ ఫలితాల కోసం, దాన్ని ఎత్తులో కిటికీకి దగ్గరగా ఉంచండి." "కొనసాగించు" "ఇప్పుడు కాదు" @@ -40,6 +38,7 @@ "ఛానెల్ సెటప్‌ను మళ్లీ అమలు చేయాలా?" "ఇది టీవీ ట్యూనర్ నుండి కనుగొన్న ఛానెల్‌లను తీసివేస్తుంది మరియు మళ్లీ కొత్త ఛానెల్‌ల కోసం స్కాన్ చేస్తుంది.\n\nటీవీ సిగ్నల్ సోర్స్‌కు మీ టీవీ కనెక్ట్ చేయబడిందని ధృవపరుచుకోండి.\n\nప్రసారాల కోసం యాంటెన్నాను ఉపయోగిస్తుంటే, మీరు మరిన్ని ఛానెల్‌లను స్వీకరించడానికి దాని స్థానాన్ని లేదా దిశను మార్చాల్సి రావచ్చు. ఉత్తమ ఫలితాల కోసం, దాన్ని ఎత్తులో కిటికీకి దగ్గరగా ఉంచండి." "ఇది USB ట్యూనర్ నుండి కనుగొన్న ఛానెల్‌లను తీసివేస్తుంది మరియు మళ్లీ కొత్త ఛానెల్‌ల కోసం స్కాన్ చేస్తుంది.\n\nUSB ట్యూనర్ ప్లగిన్ చేయబడి, టీవీ సిగ్నల్ సోర్స్‌కు కనెక్ట్ చేయబడిందని ధృవపరుచుకోండి.\n\nప్రసారాల కోసం యాంటెన్నాను ఉపయోగిస్తుంటే, మీరు మరిన్ని ఛానెల్‌లను స్వీకరించడానికి దాని స్థానాన్ని లేదా దిశను మార్చాల్సి రావచ్చు. ఉత్తమ ఫలితాల కోసం, దాన్ని ఎత్తులో కిటికీకి దగ్గరగా ఉంచండి." + "ఇది నెట్‍వర్క్ ట్యూనర్ నుండి కనుగొన్న ఛానెల్‌లను తీసివేస్తుంది మరియు మళ్లీ కొత్త ఛానెల్‌ల కోసం స్కాన్ చేస్తుంది.\n\nనెట్‍వర్క్ ట్యూనర్ పవర్ ఆన్ చేయబడి, టీవీ సిగ్నల్ సోర్స్‌కు కనెక్ట్ చేయబడిందని ధృవపరుచుకోండి.\n\nప్రసారాల కోసం యాంటెన్నాను ఉపయోగిస్తుంటే, మీరు మరిన్ని ఛానెల్‌లను స్వీకరించడానికి దాని స్థానాన్ని లేదా దిశను మార్చాలి. ఉత్తమ ఫలితాల కోసం, దాన్ని ఎత్తులో కిటికీకి దగ్గరగా ఉంచండి." "కొనసాగించు" "రద్దు చేయి" @@ -54,6 +53,7 @@ "టీవీ ట్యూనర్ సెటప్" "USB ఛానెల్ ట్యూనర్ సెటప్" + "నెట్‍వర్క్ ఛానెల్ ట్యూనర్ సెటప్" "దీనికి కొన్ని నిమిషాలు పట్టవచ్చు" "ట్యూనర్ తాత్కాలికంగా అందుబాటులో లేదు లేదా ఇప్పటికే రికార్డింగ్ ద్వారా ఉపయోగించబడుతోంది." @@ -76,6 +76,7 @@ "ఛానెల్‌లు ఏవీ కనుగొనబడలేదు" "స్కాన్‌లో ఏ ఛానెల్ కనుగొనబడలేదు. టీవీ సిగ్నల్ సోర్స్‌కి మీ టీవీ కనెక్ట్ చేయబడిందని ధృవపరుచుకోండి. \n\nప్రసారాల కోసం యాంటెన్నాను ఉపయోగిస్తుంటే, దాని స్థానాన్ని లేదా దిశను సర్దుబాటు చేయండి. ఉత్తమ ఫలితాల కోసం, దాన్ని ఎత్తులో కిటికీకి దగ్గరగా ఉంచి, ఆపై మళ్లీ స్కాన్ చేయండి." "స్కాన్‌లో ఛానెల్‌లు ఏవీ కనుగొనబడలేదు. USB ట్యూనర్ ప్లగిన్ చేయబడి, టీవీ సిగ్నల్ సోర్స్‌కు కనెక్ట్ చేయబడిందని ధృవపరుచుకోండి.\n\nప్రసారాల కోసం యాంటెన్నాను ఉపయోగిస్తుంటే, దాని స్థానాన్ని లేదా దిశను సర్దుబాటు చేయండి. ఉత్తమ ఫలితాల కోసం, దాన్ని ఎత్తులో కిటికీకి దగ్గరగా ఉంచి, ఆపై మళ్లీ స్కాన్ చేయండి." + "స్కాన్‌లో ఛానెల్‌లు ఏవీ కనుగొనబడలేదు. నెట్‍వర్క్ ట్యూనర్ పవర్ ఆన్ చేయబడి, టీవీ సిగ్నల్ సోర్స్‌కు కనెక్ట్ చేయబడిందని ధృవపరుచుకోండి.\n\nప్రసారాల కోసం యాంటెన్నాను ఉపయోగిస్తుంటే, దాని స్థానాన్ని లేదా దిశను సర్దుబాటు చేయండి. ఉత్తమ ఫలితాల కోసం, దాన్ని ఎత్తులో కిటికీకి దగ్గరగా ఉంచి, ఆపై మళ్లీ స్కాన్ చేయండి." "మళ్లీ స్కాన్ చేయి" "పూర్తయింది" @@ -83,5 +84,7 @@ "టీవీ ఛానెల్‌ల కోసం స్కాన్ చేయండి" "టీవీ ట్యూనర్ సెటప్" "USB టీవీ ట్యూనర్ సెటప్" - "USB టీవీ ట్యూనర్ డిస్‌కనెక్ట్ చేయబడింది." + "నెట్‍వర్క్ టీవీ ట్యూనర్ సెటప్" + "USB టీవీ ట్యూనర్ డిస్‌కనెక్ట్ చేయబడింది." + "నెట్‌వర్క్ ట్యూనర్ డిస్‌కనెక్ట్ చేయబడింది." diff --git a/usbtuner-res/values-th/strings.xml b/usbtuner-res/values-th/strings.xml index 217520df..8e41544e 100644 --- a/usbtuner-res/values-th/strings.xml +++ b/usbtuner-res/values-th/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "ตัวรับสัญญาณทีวี" "ตัวรับสัญญาณทีวีแบบ USB" - "เปิด" - "ปิด" + "ตัวรับสัญญาณทีวีเครือข่าย (เบต้า)" "โปรดรอให้การดำเนินการหยุดลงสักครู่" - "เลือกแหล่งที่มาของช่อง" - "ไม่มีสัญญาณ" - "ไม่สามารถรับสัญญาณ %s" - "ไม่สามารถรับสัญญาณ" "ซอฟต์แวร์ตัวรับสัญญาณมีการอัปเดตเมื่อเร็วๆ นี้ โปรดสแกนช่องอีกครั้ง" "เปิดใช้เสียงเซอร์ราวด์ในการตั้งค่าเสียงของระบบเพื่อเปิดใช้เสียง" + "เล่นเสียงไม่ได้ โปรดลองทีวีเครื่องอื่น" "ตั้งค่าตัวรับสัญญาณช่อง" "ตั้งค่าตัวรับสัญญาณทีวี" "ตั้งค่าตัวรับสัญญาณช่องแบบ USB" + "ตั้งค่าตัวรับสัญญาณเครือข่าย" "โปรดตรวจสอบว่าทีวีของคุณเชื่อมต่อกับแหล่งสัญญาณทีวีแล้ว\n\nหากใช้เสาอากาศแบบผ่านอากาศ คุณอาจต้องปรับตำแหน่งและทิศทางเพื่อรับช่องให้ได้มากที่สุด เพื่อผลลัพธ์ที่ดีที่สุด ให้วางไว้บนที่สูงใกล้หน้าต่าง" "โปรดตรวจสอบว่าได้เสียบปลั๊กตัวรับสัญญาณแบบ USB และเชื่อมต่อกับแหล่งสัญญาณทีวีแล้ว\n\nหากใช้เสาอากาศแบบผ่านอากาศ คุณอาจต้องปรับตำแหน่งหรือทิศทางเพื่อรับช่องให้ได้มากที่สุด เพื่อผลลัพธ์ที่ดีที่สุด ให้วางไว้บนที่สูงใกล้หน้าต่าง" + "โปรดตรวจสอบว่าตัวรับสัญญาณเครือข่ายเปิดอยู่และเชื่อมต่อกับแหล่งสัญญาณทีวีแล้ว\n\nหากใช้เสาอากาศแบบผ่านอากาศ (OTA) คุณอาจต้องปรับตำแหน่งหรือทิศทางเพื่อรับช่องให้ได้มากที่สุด เพื่อผลลัพธ์ที่ดีที่สุด ให้วางไว้บนที่สูงใกล้หน้าต่าง" "ต่อไป" "ข้ามไปก่อน" @@ -40,6 +38,7 @@ "ต้องการเริ่มการตั้งค่าช่องอีกครั้งใช่ไหม" "วิธีนี้จะนำช่องที่พบจากตัวรับสัญญาณทีวีออกและสแกนหาช่องใหม่อีกครั้ง\n\nโปรดตรวจสอบว่าทีวีของคุณเชื่อมต่อกับแหล่งสัญญาณทีวีแล้ว\n\nหากใช้เสาอากาศแบบผ่านอากาศ คุณอาจต้องปรับตำแหน่งและทิศทางเพื่อรับช่องให้ได้มากที่สุด เพื่อผลลัพธ์ที่ดีที่สุด ให้วางไว้บนที่สูงใกล้หน้าต่าง" "วิธีนี้จะนำช่องที่พบจากตัวรับสัญญาณแบบ USB ออกและสแกนหาช่องใหม่อีกครั้ง\n\nโปรดตรวจสอบว่าได้เสียบปลั๊กตัวรับสัญญาณแบบ USB และเชื่อมต่อกับแหล่งสัญญาณทีวีแล้ว\n\nหากใช้เสาอากาศแบบผ่านอากาศ คุณอาจต้องปรับตำแหน่งหรือทิศทางเพื่อรับช่องให้ได้มากที่สุด เพื่อผลลัพธ์ที่ดีที่สุด ให้วางไว้บนที่สูงใกล้หน้าต่าง" + "การดำเนินการนี้จะนำช่องที่พบจากตัวรับสัญญาณเครือข่ายออกแล้วสแกนหาช่องใหม่อีกครั้ง\n\nโปรดตรวจสอบว่าตัวรับสัญญาณเครือข่ายเปิดอยู่และเชื่อมต่อกับแหล่งสัญญาณทีวีแล้ว\n\nหากใช้เสาอากาศแบบผ่านอากาศ (OTA) คุณอาจต้องปรับตำแหน่งหรือทิศทางเพื่อรับช่องให้ได้มากที่สุด เพื่อผลลัพธ์ที่ดีที่สุด ให้วางไว้บนที่สูงใกล้หน้าต่าง" "ต่อไป" "ยกเลิก" @@ -54,6 +53,7 @@ "ตั้งค่าตัวรับสัญญาณทีวี" "ตั้งค่าตัวรับสัญญาณช่องแบบ USB" + "ตั้งค่าตัวรับสัญญาณช่องเครือข่าย" "อาจใช้เวลาหลายนาที" "ตัวรับสัญญาณไม่สามารถใช้ได้ชั่วคราว หรือถูกใช้ในการบันทึกแล้ว" @@ -76,6 +76,7 @@ "ไม่พบช่อง" "สแกนไม่พบช่องใดเลย โปรดตรวจสอบว่าทีวีของคุณเชื่อมต่อกับแหล่งสัญญาณทีวีแล้ว\n\nหากคุณใช้เสาอากาศแบบผ่านอากาศ ให้ปรับตำแหน่งและทิศทาง เพื่อผลลัพธ์ที่ดีที่สุด ให้วางไว้บนที่สูงใกล้หน้าต่างแล้วสแกนอีกครั้ง" "สแกนไม่พบช่องใดเลย โปรดตรวจสอบว่าได้เสียบปลั๊กตัวรับสัญญาณแบบ USB และเชื่อมต่อกับแหล่งสัญญาณทีวีแล้ว\n\nหากคุณใช้เสาอากาศแบบผ่านอากาศ ให้ปรับตำแหน่งหรือทิศทาง เพื่อให้ได้ผลลัพธ์ที่ดีที่สุด ให้วางไว้บนที่สูงใกล้หน้าต่างแล้วสแกนอีกครั้ง" + "สแกนไม่พบช่องใดเลย โปรดตรวจสอบว่าตัวรับสัญญาณเครือข่ายเปิดอยู่และเชื่อมต่อกับแหล่งสัญญาณทีวีแล้ว\n\nหากใช้เสาอากาศแบบผ่านอากาศ (OTA) ให้ปรับตำแหน่งหรือทิศทาง เพื่อผลลัพธ์ที่ดีที่สุด ให้วางไว้บนที่สูงใกล้หน้าต่างแล้วสแกนอีกครั้ง" "สแกนอีกครั้ง" "เสร็จสิ้น" @@ -83,5 +84,7 @@ "สแกนหาช่องทีวี" "ตั้งค่าตัวรับสัญญาณทีวี" "ตั้งค่าตัวรับสัญญาณทีวีแบบ USB" - "ยกเลิกการเชื่อมต่อตัวรับสัญญาณทีวีผ่าน USB แล้ว" + "ตั้งค่าตัวรับสัญญาณทีวีเครือข่าย" + "ยกเลิกการเชื่อมต่อตัวรับสัญญาณทีวีผ่าน USB แล้ว" + "ยกเลิกการเชื่อมต่อตัวรับสัญญาณเครือข่ายแล้ว" diff --git a/usbtuner-res/values-tl/strings.xml b/usbtuner-res/values-tl/strings.xml index 7a0ce826..83550b1a 100644 --- a/usbtuner-res/values-tl/strings.xml +++ b/usbtuner-res/values-tl/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "TV Tuner" "USB TV Tuner" - "I-on" - "I-off" + "Network TV Tuner (BETA)" "Mangyaring maghintay na matapos ang pagpoproseso" - "Piliin ang pinagmulan ng iyong channel" - "Walang Signal" - "Hindi na-tune sa %s" - "Hindi na-tune" "Na-update kamakailan ang software ng tuner. Paki-scan muli ang mga channel." "I-enable ang surround sound sa mga setting ng tunog ng system upang ma-enable ang audio" + "Hindi ma-play ang audio. Mangyaring sumubok ng ibang TV" "Setup ng channel tuner" "Setup ng TV Tuner" "Setup ng USB channel tuner" + "Pag-set up ng network tuner" "I-verify na nakakonekta ang iyong TV sa isang TV signal source.\n\nKung gumagamit ng over-the-air antenna, maaaring kailanganin mong ayusin ang pagkakalagay o direksyon nito upang makuha ang pinakamaraming channel. Para sa mga pinakamainam na resulta, ilagay ito sa mataas na lugar at malapit sa isang bintana." "I-verify na nakasaksak at nakakonekta ang USB tuner sa isang TV source signal.\n\nKung gumagamit ka ng isang over-the-air antenna, maaaring kailanganin mong ayusin ang pagkakalagay o direksyon nito upang makuha ang pinakamaraming channel. Para sa mga pinakamainam na resulta, ilagay ito sa isang mataas na lugar at malapit sa isang bintana." + "I-verify na naka-on ang network tuner at nakakonekta sa isang pinagmumulan ng TV signal.\n\nKung gumagamit ng over-the-air na antenna, maaaring kailanganin mong ayusin ang pagkakalagay o direksyon nito upang masagap ang karamihan ng mga channel. Para sa mga pinakamainam na resulta, ilagay ito sa mataas na lugar at malapit sa bintana." "Magpatuloy" "Hindi ngayon" @@ -40,6 +38,7 @@ "Gusto mo bang muling patakbuhin ang pag-set up ng channel?" "Aalisin nito ang mga nakitang channel mula sa TV tuner at mag-scan muli para sa mga bagong channel.\n\nI-verify na nakakonekta ang iyong TV sa isang TV signal source.\n\nKung gumagamit ka ng over-the-air antenna, maaaring kailanganin mong ayusin ang pagkakalagay o direksyon nito upang makuha ang pinakamaraming channel. Para sa mga pinakamainam na resulta, ilagay ito sa mataas na lugar at malapit sa isang bintana." "Aalisin nito ang mga channel na nahanap mula sa USB tuner at mag-scan muli para sa mga bagong channel.\n\nI-verify na nakasaksak at nakakonekta ang USB tuner sa isang TV source signal.\n\nKung gumagamit ng over-the-air antenna, maaaring kailanganin mong ayusin ang pagkakalagay o direksyon nito upang makuha ang pinakamaraming channel. Para sa mga pinakamainam na resulta, ilagay ito sa isang mataas na lugar at malapit sa isang bintana." + "Aalisin nito ang mga nahanap na channel sa network tuner at muling magsa-scan ng mga bagong channel.\n\nI-verify na naka-on ang network tuner at nakakonekta sa isang pinagmumulan ng TV signal.\n\nKung gumagamit ng over-the-air na antenna, maaaring kailanganin mong ayusin ang pagkakalagay o direksyon nito upang masagap ang karamihan ng mga channel. Para sa mga pinakamainam na resulta, ilagay ito sa mataas na lugar at malapit sa bintana." "Magpatuloy" "Kanselahin" @@ -54,6 +53,7 @@ "Setup ng TV tuner" "Setup ng USB channel tuner" + "Pag-set up ng network channel tuner" "Maaari itong tumagal ng ilang minuto" "Pansamantalang hindi available ang Tuner o ginagamit na ito ng recording." @@ -76,6 +76,7 @@ "Walang nakitang Mga Channel" "Hindi nakahanap ng anumang mga channel ang pag-scan. I-verify na nakakonekta ang iyong TV sa isang TV signal source.\n\nKung gumagamit ka ng over-the-air antenna, ayusin ang pagkakalagay o direksyon nito. Para sa mga pinakamainam na resulta, ilagay ito sa mataas na lugar malapit sa isang bintana at mag-scan muli." "Walang nahanap na anumang mga channel noong nag-scan. I-verify na nakasaksak at nakakonekta ang USB tuner sa isang TV signal source.\n\nKung gumagamit ka ng over-the-air antenna, ayusin ang pagkakalagay o direksyon nito. Para sa mga pinakamainam na resulta, ilagay ito sa isang mataas na lugar at malapit sa isang bintana mag-scan muli." + "Walang nahanap na channel sa pag-scan. I-verify na naka-on ang network tuner at nakakonekta sa isang pinagmumulan ng TV signal.\n\nKung gumagamit ng over-the-air na antenna, ayusin ang pagkakalagay o direksyon nito. Para sa mga pinakamainam na resulta, ilagay ito sa mataas na lugar at malapit sa bintana at muling mag-scan." "Muling mag-scan" "Tapos Na" @@ -83,5 +84,7 @@ "Mag-scan ng mga channel sa TV" "Setup ng TV Tuner" "Setup ng USB TV Tuner" - "Nadiskonekta ang USB TV tuner." + "Pag-set up ng Network TV Tuner" + "Nadiskonekta ang USB TV tuner." + "Nadiskonekta ang network tuner." diff --git a/usbtuner-res/values-tr/strings.xml b/usbtuner-res/values-tr/strings.xml index 29910f9e..198d72ae 100644 --- a/usbtuner-res/values-tr/strings.xml +++ b/usbtuner-res/values-tr/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "TV Kanal Ayarlayıcı" "USB TV Kanal Ayarlayıcı" - "Açık" - "Kapalı" + "Ağ TV Kanal Ayarlayıcı (BETA)" "Lütfen işlemin tamamlanmasını bekleyin" - "Kanal kaynağınızı seçin" - "Sinyal Yok" - "%s adlı kanala ayarlanamadı" - "Kanal ayarlanamadı" "Kanal ayarlayıcı yazılımı yakın zamanda güncellendi. Lütfen kanalları yeniden tarayın." "Sesi etkinleştirmek için sistemin ses ayarlarında surround sesi etkinleştirin" + "Ses çalınamıyor. Lütfen başka bir TV deneyin" "Kanal ayarlayıcı kurulumu" "TV Kanal Ayarlayıcı kurulumu" "USB kanal ayarlayıcı kurulumu" + "Ağ kanal ayarlayıcı kurulumu" "TV\'nizin bir TV sinyal kaynağına bağlı olduğunu doğrulayın.\n\nHavadan yayın alan bir anten kullanıyorsanız en çok sayıda kanalı almak için antenin yerini veya yönünü ayarlamanız gerekebilir. En iyi sonucu elde etmek için anteni yüksek ve pencereye yakın bir yere yerleştirin." "USB kanal ayarlayıcının takılı olduğunu ve bir TV sinyali kaynağına bağlandığını doğrulayın.\n\nHavadan yayın alan bir anten kullanıyorsanız en çok sayıda kanalı almak için antenin yerini veya yönünü ayarlamanız gerekebilir. En iyi sonucu elde etmek için anteni yüksek ve pencereye yakın bir yere yerleştirin." + "Ağ kanal ayarlayıcının açık olduğunu ve TV sinyali alabileceği bir kaynağa bağlandığını doğrulayın.\n\nHavadan gelen yayını alacak bir anten kullanıyorsanız en fazla sayıda kanalı almak için antenin yerini veya yönünü ayarlamanız gerekebilir. En iyi sonuçları elde etmek için anteni yüksek ve pencereye yakın bir yere yerleştirin." "Devam" "Şimdi değil" @@ -40,6 +38,7 @@ "Kanal kurulumu yeniden çalıştırılsın mı?" "Bu işlem, TV kanal ayarlayıcının bulduğu kanalları kaldıracak ve tekrar yeni kanallar arayacak.\n\nTV\'nizin bir TV sinyal kaynağına bağlı olduğunu doğrulayın.\n\nHavadan yayın alan bir anten kullanıyorsanız en çok sayıda kanalı almak için antenin yerini veya yönünü ayarlamanız gerekebilir. En iyi sonucu elde etmek için anteni yüksek ve pencereye yakın bir yere yerleştirin." "Bu işlem, USB kanal ayarlayıcıdan bulunan kanalları kaldıracak ve yeni kanallar için tekrar tarama yapacaktır.\n\nUSB kanal ayarlayıcının takılı olduğunu ve bir TV sinyali kaynağına bağlandığını doğrulayın.\n\nHavadan yayın alan bir anten kullanıyorsanız en çok sayıda kanalı almak için antenin yerini veya yönünü ayarlamanız gerekebilir. En iyi sonucu elde etmek için anteni yüksek ve pencereye yakın bir yere yerleştirin." + "Bu işlem, bulunan kanalları ağ kanal ayarlayıcıdan kaldıracak ve yeni kanalların bulunması için tekrar tarama yapacak.\n\nAğ kanal ayarlayıcının açık olduğunu ve TV sinyali alabileceği bir kaynağa bağlandığını doğrulayın.\n\nHavadan gelen yayını alacak bir anten kullanıyorsanız en fazla sayıda kanalı almak için antenin yerini veya yönünü ayarlamanız gerekebilir. En iyi sonucu elde etmek için anteni yüksek ve pencereye yakın bir yere yerleştirin." "Devam" "İptal" @@ -54,6 +53,7 @@ "TV kanal ayarlayıcı kurulumu" "USB kanal ayarlayıcı kurulumu" + "Ağ kanal ayarlayıcı kurulumu" "Bu işlem birkaç dakika sürebilir" "Kanal ayarlayıcı geçici olarak kullanılamıyor veya şu anda kayıt yapmak için kullanılıyor." @@ -76,6 +76,7 @@ "Hiçbir kanal bulunamadı" "Tarama işleminde hiçbir kanal bulunamadı. TV\'nizin bir TV sinyal kaynağına bağlı olduğunu doğrulayın.\n\nHavadan yayın alan bir anten kullanıyorsanız yerini veya yönünü ayarlayın. En iyi sonucu elde etmek için anteni yüksek ve pencereye yakın bir yere yerleştirerek tekrar tarayın." "Tarama işlemi herhangi bir kanal bulamadı. USB kanal ayarlayıcının takılı olduğunu ve bir TV sinyali kaynağına bağlandığını doğrulayın.\n\nHavadan yayın alan bir anten kullanıyorsanız yerini veya yönünü ayarlayın. En iyi sonucu elde etmek için anteni yüksek ve pencereye yakın bir yere yerleştirerek tekrar tarayın." + "Taramada herhangi bir kanal bulunamadı. Ağ kanal ayarlayıcının açık olduğunu ve TV sinyali alabileceği bir kaynağa bağlandığını doğrulayın.\n\nHavadan gelen yayınları alacak bir anten kullanıyorsanız antenin yerini veya yönünü ayarlayın. En iyi sonucu elde etmek için anteni yüksek ve pencereye yakın bir yere yerleştirin." "Yeniden tara" "Bitti" @@ -83,5 +84,7 @@ "TV kanallarını tarayın" "TV Kanal Ayarlayıcı kurulumu" "USB TV Kanal Ayarlayıcı kurulumu" - "USB TV kanal ayarlayıcı bağlantısı kesildi." + "Ağ TV Kanal Ayarlayıcı kurulumu" + "USB TV kanal ayarlayıcı bağlantısı kesildi." + "Ağ kanal ayarlayıcı bağlantısı kesildi." diff --git a/usbtuner-res/values-uk/strings.xml b/usbtuner-res/values-uk/strings.xml index cf7cc85e..06076825 100644 --- a/usbtuner-res/values-uk/strings.xml +++ b/usbtuner-res/values-uk/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "ТВ-тюнер" "ТВ-тюнер USB" - "Увімкнути" - "Вимкнути" + "Мережевий ТВ-тюнер (БЕТА-ВЕРСІЯ)" "Зачекайте, доки завершиться пошук" - "Виберіть джерело каналу" - "Немає сигналу" - "Не вдалося налаштувати канал %s" - "Не вдалося налаштувати" "Програмне забезпечення тюнера нещодавно оновлено. Проскануйте канали знову." "Увімкнути об’ємний звук у налаштуваннях системи, щоб слухати аудіо" + "Не вдається відтворити відео. Спробуйте на іншому телевізорі" "Налаштування тюнера" "Налаштування ТВ-тюнера" "Налаштування USB-тюнера" + "Налаштування мережевого тюнера" "Переконайтеся, що ви під’єднали телевізор до джерела вхідного телевізійного сигналу.\n\nЯкщо ви користуєтесь ефірною антеною, змініть її положення,щоб знайти більше каналів. Розмістіть антену вище та біля вікна." "Переконайтеся, що ви підключили USB-тюнер і під’єднали джерело вхідного телевізійного сигналу.\n\nЯкщо ви користуєтесь ефірною антеною, змініть її положення, щоб знайти більше каналів. Розмістіть антену вище та біля вікна." + "Переконайтеся, що мережевий тюнер увімкнено та під’єднано до джерела телевізійного сигналу.\n\nЯкщо у вас ефірна антена, змініть її положення. Розмістіть антену вище та біля вікна й повторіть спробу." "Продовжити" "Не зараз" @@ -40,6 +38,7 @@ "Відновити налаштування каналу?" "Канали, знайдені за допомогою ТВ-тюнера, буде видалено. Пошук почнеться знову.\n\nПереконайтеся, що ви під’єднали телевізор до джерела вхідного телевізійного сигналу.\n\nЯкщо ви користуєтесь ефірною антеною, змініть її положення, щоб знайти більше каналів. Розмістіть антену вище та біля вікна й повторіть спробу." "Канали, знайдені за допомогою USB-тюнера, буде видалено. Пошук почнеться знову.\n\nПереконайтеся, що ви підключили USB-тюнер і під’єднали джерело вхідного телевізійного сигналу.\n\nЯкщо ви користуєтесь ефірною антеною, змініть її положення, щоб знайти більше каналів. Розмістіть антену вище та біля вікна." + "Канали, знайдені мережевим тюнером, буде видалено. Пошук почнеться знову.\n\nПереконайтеся, що мережевий тюнер увімкнено та під’єднано до джерела телевізійного сигналу.\n\nЯкщо у вас ефірна антена, змініть її положення. Розмістіть антену вище та біля вікна й повторіть спробу." "Продовжити" "Скасувати" @@ -54,6 +53,7 @@ "Налаштування ТВ-тюнера" "Налаштування USB-тюнера" + "Налаштування мережевого тюнера" "Це може зайняти декілька хвилин" "Тюнер тимчасово недоступний або вже використовується для запису." @@ -82,6 +82,7 @@ "Канали не знайдено" "Канали не знайдено. Переконайтеся, що ви під’єднали телевізор до джерела вхідного телевізійного сигналу.\n\nЯкщо ви користуєтесь ефірною антеною, змініть її положення. Розмістіть антену вище та біля вікна й повторіть спробу." "Канали не знайдено. Переконайтеся, що ви підключили USB-тюнер і під’єднали джерело вхідного телевізійного сигналу.\n\nЯкщо ви користуєтесь ефірною антеною, змініть її положення. Розмістіть антену вище та біля вікна й повторіть спробу." + "Каналів не знайдено. Переконайтеся, що мережевий тюнер увімкнено та під’єднано до джерела телевізійного сигналу.\n\nЯкщо у вас ефірна антена, змініть її положення. Розмістіть антену вище та біля вікна й повторіть спробу." "Шукати знову" "Готово" @@ -89,5 +90,7 @@ "Пошук телевізійних каналів" "Налаштування ТВ-тюнера" "Налаштування ТВ-тюнера USB" - "ТВ-тюнер USB від’єднано." + "Налаштування мережевого ТВ-тюнера" + "ТВ-тюнер USB від’єднано." + "Мережевий тюнер від’єднано." diff --git a/usbtuner-res/values-ur-rPK/strings.xml b/usbtuner-res/values-ur-rPK/strings.xml index 68c85d05..0debaa53 100644 --- a/usbtuner-res/values-ur-rPK/strings.xml +++ b/usbtuner-res/values-ur-rPK/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "‏TV ٹیونر" "‏USB TV ٹیونر" - "آن" - "آف" + "‏نیٹ ورک TV ٹیونر (بی ٹا)" "براہ کرم کارروائی ختم ہونے کا انتظار کریں" - "اپنے چینل کا ماخذ منتخب کریں" - "کوئی سگنل نہیں ہے" - "%s پر ٹیون ہونے میں ناکام ہوگیا" - "ٹیون کرنے میں ناکام ہو گیا" "ٹیونر سافٹ ویئر حال ہی میں اپ ڈیٹ کیا گیا ہے۔ براہ کرم چینلز کو دوبارہ اسکین کریں۔" "آڈیو کو فعال کرنے کیلئے سسٹم کی آواز کی ترتیبات میں محیط آواز فعال کریں" + "آڈیو نہیں چل رہی۔ براہ کرم کوئی اور ٹی وی آزمائیں" "چینل ٹیونر سیٹ اپ" "‏TV ٹیونر سیٹ اپ" "‏USB چینل ٹیونر سیٹ اپ" + "نیٹ ورک ٹیونر سیٹ اپ" "‏اپنے TV کے کسی TV سگنل ماخذ سے منسلک ہونے کی توثیق کریں۔\n\nاگر وائرلیس نیٹ ورک کنکشن انٹینا استعمال کر رہے ہیں تو زیادہ تر چینلز موصول کرنے کیلئے آپ کو اس کا مقام یا سمت ایڈجسٹ کرنے کی ضرورت پیش آ سکتی ہے۔ بہترین نتائج کیلئے اسے اونچی جگہ اور کھڑکی کے پاس رکھیں۔" "‏USB ٹیونر کے پلگ ان ہونے اور TV سگنل ماخذ سے منسلک ہونے کی توثیق کریں۔\n\nاگر وائرلیس نیٹ ورک کنکشن انٹینا استعمال کر رہے ہیں تو زیادہ تر چینلز موصول کرنے کیلئے آپ کو اس کا مقام یا سمت ایڈجسٹ کرنے کی ضرورت پیش آ سکتی ہے۔ بہترین نتائج کیلئے اسے اونچی جگہ اور کھڑکی کے پاس رکھیں۔" + "‏نیٹ ورک ٹیونر کے آن ہونے اور TV سگنل ماخذ سے منسلک ہونے کی توثیق کریں۔\n\nاگر وائرلیس نیٹ ورک کنکشن انٹینا استعمال کر رہے ہیں تو زیادہ تر چینلز موصول کرنے کیلئے آپ کو اس کا مقام یا سمت ایڈجسٹ کرنے کی ضرورت پیش آ سکتی ہے۔ بہترین نتائج کیلئے اسے اونچی جگہ اور کھڑکی کے پاس رکھیں۔" "جاری رکھیں" "ابھی نہیں" @@ -40,6 +38,7 @@ "چینل سیٹ اپ دوبارہ چلائیں؟" "‏یہ TV ٹیونر سے ملے چینلز ہٹا دے گا اور دوبارہ نئے چینلز کیلئے اسکین کرے گا۔\n\nاپنے TV کے کسی سگنل ماخذ سے منسلک ہونے کی توثیق کریں۔\n\nاگر وائرلیس نیٹ ورک کنکشن انٹینا استعمال کر رہے ہیں تو زیادہ تر چینلز موصول کرنے کیلئے آپ کو اس کا مقام یا سمت ایڈجسٹ کرنے کی ضرورت پیش آ سکتی ہے۔ بہترین نتائج کیلئے اسے اونچی جگہ اور کھڑکی کے پاس رکھیں۔" "‏یہ USB ٹیونر سے ملے چینلز ہٹا دے گا اور دوبارہ نئے چینلز کیلئے اسکین کرے گا۔\n\nTV ٹیونر کے پلگ ان ہونے اور سگنل ماخذ سے منسلک ہونے کی توثیق کریں۔\n\nاگر وائرلیس نیٹ ورک کنکشن انٹینا استعمال کر رہے ہیں تو زیادہ تر چینلز موصول کرنے کیلئے آپ کو اس کا مقام یا سمت ایڈجسٹ کرنے کی ضرورت پیش آ سکتی ہے۔ بہترین نتائج کیلئے اسے اونچی جگہ اور کھڑکی کے پاس رکھیں۔" + "‏یہ نیٹ ورک ٹیونر سے ملنے والے چیلنز ہٹا دے گا اور دوبارہ نئے چینلز کیلئے اسکین کرے گا۔\n\nTV ٹیونر کے آن ہونے اور سگنل ماخذ سے منسلک ہونے کی توثیق کریں۔\n\nاگر وائرلیس نیٹ ورک کنکشن انٹینا استعمال کر رہے ہیں، تو زیادہ تر چینلز موصول کرنے کیلئے آپ کو اس کی مقام بندی یا سمت ایڈجسٹ کرنے کی ضرورت پیش آ سکتی ہے۔ بہترین نتائج کیلئے اسے اونچی جگہ اور کھڑکی کے پاس رکھیں۔" "جاری رکھیں" "منسوخ کریں" @@ -54,6 +53,7 @@ "‏TV ٹیونر سیٹ اپ" "‏USB چینل ٹیونر سیٹ اپ" + "نیٹ ورک چینل ٹیونر سیٹ اپ" "اس میں کئی منٹ لگ سکتے ہیں" "ٹیونر عارضی طور پر غیر دستیاب ہے یا پہلے سے ریکارڈنگ کی وجہ سے استعمال ہو گیا ہے۔" @@ -76,6 +76,7 @@ "کوئی چینلز نہیں ملے" "‏اسکین سے کوئی چینلز نہیں ملے۔ اپنے TV کے ایک TV سگنل ماخذ سے منسلک ہونے کی توثیق کریں۔\n\nاگر وائرلیس نیٹ ورک کنکشن انٹینا استعمال کر رہے ہیں تو اس کا مقام یا سمت ایڈجسٹ کریں۔ بہترین نتائج کیلئے اسے اونچی جگہ اور کھڑکی کے پاس رکھیں اور دوبارہ اسکین کریں۔" "‏اسکین سے کوئی چینلز نہیں ملے۔ USB ٹیونر کے پلگ ان ہونے اور TV سگنل ماخذ سے منسلک ہونے کی توثیق کریں۔\n\nاگر وائرلیس نیٹ ورک کنکشن انٹینا استعمال کر رہے ہیں تو اس کا مقام یا سمت ایڈجسٹ کریں۔ بہترین نتائج کیلئے اسے اونچی جگہ اور کھڑکی کے پاس رکھیں اور دوبارہ اسکین کریں۔" + "‏اسکین سے کوئی چینلز نہیں ملے۔ توثیق کریں کہ نیٹ ورک ٹیونر آن ہے اور TV سگنل ماخذ سے منسلک ہے۔\n\nاگر وائرلیس نیٹ ورک کنکشن انٹینا استعمال کر رہے ہیں، تو اس کا مقام یا سمت ایڈجسٹ کریں۔ بہترین نتائج کیلئے، اسے اونچی جگہ اور کھڑکی کے قریب رکھیں اور دوبارہ اسکین کریں۔" "دوبارہ اسکین کریں" "ہو گیا" @@ -83,5 +84,7 @@ "‏TV چینلز کے لیے اسکین کریں" "‏TV ٹیونر سیٹ اپ" "‏USB TV ٹیونر سیٹ اپ" - "‏USB TV ٹیونر غیر منسلک ہے۔" + "‏نیٹ ورک TV ٹیونر سیٹ اپ" + "‏USB TV ٹیونر غیر منسلک ہے۔" + "نیٹ ورک ٹیونر غیر منسلک ہے۔" diff --git a/usbtuner-res/values-uz-rUZ/strings.xml b/usbtuner-res/values-uz-rUZ/strings.xml index 9aefd57b..97bd97b6 100644 --- a/usbtuner-res/values-uz-rUZ/strings.xml +++ b/usbtuner-res/values-uz-rUZ/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "TV-tyuner" "USB TV-tyuner" - "Yoqish" - "O‘chirib qo‘yish" + "Tarmoq TV-tyuneri (BETA)" "Iltimos, jarayon tugashini kuting" - "Kanal manbasini tanlang" - "Signal yo‘q" - "“%s” kanaliga sozlab bo‘lmadi" - "Sozlab bo‘lmadi" "Tyunerning dasturiy ta’minoti yaqinda yangilandi. Kanallarni qaytadan qidiring." "Audioni yoqish uchun tizim ovozi sozlamalari orqali qamrovli ovozni yoqing" + "Audioni ijro ettirib bo‘lmadi. Boshqa kanalni sinab ko‘ring." "Tyunerni sozlash" "TV-tyunerni sozlash" "USB-tyunerni sozlash" + "Tarmoq tyunerini sozlash" "Televizor signali manbasiga ulangan ekanligini tekshiring.\n\nAgar havo orqali to‘lqin tutuvchi antennadan foydalanayotgan bo‘lsangiz, ko‘proq kanal topilishi uchun uning joyi va yo‘nalishini sozlang. Eng yaxshi natijaga erishish uchun uni yuqoriroq va oynaga yaqin joyga o‘rnating." "USB-tyuner suqilgan hamda televizor signali manbasiga ulangan ekanligini tekshiring.\n\nAgar havo orqali to‘lqin tutuvchi antennadan foydalanayotgan bo‘lsangiz, ko‘proq kanal topilishi uchun uning joyi va yo‘nalishini sozlang. Eng yaxshi natijaga erishish uchun uni yuqoriroq va oynaga yaqin joyga o‘rnating." + "Tarmoq tyuneri yoqilgan hamda televizor signali manbasiga ulangan ekanligini tekshiring.\n\nAgar havo orqali to‘lqin tutuvchi antennadan foydalanayotgan bo‘lsangiz, ko‘proq kanal topilishi uchun uning joyi va yo‘nalishini sozlang. Eng yaxshi natijaga erishish uchun uni yuqoriroq va oynaga yaqin joyga o‘rnating." "Davom etish" "Hozir emas" @@ -40,6 +38,7 @@ "Kanallar qaytadan sozlansinmi?" "Buning natijasida TV-tyuner orqali topilgan kanallar o‘chirib tashlanadi va kanallar boshqatdan qidiriladi.\n\nTelevizor signal manbasiga ulangan ekanligini tekshiring.\n\nAgar havo orqali to‘lqin tutuvchi antennadan foydalanayotgan bo‘lsangiz, uning joyi va yo‘nalishini sozlang. Eng yaxshi natijaga erishish uchun uni yuqoriroq va oynaga yaqin joyga o‘rnating hamda qaytadan qidiring." "Buning natijasida USB-tyuner orqali topilgan kanallar o‘chirib tashlanadi va kanallar boshqatdan qidiriladi.\n\nUSB-tyuner suqilgan hamda televizor signali manbasiga ulangan ekanligini tekshiring.\n\nAgar havo orqali to‘lqin tutuvchi antennadan foydalanayotgan bo‘lsangiz, uning joyi va yo‘nalishini sozlang. Eng yaxshi natijaga erishish uchun uni yuqoriroq va oynaga yaqin joyga o‘rnating hamda qaytadan qidiring." + "Buning natijasida tarmoq tyuneri orqali topilgan kanallar o‘chirib tashlanadi va kanallar boshqatdan qidiriladi.\n\nTarmoq tyuneri yoqilgan hamda televizor signali manbasiga ulangan ekanligini tekshiring.\n\nAgar havo orqali to‘lqin tutuvchi antennadan foydalanayotgan bo‘lsangiz, uning joyi va yo‘nalishini sozlang. Eng yaxshi natijaga erishish uchun uni yuqoriroq va oynaga yaqin joyga o‘rnating hamda qaytadan qidiring." "Davom etish" "Bekor qilish" @@ -54,6 +53,7 @@ "TV-tyunerni sozlash" "USB-tyunerni sozlash" + "Tarmoq tyuneri kanallarini sozlash" "Bu bir necha daqiqa vaqt olishi mumkin" "Tyunerdan vaqtinchalik foydalanib bo‘lmaydi yoki allaqachon yozib olishda foydalanilmoqda." @@ -76,6 +76,7 @@ "Hech qanday kanal topilmadi" "Qidiruv natijasida hech qanday kanal topilmadi. Televizor signal manbasiga ulangan ekanligini tekshiring.\n\nAgar havo orqali to‘lqin tutuvchi antennadan foydalanayotgan bo‘lsangiz, uning joyi va yo‘nalishini sozlang. Eng yaxshi natijaga erishish uchun uni yuqoriroq va oynaga yaqin joyga o‘rnating hamda qaytadan qidiring." "Qidiruv natijasida hech qanday kanal topilmadi. USB-tyuner suqilgan hamda televizor signali manbasiga ulangan ekanligini tekshiring.\n\nAgar havo orqali to‘lqin tutuvchi antennadan foydalanayotgan bo‘lsangiz, uning joyi va yo‘nalishini sozlang. Eng yaxshi natijaga erishish uchun uni yuqoriroq va oynaga yaqin joyga o‘rnating hamda qaytadan qidiring." + "Qidiruv natijasida hech qanday kanal topilmadi. Tarmoq tyuneri yoqilgan hamda televizor signali manbasiga ulangan ekanligini tekshiring.\n\nAgar havo orqali to‘lqin tutuvchi antennadan foydalanayotgan bo‘lsangiz, uning joyi va yo‘nalishini sozlang. Eng yaxshi natijaga erishish uchun uni yuqoriroq va oynaga yaqin joyga o‘rnating hamda qaytadan qidiring." "Yana qidirish" "Tayyor" @@ -83,5 +84,7 @@ "Telekanallarni qidiring" "TV-tyunerni sozlash" "USB TV-tyunerni sozlang" - "USB-tyuner o‘chirib qo‘yildi." + "Tarmoq TV-tyunerini sozlash" + "USB-tyuner o‘chirib qo‘yildi." + "Tarmoq tyuneri uzib qo‘yildi." diff --git a/usbtuner-res/values-vi/strings.xml b/usbtuner-res/values-vi/strings.xml index 605234e4..838ae769 100644 --- a/usbtuner-res/values-vi/strings.xml +++ b/usbtuner-res/values-vi/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "Bộ dò TV" "Bộ dò TV USB" - "Bật" - "Tắt" + "Bộ dò TV mạng (BETA)" "Vui lòng đợi để hoàn tất xử lý" - "Chọn nguồn kênh của bạn" - "Không có tín hiệu" - "Dò tới %s không thành công" - "Dò không thành công" "Phần mềm bộ dò đã được cập nhật gần đây. Vui lòng quét lại các kênh." "Bật tính năng âm thanh vòm trong cài đặt âm thanh hệ thống để bật âm thanh" + "Không thể phát âm thanh. Vui lòng thử TV khác" "Thiết lập bộ dò kênh" "Thiết lập bộ dò TV" "Thiết lập bộ dò kênh USB" + "Thiết lập bộ dò mạng" "Hãy xác minh rằng TV của bạn đã được kết nối với nguồn tín hiệu TV.\n\nNếu sử dụng ăng-ten không dây thì bạn có thể cần phải điều chỉnh vị trí hoặc hướng của ăng-ten đó để nhận nhiều kênh nhất. Để có kết quả tốt nhất, hãy đặt ăng-ten lên cao và gần cửa sổ." "Xác minh rằng bộ dò USB đã được cắm và được kết nối với nguồn tín hiệu TV.\n\nNếu sử dụng ăng-ten không dây, có thể bạn cần phải điều chỉnh vị trí hoặc hướng của ăng-ten đó để nhận được nhiều kênh nhất. Để có kết quả tốt nhất, hãy đặt ăng-ten lên cao và gần cửa sổ." + "Hãy xác minh là bộ dò mạng đã được bật nguồn và kết nối với một nguồn tín hiệu TV.\n\nNếu sử dụng ăng-ten không dây, hãy điều chỉnh vị trí hoặc hướng của ăng-ten để nhận hầu hết kênh. Để có kết quả tốt nhất, hãy đặt ăng-ten ở vị trí cao và gần cửa sổ." "Tiếp tục" "Không phải bây giờ" @@ -40,6 +38,7 @@ "Chạy lại quá trình thiết lập kênh?" "Điều này sẽ xóa các kênh được tìm thấy khỏi bộ dò TV và quét các kênh mới lần nữa.\n\nHãy xác minh rằng TV của bạn đã được kết nối với nguồn tín hiệu TV.\n\nNếu sử dụng ăng-ten không dây thì bạn có thể cần phải điều chỉnh vị trí hoặc hướng của ăng-ten đó để nhận nhiều kênh nhất. Để có kết quả tốt nhất, hãy đặt ăng-ten lên cao và gần cửa sổ." "Quá trình này sẽ xóa các kênh bộ dò USB đã tìm thấy và quét lại để tìm các kênh mới.\n\nHãy xác minh rằng bộ dò USB đã được cắm và kết nối với nguồn tín hiệu TV.\n\nNếu sử dụng ăng-ten không dây, bạn có thể cần phải điều chỉnh vị trí hoặc hướng của ăng-ten đó để nhận được nhiều kênh nhất. Để có kết quả tốt nhất, hãy đặt ăng-ten lên cao và gần cửa sổ." + "Điều này sẽ xóa các kênh được tìm thấy từ bộ dò mạng và quét các kênh mới một lần nữa.\n\nHãy xác minh là bộ dò mạng đã được bật nguồn và kết nối với một nguồn tín hiệu TV.\n\nNếu sử dụng ăng-ten không dây, hãy điều chỉnh vị trí hoặc hướng của ăng-ten để nhận hầu hết kênh. Để có kết quả tốt nhất, hãy đặt ăng-ten ở vị trí cao và gần cửa sổ." "Tiếp tục" "Hủy" @@ -54,6 +53,7 @@ "Thiết lập bộ dò TV" "Thiết lập bộ dò kênh USB" + "Thiết lập bộ do kênh mạng" "Quá trình này có thể mất vài phút" "Bộ dò tạm thời không có sẵn hoặc đã được sử dụng để ghi." @@ -76,6 +76,7 @@ "Không tìm thấy kênh nào" "Quá trình quét không tìm thấy bất kỳ kênh nào. Hãy xác minh rằng TV của bạn đã được kết nối với nguồn tín hiệu TV.\n\nNếu sử dụng ăng-ten không dây, hãy điều chỉnh vị trí hoặc hướng của ăng-ten đó. Để có kết quả tốt nhất, hãy đặt ăng-ten lên cao và gần cửa sổ rồi quét lại." "Quá trình quét không tìm thấy bất kỳ kênh nào. Hãy xác minh rằng bộ dò USB đã được cắm và kết nối với nguồn tín hiệu TV.\n\nNếu sử dụng ăng-ten không dây, hãy điều chỉnh vị trí hoặc hướng của ăng-ten đó. Để có kết quả tốt nhất, hãy đặt ăng-ten lên cao và gần cửa sổ rồi quét lại." + "Quá trình quét không tìm thấy bất kỳ kênh nào. Hãy xác minh là bộ dò mạng đã được bật nguồn và kết nối với một nguồn tín hiệu TV.\n\nNếu sử dụng ăng-ten không dây, hãy điều chỉnh vị trí hoặc hướng của ăng-ten. Để có kết quả tốt nhất, hãy đặt ăng-ten ở vị trí cao và gần cửa sổ và quét lại." "Quét lại" "Xong" @@ -83,5 +84,7 @@ "Quét tìm các kênh TV" "Thiết lập bộ dò TV" "Thiết lập bộ dò TV USB." - "Đã ngắt kết nối bộ điều chỉnh TV USB." + "Thiết lập bộ dò TV mạng" + "Đã ngắt kết nối bộ dò truyền hình USB." + "Đã ngắt kết nối bộ dò mạng." diff --git a/usbtuner-res/values-zh-rCN/strings.xml b/usbtuner-res/values-zh-rCN/strings.xml index a4f6da22..546cfb5b 100644 --- a/usbtuner-res/values-zh-rCN/strings.xml +++ b/usbtuner-res/values-zh-rCN/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "电视调谐器" "USB 电视调谐器" - "开启" - "关闭" + "网络电视调谐器(测试版)" "请耐心等待处理完毕" - "选择您的频道来源" - "无信号" - "无法调到%s" - "无法调到相应频道" "调谐器软件近期已更新。请重新扫描频道。" "在系统声音设置中启用环绕声即可启用音频" + "无法播放音频,请试试其他电视频道" "频道调谐器设置" "电视调谐器设置" "USB 频道调谐器设置" + "网络调谐器设置" "请检查您的电视是否已连接到电视信号源。\n\n如果您使用的是无线电视,则可能需要调节天线的位置或方向,以便接收尽可能多的频道。要获得最佳效果,请将天线的位置调高并靠近窗户。" "请检查 USB 调谐器是否已插好并连接到电视信号源。\n\n如果您使用的是无线电视,则可能需要调节天线的位置或方向,以便接收尽可能多的频道。要获得最佳效果,请将天线的位置调高并靠近窗户。" + "请检查网络调谐器是否已接通电源并连接到电视信号源。\n\n如果您使用的是无线电视,则可能需要调节天线的位置或方向,以便接收尽可能多的频道。要获得最佳效果,请将天线的位置调高并靠近窗户。" "继续" "以后再说" @@ -40,6 +38,7 @@ "要重新进行频道设置吗?" "此操作将移除通过电视调谐器找到的频道,并重新扫描新频道。\n\n请检查您的电视是否已连接到电视信号源。\n\n如果您使用的是无线电视,则可能需要调节天线的位置或方向,以便接收尽可能多的频道。要获得最佳效果,请将天线的位置调高并靠近窗户。" "此操作将移除通过 USB 调谐器找到的频道,并重新扫描新频道。\n\n请检查 USB 调谐器是否已插好并连接到电视信号源。\n\n如果您使用的是无线电视,则可能需要调节天线的位置或方向,以便接收尽可能多的频道。要获得最佳效果,请将天线的位置调高并靠近窗户。" + "此操作将移除通过网络调谐器找到的频道,并重新扫描新频道。\n\n请检查网络调谐器是否已接通电源并连接到电视信号源。\n\n如果您使用的是无线电视,则可能需要调节天线的位置或方向,以便接收尽可能多的频道。要获得最佳效果,请将天线的位置调高并靠近窗户。" "继续" "取消" @@ -54,6 +53,7 @@ "电视调谐器设置" "USB 频道调谐器设置" + "网络频道调谐器设置" "此过程可能需要几分钟时间" "调谐器暂时无法使用或已用于录制。" @@ -76,6 +76,7 @@ "未找到任何频道" "扫描后未找到任何频道。请检查您的电视是否已连接到电视信号源。\n\n如果您使用的是无线电视,请调节天线的位置或方向。要获得最佳效果,请将天线的位置调高并靠近窗户,然后再扫描一次。" "扫描后未找到任何频道。请检查 USB 调谐器是否已插好并连接到电视信号源。\n\n如果您使用的是无线电视,请调节天线的位置或方向。要获得最佳效果,请将天线的位置调高并靠近窗户,然后再扫描一次。" + "扫描后未找到任何频道。请检查网络调谐器是否已接通电源并连接到电视信号源。\n\n如果您使用的是无线电视,请调节天线的位置或方向。要获得最佳效果,请将天线的位置调高并靠近窗户,然后再扫描一次。" "重新扫描" "完成" @@ -83,5 +84,7 @@ "扫描电视频道" "电视调谐器设置" "USB 电视调谐器设置" - "USB 电视调谐器已断开连接。" + "网络电视调谐器设置" + "USB 电视调谐器已断开连接。" + "网络调谐器已断开连接。" diff --git a/usbtuner-res/values-zh-rHK/strings.xml b/usbtuner-res/values-zh-rHK/strings.xml index 6d40401e..84a04685 100644 --- a/usbtuner-res/values-zh-rHK/strings.xml +++ b/usbtuner-res/values-zh-rHK/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "電視調諧器" "USB 電視調諧器" - "開啟" - "關閉" + "網絡電視調諧器 (測試版)" "請等待系統完成處理程序" - "請選擇您的頻道來源" - "無訊號" - "無法調校至%s" - "無法調校頻道" "調諧器軟件最近已更新。請重新掃瞄頻道。" "在系統音效設定中啟用環迴立體聲功能即可啟用音效" + "無法播放音效,請嘗試使用其他電視頻道" "頻道調諧器設定" "電視調諧器設定" "USB 頻道調諧器設定" + "網絡調諧器設定" "請確定電視已連接至電視訊號來源。\n\n如果您使用無線天線,可能需要調整天線的位置或方向,以接收最多頻道。您亦可將天線放在較高位置並靠近窗戶,以獲得最佳效果。" "請確定 USB 調諧器已接駁並連接至電視訊號來源。\n\n如果您使用無線天線,可能需要調整天線的位置或方向,以接收最多頻道。您亦可以將天線放在較高位置並靠近窗戶,以獲取最佳效果。" + "請確定網絡調諧器已開啟電源,並連接至電視訊號來源。\n\n如果您使用無線天線,可能需要調整天線的位置或方向,以接收最多頻道。要取得最佳效果,請將天線放在較高位置並靠近窗戶,然後重新掃瞄。" "繼續" "暫時不要" @@ -40,6 +38,7 @@ "要重新設定頻道嗎?" "這項操作將移除電視調諧器找到的頻道,並重新掃瞄新的頻道。\n\n請確定電視已連接至電視訊號來源。\n\n如果您使用無線天線,可能需要調整天線的位置或方向,以接收最多頻道。您亦可將天線放在較高位置並靠近窗戶,以獲得最佳效果。" "這項操作將移除 USB 調諧器找到的頻道,並會重新掃瞄新頻道。\n\n請確定 USB 調諧器已接駁並連接至電視訊號來源。\n\n如果您使用無線天線,可能需要調整天線的位置或方向,以接收最多頻道。您亦可將天線放在較高位置並靠近窗戶,以獲取最佳效果。" + "這項操作將移除網絡調諧器找到的頻道,並再次掃瞄新頻道。\n\n請確定網絡調諧器已開啟電源並連接至電視訊號來源。\n\n如果您使用無線天線,請調整天線的位置或方向。要取得最佳效果,請將天線放在較高位置並靠近窗戶,然後重新掃瞄。" "繼續" "取消" @@ -54,6 +53,7 @@ "電視調諧器設定" "USB 頻道調諧器設定" + "網絡頻道調諧器設定" "可能需時數分鐘" "調諧器暫時無法使用,或已用於錄影。" @@ -76,6 +76,7 @@ "找不到頻道" "掃瞄後找不到頻道。請確定電視已連接至電視訊號來源。\n\n如果您使用無線天線,請調整天線的位置或方向。您亦可將天線放在較高位置並靠近窗戶,然後重新掃瞄,以獲得最佳效果。" "掃瞄找不到頻道。請確定 USB 調諧器已接駁並連接至電視訊號來源。\n\n如果您使用無線天線,請調整天線的位置或方向。您亦可將天線放在較高位置並靠近窗戶,然後重新掃瞄,以獲取最佳效果。" + "掃瞄後找不到任何頻道。請確定網絡調諧器已開啟電源,並連接至電視訊號來源。\n\n如果您使用無線天線,請調整天線的位置或方向。要取得最佳效果,請將天線放在較高位置並靠近窗戶,然後重新掃瞄。" "重新掃瞄" "完成" @@ -83,5 +84,7 @@ "掃瞄電視頻道" "電視調諧器設定" "USB 電視調諧器設定" - "已解除 USB 電視調諧器的連接。" + "網絡電視調諧器設定" + "USB 電視調諧器已中斷連線。" + "網絡調諧器已中斷連線。" diff --git a/usbtuner-res/values-zh-rTW/strings.xml b/usbtuner-res/values-zh-rTW/strings.xml index 802d8b73..9f46c3ef 100644 --- a/usbtuner-res/values-zh-rTW/strings.xml +++ b/usbtuner-res/values-zh-rTW/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "電視調諧器" "USB 電視調諧器" - "開啟" - "關閉" + "網路電視調諧器 (測試版)" "請等待處理程序完成" - "選取你的頻道來源" - "無訊號" - "無法轉到「%s」" - "無法轉台" "調諧器軟體最近已更新。請重新掃描頻道。" "前往系統音效設定開啟環繞音效即可啟用音訊" + "無法播放音訊,請改用其他電視頻道" "頻道調諧器設定" "電視調諧器設定" "USB 頻道調諧器設定" + "網路調諧器設定" "請確認你的電視已連接到電視訊號來源。\n\n如果你使用無線電視天線,可能需要調整天線的位置和方向,以便接收最多頻道。為達最佳效果,請將天線放在靠近窗戶的較高位置。" "請確認 USB 調諧器已插入並連接到電視訊號來源。\n\n如果你使用無線電視天線,可能需要調整天線的位置和方向,以便接收最多頻道。為達最佳效果,請將天線放在靠近窗戶的較高位置。" + "請確認你的網路調諧器已開啟電源,並連接到電視訊號來源。\n\n如果你使用無線電視天線,可能需要調整天線的位置和方向,以便接收最多頻道。為達最佳效果,請將天線放在靠近窗戶的較高位置。" "繼續" "暫時不要" @@ -40,6 +38,7 @@ "要重新設定頻道嗎?" "這項動作將移除電視調諧器找到的頻道,並再次掃描新的頻道。\n\n請確認你的電視已連接到電視訊號來源。\n\n如果你使用無線電視天線,可能需要調整天線的位置和方向,以便接收最多頻道。為達最佳效果,請將天線放在靠近窗戶的較高位置。" "這項動作將移除 USB 調諧器找到的頻道,並再次掃描新的頻道。\n\n請確認 USB 調諧器已插入並連接到電視訊號來源。\n\n如果你使用無線電視天線,可能需要調整天線的位置和方向,以便接收最多頻道。為達最佳效果,請將天線放在靠近窗戶的較高位置。" + "這個動作將移除網路調諧器找到的頻道,並再次掃描新的頻道。\n\n請確認你的網路調諧器已開啟電源,並連接到電視訊號來源。\n\n如果你使用無線電視天線,可能需要調整天線的位置和方向,以便接收最多頻道。為達最佳效果,請將天線放在靠近窗戶的較高位置。" "繼續" "取消" @@ -54,6 +53,7 @@ "電視調諧器設定" "USB 頻道調諧器設定" + "網路頻道調諧器設定" "這可能需要幾分鐘的時間" "調諧器暫時無法使用,或是已用於錄製。" @@ -76,6 +76,7 @@ "找不到任何頻道" "掃描後並未發現任何頻道。請確認你的電視已連接到電視訊號來源。\n\n如果你使用無線電視天線,請調整天線的位置和方向。為達最佳效果,請將天線放在靠近窗戶的較高位置,然後再掃描一次。" "掃描後並未發現任何頻道。請確認 USB 調諧器已插入並連接到電視訊號來源。\n\n如果你使用無線電視天線,請調整天線的位置和方向。為達最佳效果,請將天線放在靠近窗戶的較高位置,然後再掃描一次。" + "掃描後並未發現任何頻道。請確認你的網路調諧器已開啟電源,並連接到電視訊號來源。\n\n如果你使用無線電視天線,請調整天線的位置和方向。為達最佳效果,請將天線放在靠近窗戶的較高位置,然後再掃描一次。" "重新掃描" "完成" @@ -83,5 +84,7 @@ "掃描電視頻道" "電視調諧器設定" "USB 電視調諧器設定" - "USB 電視調諧器已中斷連結。" + "網路電視調諧器設定" + "USB 電視調諧器已中斷連線。" + "網路調諧器已中斷連線。" diff --git a/usbtuner-res/values-zu/strings.xml b/usbtuner-res/values-zu/strings.xml index 905e9a16..ddeb0faa 100644 --- a/usbtuner-res/values-zu/strings.xml +++ b/usbtuner-res/values-zu/strings.xml @@ -19,20 +19,18 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "Ishuna ye-TV" "Ishuna ye-USB TV" - "Vuliwe" - "Valiwe" + "Ishuna yenethiwekhi ye-TV (i-BETA)" "Sicela ulinde ukuze uqede ukucubungula" - "Khetha umthombo wesiteshi sakho" - "Ayikho isignali" - "Yehlulekile ukushunela ku-%s" - "Yehlulekile ukushuna" "Isofthiwe yeshuna ibuyekezwe kamuva. Sicela uphinde uskene iziteshi." "Nika amandla umsindo ozungezile kuzilungiselelo zomsindo wesistimu" + "Ayikwazi ukudlala umsindo. Sicela uzame enye i-TV" "Ukusethwa kweshuna yesiteshi" "Ukusethwa kweshuna ye-TV" "Ukusetha kweshuna yesiteshi se-USB" + "Ukusethwa kweshuna yenethiwekhi" "Qinisekisa ukuthi i-TV yakho ixhunywe kumthombo wesignali ye-TV.\n\nUma usebenzisa i-antenna esemoyeni, kungenzeka ukuthi kumele ulungise ukubekwa noma ukubheka ukuze uthole iziteshi eziningi. Ukuze uthole imiphumela ehamba phambili, beka phezulu naseduze kwewindi." "Qinisekisa ukuthi ishuna ye-USB ixhunyiwe futhi ixhumeke kumthombo wesignali ye-TV.\n\nUma usebenzisa i-antenna esemoyeni, kuzomele ulungise ukubekwa kwayo noma ukubheka kwayo ukuze uthole iziteshi eziningi. Ukuze uthole imiphumela ehamba phambili, yibeke phezulu eduze kwewindi." + "Qinisekisa ukuthi ishuna yenethiwekhi ivuliwe yaphinde yaxhunywa kumthombo wesignali ye-TV.\n\nUma usebenzisa i-antenna esemoyeni, kungenzeka ukuthi kumele ulungise ukubekwa kwayo noma indawo ebhekiwe ukuze uthole iziteshi eziningi. Ukuze uthole imiphumela ehamba phambili, ibeke ngaphezulu naseduze kwewindi." "Qhubeka" "Hhayi manje" @@ -40,6 +38,7 @@ "Phinda uqalise ukusethwa kwesiteshi?" "Lokhu kuzosusa iziteshi ezitholwe kusukela kushuna ye-TV kuphinde kuskenele iziteshi ezintsha.\n\nQinisekisa ukuthi i-TV yakho ixhunywe kumthombo wesignali ye-TV.\n\nUma usebenzisa i-antenna esemoyeni, kungenzeka ukuthi kumele ulungise ukubekwa noma ukubheka ukuze uthole iziteshi eziningi. Ukuze uthole imiphumela ehamba phambili, beka phezulu naseduze kwewindi." "Lokhu kuzosusa iziteshi ezitholakele kusukela kushuna ye-USB kuphinde kuskenele iziteshi ezintsha futhi.\n\nQinisekisa ukuthi ishuna ye-USB ixhunyiwe futhi ixhumeke kumthombo wesignali ye-TV.\n\nUma usebenzisa i-antenna esemoyeni, kuzomele ulungise ukubekwa kwayo noma ukubheka kwayo ukuze uthole iziteshi eziningi. Ukuze uthole imiphumela ehamba phambili, yibeke ngaphezulu naseduze kwewindi." + "Lokhu kuzosusa iziteshi ezitholakele kusukela kushuna yenethiwekhi kuphinde kuskenele iziteshi ezintsha futhi.\n\nQinisekisa ukuthi ishuna yenethiwekhi ixhunyiwe futhi ixhumeke kumthombo wesignali ye-TV.\n\nUma usebenzisa i-antenna esemoyeni, kuzomele ulungise ukubekwa kwayo noma ukubheka kwayo ukuze uthole iziteshi eziningi. Ukuze uthole imiphumela ehamba phambili, yibeke ngaphezulu naseduze kwewindi." "Qhubeka" "Khansela" @@ -54,6 +53,7 @@ "Ukusethwa kweshuna ye-TV" "Ukusetha kweshuna yesiteshi se-USB" + "Ukusethwa kweshuna yesiteshi senethiwekhi" "Lokhu kungathatha amaminithi athile" "Ishuna okwamanje ayitholakali noma isivele isetshenziswa ngokurekhodwa." @@ -76,6 +76,7 @@ "Azikho iziteshi ezitholiwe" "Ukuskena akuzange kuthole iziteshi. Qinisekisa ukuthi i-TV yakho ixhumeke kumthombo wesignali we-TV.\n\nUma usebenzisa i-antenna esemoyeni, kungenzeka ukuthi kumele ulungise ukubekwa noma ukubheka ukuze uthole iziteshi eziningi. Ukuze uthole imiphumela ehamba phambili, beka phezulu naseduze kwewindi." "Iskena asizange sithole noma yiziphi iziteshi. Qinisekisa ukuthi ishuna ye-USB ixhunyiwe futhi ixhumeke kumthombo wesignali ye-TV.\n\nUma usebenzisa i-antenna esemoyeni, lungisa ukubekwa kwayo noma ukuma kwayo. Ukuze uthole imiphumela ehamba phambili, yibeke phezulu naseduze kwewindi uphinde uskene." + "Iskena asizange sithole noma iziphi iziteshi. Qinisekisa ukuthi ishuna ye-USB ixhunyiwe futhi ixhumeke kumthombo wesignali ye-TV.\n\nUma usebenzisa i-antenna esemoyeni, lungisa ukubekwa kwayo noma ukuma kwayo. Ukuze uthole imiphumela ehamba phambili, yibeke phezulu naseduze kwewindi uphinde uskene." "Skena futhi" "Kwenziwe" @@ -83,5 +84,7 @@ "Skenela iziteshi ze-TV" "Ukusethwa kweshuna ye-TV" "Ukusetha kweshuna ye-USB TV" - "Isishuni se-USB TV sinqanyuliwe." + "Ukusethwa kweshuna yenethiwekhi ye-TV" + "Ishuna ye-USB TV inqanyuliwe." + "Ishuna yenethiwekhi inqanyuliwe." diff --git a/usbtuner-res/values/colors.xml b/usbtuner-res/values/colors.xml index bbcc431f..873ecc8e 100644 --- a/usbtuner-res/values/colors.xml +++ b/usbtuner-res/values/colors.xml @@ -16,15 +16,9 @@ --> - #01579B - #EEEEEE - #EEEEEE - #B3EEEEEE - #EEEEEE #EEEEEE #B3EEEEEE #EEEEEE #26EEEEEE #EEEEEE - #26EEEEEE \ No newline at end of file diff --git a/usbtuner-res/values/dimens.xml b/usbtuner-res/values/dimens.xml index 0a09b062..1fe39f6c 100644 --- a/usbtuner-res/values/dimens.xml +++ b/usbtuner-res/values/dimens.xml @@ -16,41 +16,7 @@ --> - - 16dp - 16dp - - 592dp - 368dp - - 100dp - -100dp - - 56dp - 32dp - 27dp - 156dp - - 34sp - 14sp - 14sp - 8dp - - 24dp - 40dp - 27dp - 48dp - 163dp - 48dp - - 48dp - 16dp - 16dp - 14dp - 15dp - 80dp - 80dp 24dp 34sp diff --git a/usbtuner-res/values/integers.xml b/usbtuner-res/values/integers.xml index 21b438e9..65d20c67 100644 --- a/usbtuner-res/values/integers.xml +++ b/usbtuner-res/values/integers.xml @@ -16,10 +16,5 @@ --> - 2 - 10 - 8 2 - 300 - 300 \ No newline at end of file diff --git a/usbtuner-res/values/strings.xml b/usbtuner-res/values/strings.xml index e3a8586d..2b08a354 100644 --- a/usbtuner-res/values/strings.xml +++ b/usbtuner-res/values/strings.xml @@ -18,26 +18,15 @@ TV Tuner - USB TV Tuner - - On - - Off + + Network TV Tuner (BETA) Please wait to finish processing - - Select your channel source - - No Signal - - Failed to tune to %s - - Failed to tune Tuner software has been recently updated. Please re-scan the @@ -45,12 +34,16 @@ Enable surround sound in system sound settings to enable audio + + Cannot play audio. Please try another TV Channel tuner setup TV Tuner setup USB channel tuner setup + + Network tuner setup Verify your TV is connected to a TV signal source.\n\nIf using an over-the-air antenna, you may need to adjust its placement or @@ -61,6 +54,11 @@ to a TV signal source.\n\nIf using an over-the-air antenna, you may need to adjust its placement or direction to receive the most channels. For best results, place it high and near a window. + + Verify the network tuner is powered on and connected + to a TV signal source.\n\nIf using an over-the-air antenna, you may need to adjust its + placement or direction to receive the most channels. For best results, + place it high and near a window. @@ -82,6 +80,12 @@ to a TV signal source.\n\nIf using an over-the-air antenna, you may need to adjust its placement or direction to receive the most channels. For best results, place it high and near a window. + + This will remove the channels found from the network + tuner and scan for new channels again.\n\nVerify the network tuner is powered on and + connected to a TV signal source.\n\nIf using an over-the-air antenna, you may need to adjust + its placement or direction to receive the most channels. For best results, place it high and + near a window. @@ -109,6 +113,8 @@ TV tuner setup USB channel tuner setup + + Network channel tuner setup This may take several minutes @@ -145,11 +151,17 @@ your TV is connected to a TV signal source.\n\nIf using an over-the-air antenna, adjust its placement or direction. For best results, place it high and near a window and scan again. - + The scan did not find any channels. Verify the USB tuner is plugged in and connected to a TV signal source.\n\nIf using an over-the-air antenna, adjust its placement or direction. For best results, place it high and near a window and scan again. + + The scan did not find any channels. Verify the + network tuner is powered on and connected to a TV signal source.\n\nIf using an over-the-air + antenna, adjust its placement or direction. For best results, place it high and near a + window and scan again. Scan again @@ -167,7 +179,12 @@ USB TV Tuner setup + + Network TV Tuner setup - - USB TV tuner disconnected. + + USB TV tuner disconnected. + + Network tuner disconnected. diff --git a/usbtuner-res/values/styles.xml b/usbtuner-res/values/styles.xml deleted file mode 100644 index c79319ba..00000000 --- a/usbtuner-res/values/styles.xml +++ /dev/null @@ -1,120 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/version.mk b/version.mk index f63f5834..6db8c732 100644 --- a/version.mk +++ b/version.mk @@ -48,13 +48,13 @@ base_version_major := 1 # Change this for each branch -base_version_minor := 11 +base_version_minor := 13 # code_version_major will overflow at 22 code_version_major := $(shell echo $$(($(base_version_major)+3))) # x86 and arm sometimes don't match. -code_version_build := 011 +code_version_build := 016 ##################################################### ##################################################### # Collect automatic version code parameters -- cgit v1.2.3