aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorXin Li <delphij@google.com>2019-07-01 21:00:07 +0000
committerXin Li <delphij@google.com>2019-07-01 21:00:07 +0000
commiteffbc99d77bb465246a80f96b2c0128885783f0b (patch)
treef4ba89447e2d7ac3355508e583a5cf0574c29af5
parentc8b794291c5b8b5be03d740355ab33f2df79a76e (diff)
parentd12bfa398c56027290a9e6e4fd14f635458ec581 (diff)
downloadTV-temp_140451723.tar.gz
DO NOT MERGE - Merge qt-dev-plus-aosp-without-vendor (5699924) into stage-aosp-mastertemp_140451723
Bug: 134405016 Change-Id: If83c4123ed715e08bc37b8f5772adc84e86298af
-rw-r--r--Android.bp96
-rw-r--r--Android.mk102
-rw-r--r--AndroidManifest.xml120
-rw-r--r--README.md36
-rw-r--r--ResourceManifest.xml22
-rw-r--r--build.gradle99
-rw-r--r--common/Android.bp61
-rw-r--r--common/Android.mk31
-rw-r--r--common/AndroidManifest.xml2
-rw-r--r--common/build.gradle129
-rw-r--r--common/src/com/android/tv/common/BaseApplication.java34
-rw-r--r--common/src/com/android/tv/common/BaseSingletons.java13
-rw-r--r--common/src/com/android/tv/common/BuildConfig.java26
-rw-r--r--common/src/com/android/tv/common/CommonConstants.java10
-rw-r--r--common/src/com/android/tv/common/TvContentRatingCache.java46
-rw-r--r--common/src/com/android/tv/common/buildtype/AospBuildTypeProvider.java25
-rw-r--r--common/src/com/android/tv/common/buildtype/EngBuildTypeProvider.java25
-rw-r--r--common/src/com/android/tv/common/buildtype/HasBuildType.java34
-rw-r--r--common/src/com/android/tv/common/buildtype/NoJniTestBuildTypeProvider.java25
-rw-r--r--common/src/com/android/tv/common/buildtype/ProdBuildTypeProvider.java25
-rw-r--r--common/src/com/android/tv/common/compat/README.md7
-rw-r--r--common/src/com/android/tv/common/compat/RecordingSessionCompat.java69
-rw-r--r--common/src/com/android/tv/common/compat/TisSessionCompat.java77
-rw-r--r--common/src/com/android/tv/common/compat/TvInputConstantCompat.java46
-rw-r--r--common/src/com/android/tv/common/compat/TvInputInfoCompat.java117
-rw-r--r--common/src/com/android/tv/common/compat/TvRecordingClientCompat.java101
-rw-r--r--common/src/com/android/tv/common/compat/TvViewCompat.java111
-rw-r--r--common/src/com/android/tv/common/compat/api/PrivateCommandSender.java24
-rw-r--r--common/src/com/android/tv/common/compat/api/RecordingClientCallbackCompatEvents.java27
-rw-r--r--common/src/com/android/tv/common/compat/api/RecordingSessionCompatCommands.java22
-rw-r--r--common/src/com/android/tv/common/compat/api/RecordingSessionCompatEvents.java24
-rw-r--r--common/src/com/android/tv/common/compat/api/SessionCompatCommands.java22
-rw-r--r--common/src/com/android/tv/common/compat/api/SessionCompatEvents.java24
-rw-r--r--common/src/com/android/tv/common/compat/api/SessionEventNotifier.java24
-rw-r--r--common/src/com/android/tv/common/compat/api/TvInputCallbackCompatEvents.java26
-rw-r--r--common/src/com/android/tv/common/compat/api/TvRecordingClientCompatCommands.java22
-rw-r--r--common/src/com/android/tv/common/compat/api/TvViewCompatCommands.java22
-rw-r--r--common/src/com/android/tv/common/compat/internal/Constants.java28
-rw-r--r--common/src/com/android/tv/common/compat/internal/RecordingClientCompatProcessor.java85
-rw-r--r--common/src/com/android/tv/common/compat/internal/RecordingSessionCompatProcessor.java78
-rw-r--r--common/src/com/android/tv/common/compat/internal/SessionCompatProcessor.java75
-rw-r--r--common/src/com/android/tv/common/compat/internal/TifSessionCompatProcessor.java75
-rw-r--r--common/src/com/android/tv/common/compat/internal/TvViewCompatProcessor.java90
-rw-r--r--common/src/com/android/tv/common/compat/internal/ViewCompatProcessor.java90
-rw-r--r--common/src/com/android/tv/common/compat/internal/recording_commands.proto39
-rw-r--r--common/src/com/android/tv/common/compat/internal/recording_events.proto49
-rw-r--r--common/src/com/android/tv/common/compat/internal/tif_commands.proto39
-rw-r--r--common/src/com/android/tv/common/compat/internal/tif_events.proto47
-rw-r--r--common/src/com/android/tv/common/config/DefaultConfigManager.java60
-rw-r--r--common/src/com/android/tv/common/config/RemoteConfigFeature.java42
-rw-r--r--common/src/com/android/tv/common/config/api/RemoteConfig.java54
-rw-r--r--common/src/com/android/tv/common/config/api/RemoteConfigValue.java48
-rw-r--r--common/src/com/android/tv/common/dagger/ApplicationModule.java60
-rw-r--r--common/src/com/android/tv/common/dagger/annotations/ApplicationContext.java22
-rw-r--r--common/src/com/android/tv/common/dagger/annotations/MainLooper.java (renamed from src/com/android/tv/util/Filter.java)13
-rw-r--r--common/src/com/android/tv/common/data/RecordedProgramState.java14
-rw-r--r--common/src/com/android/tv/common/experiments/ExperimentFlag.java29
-rw-r--r--common/src/com/android/tv/common/experiments/Experiments.java9
-rw-r--r--common/src/com/android/tv/common/feature/BuildTypeFeature.java (renamed from common/src/com/android/tv/common/feature/EngOnlyFeature.java)15
-rw-r--r--common/src/com/android/tv/common/feature/CommonFeatures.java74
-rw-r--r--common/src/com/android/tv/common/feature/FeatureUtils.java41
-rw-r--r--common/src/com/android/tv/common/feature/FlagFeature.java (renamed from common/src/com/android/tv/common/feature/GServiceFeature.java)31
-rw-r--r--common/src/com/android/tv/common/feature/Sdk.java40
-rw-r--r--common/src/com/android/tv/common/flags/BackendKnobsFlags.java46
-rwxr-xr-xcommon/src/com/android/tv/common/flags/CloudEpgFlags.java34
-rwxr-xr-xcommon/src/com/android/tv/common/flags/ConcurrentDvrPlaybackFlags.java34
-rwxr-xr-xcommon/src/com/android/tv/common/flags/TunerFlags.java34
-rwxr-xr-xcommon/src/com/android/tv/common/flags/UiFlags.java41
-rw-r--r--common/src/com/android/tv/common/flags/has/HasCloudEpgFlags.java29
-rw-r--r--common/src/com/android/tv/common/flags/has/HasConcurrentDvrPlaybackFlags.java30
-rw-r--r--common/src/com/android/tv/common/flags/has/HasUiFlags.java24
-rw-r--r--common/src/com/android/tv/common/flags/has/HasUtils.java30
-rw-r--r--common/src/com/android/tv/common/flags/impl/DefaultBackendKnobsFlags.java56
-rw-r--r--common/src/com/android/tv/common/flags/impl/DefaultCloudEpgFlags.java46
-rw-r--r--common/src/com/android/tv/common/flags/impl/DefaultConcurrentDvrPlaybackFlags.java37
-rw-r--r--common/src/com/android/tv/common/flags/impl/DefaultFlagsModule.java60
-rw-r--r--common/src/com/android/tv/common/flags/impl/DefaultTunerFlags.java37
-rw-r--r--common/src/com/android/tv/common/flags/impl/DefaultUiFlags.java42
-rw-r--r--common/src/com/android/tv/common/recording/RecordingStorageStatusManager.java3
-rw-r--r--common/src/com/android/tv/common/singletons/HasSingletons.java33
-rw-r--r--common/src/com/android/tv/common/singletons/HasTvInputId.java27
-rw-r--r--common/src/com/android/tv/common/support/README.md8
-rw-r--r--common/src/com/android/tv/common/support/tis/BaseTvInputService.java90
-rw-r--r--common/src/com/android/tv/common/support/tis/SessionManager.java31
-rw-r--r--common/src/com/android/tv/common/support/tis/SimpleSessionManager.java62
-rw-r--r--common/src/com/android/tv/common/support/tis/TifSession.java203
-rw-r--r--common/src/com/android/tv/common/support/tis/WrappedSession.java148
-rw-r--r--common/src/com/android/tv/common/ui/setup/SetupActivity.java23
-rw-r--r--common/src/com/android/tv/common/util/CommonUtils.java19
-rw-r--r--common/src/com/android/tv/common/util/LocationUtils.java50
-rw-r--r--common/src/com/android/tv/common/util/NetworkTrafficTags.java23
-rw-r--r--common/src/com/android/tv/common/util/PermissionUtils.java5
-rw-r--r--common/src/com/android/tv/common/util/StringUtils.java5
-rw-r--r--common/src/com/android/tv/common/util/SystemProperties.java4
-rw-r--r--[-rwxr-xr-x]gradle.properties (renamed from jni/gen_jni.sh)14
-rw-r--r--jni/Android.bp30
-rw-r--r--jni/Android.mk30
-rw-r--r--jni/DvbManager.cpp31
-rw-r--r--jni/DvbManager.h3
-rw-r--r--jni/tunertvinput_jni.cpp17
-rwxr-xr-x[-rw-r--r--]jni/tunertvinput_jni.h100
-rw-r--r--libs/Android.bp165
-rw-r--r--libs/auto-factory-1.0-beta2.jarbin0 -> 56167 bytes
-rw-r--r--libs/auto-value-1.5.2.jarbin0 -> 1611730 bytes
-rw-r--r--libs/dagger-2.15.jarbin0 -> 39406 bytes
-rw-r--r--libs/dagger-android-2.15.aarbin0 -> 23228 bytes
-rw-r--r--libs/dagger-android-jarimpl-2.15.jarbin0 -> 28057 bytes
-rw-r--r--libs/dagger-android-processor-2.15.jarbin0 -> 111197 bytes
-rw-r--r--libs/dagger-android-support-2.15.aarbin0 -> 19089 bytes
-rw-r--r--libs/dagger-android-support-jarimpl-2.15.jarbin0 -> 11346 bytes
-rw-r--r--libs/dagger-compiler-2.15.jarbin0 -> 882144 bytes
-rw-r--r--libs/dagger-producers-2.15.jarbin0 -> 71006 bytes
-rw-r--r--libs/dagger-spi-2.15.jarbin0 -> 125799 bytes
-rw-r--r--libs/error_prone_annotations-2.3.1.jarbin0 -> 13162 bytes
-rw-r--r--libs/exoplayer-core-2-SNAPHOT-20180114.aarbin1101772 -> 0 bytes
-rw-r--r--libs/exoplayer-core-2.9.0.aarbin0 -> 1353252 bytes
-rw-r--r--libs/google-java-format-1.4-all-deps.jarbin0 -> 5437064 bytes
-rw-r--r--libs/guava-23.3-jre.jarbin0 -> 2655564 bytes
-rw-r--r--libs/guava-23.5-jre.jarbin0 -> 2658862 bytes
-rw-r--r--libs/guava-23.6-android.jarbin0 -> 2588045 bytes
-rw-r--r--libs/javapoet-1.8.0.jarbin0 -> 92125 bytes
-rw-r--r--libs/javawriter-2.5.1.jarbin0 -> 14012 bytes
-rw-r--r--libs/javax.annotation-api-1.2.jarbin0 -> 26366 bytes
-rw-r--r--libs/truth-0.36.jarbin0 -> 164334 bytes
-rw-r--r--material_res/drawable-hdpi/quantum_ic_arrow_downward_white_36.pngbin0 -> 232 bytes
-rw-r--r--material_res/drawable-hdpi/quantum_ic_arrow_upward_white_36.pngbin0 -> 239 bytes
-rw-r--r--material_res/drawable-hdpi/quantum_ic_check_circle_white_48.pngbin0 -> 628 bytes
-rw-r--r--material_res/drawable-hdpi/quantum_ic_developer_mode_tv_white_48.pngbin0 -> 504 bytes
-rw-r--r--material_res/drawable-hdpi/quantum_ic_error_white_48.pngbin0 -> 614 bytes
-rw-r--r--material_res/drawable-hdpi/quantum_ic_signal_cellular_0_bar_white_24.pngbin0 -> 166 bytes
-rw-r--r--material_res/drawable-hdpi/quantum_ic_signal_cellular_1_bar_white_24.pngbin0 -> 162 bytes
-rw-r--r--material_res/drawable-hdpi/quantum_ic_signal_cellular_2_bar_white_24.pngbin0 -> 162 bytes
-rw-r--r--material_res/drawable-hdpi/quantum_ic_signal_cellular_3_bar_white_24.pngbin0 -> 162 bytes
-rw-r--r--material_res/drawable-hdpi/quantum_ic_signal_cellular_4_bar_white_24.pngbin0 -> 157 bytes
-rw-r--r--material_res/drawable-hdpi/quantum_ic_warning_white_18.pngbin0 -> 276 bytes
-rw-r--r--material_res/drawable-hdpi/quantum_ic_warning_white_96.pngbin0 -> 843 bytes
-rw-r--r--material_res/drawable-mdpi/quantum_ic_arrow_downward_white_36.pngbin0 -> 184 bytes
-rw-r--r--material_res/drawable-mdpi/quantum_ic_arrow_upward_white_36.pngbin0 -> 208 bytes
-rw-r--r--material_res/drawable-mdpi/quantum_ic_check_circle_white_48.pngbin0 -> 428 bytes
-rw-r--r--material_res/drawable-mdpi/quantum_ic_developer_mode_tv_white_48.pngbin0 -> 348 bytes
-rw-r--r--material_res/drawable-mdpi/quantum_ic_error_white_48.pngbin0 -> 431 bytes
-rw-r--r--material_res/drawable-mdpi/quantum_ic_signal_cellular_0_bar_white_24.pngbin0 -> 146 bytes
-rw-r--r--material_res/drawable-mdpi/quantum_ic_signal_cellular_1_bar_white_24.pngbin0 -> 141 bytes
-rw-r--r--material_res/drawable-mdpi/quantum_ic_signal_cellular_2_bar_white_24.pngbin0 -> 139 bytes
-rw-r--r--material_res/drawable-mdpi/quantum_ic_signal_cellular_3_bar_white_24.pngbin0 -> 139 bytes
-rw-r--r--material_res/drawable-mdpi/quantum_ic_signal_cellular_4_bar_white_24.pngbin0 -> 135 bytes
-rw-r--r--material_res/drawable-mdpi/quantum_ic_warning_white_18.pngbin0 -> 203 bytes
-rw-r--r--material_res/drawable-mdpi/quantum_ic_warning_white_96.pngbin0 -> 590 bytes
-rw-r--r--material_res/drawable-xhdpi/quantum_ic_arrow_downward_white_36.pngbin0 -> 276 bytes
-rw-r--r--material_res/drawable-xhdpi/quantum_ic_arrow_upward_white_36.pngbin0 -> 319 bytes
-rw-r--r--material_res/drawable-xhdpi/quantum_ic_check_circle_white_48.pngbin0 -> 872 bytes
-rw-r--r--material_res/drawable-xhdpi/quantum_ic_developer_mode_tv_white_48.pngbin0 -> 581 bytes
-rw-r--r--material_res/drawable-xhdpi/quantum_ic_error_white_48.pngbin0 -> 814 bytes
-rw-r--r--material_res/drawable-xhdpi/quantum_ic_signal_cellular_0_bar_white_24.pngbin0 -> 182 bytes
-rw-r--r--material_res/drawable-xhdpi/quantum_ic_signal_cellular_1_bar_white_24.pngbin0 -> 200 bytes
-rw-r--r--material_res/drawable-xhdpi/quantum_ic_signal_cellular_2_bar_white_24.pngbin0 -> 197 bytes
-rw-r--r--material_res/drawable-xhdpi/quantum_ic_signal_cellular_3_bar_white_24.pngbin0 -> 191 bytes
-rw-r--r--material_res/drawable-xhdpi/quantum_ic_signal_cellular_4_bar_white_24.pngbin0 -> 181 bytes
-rw-r--r--material_res/drawable-xhdpi/quantum_ic_warning_white_18.pngbin0 -> 325 bytes
-rw-r--r--material_res/drawable-xhdpi/quantum_ic_warning_white_96.pngbin0 -> 1044 bytes
-rw-r--r--material_res/drawable-xxhdpi/quantum_ic_arrow_downward_white_36.pngbin0 -> 402 bytes
-rw-r--r--material_res/drawable-xxhdpi/quantum_ic_arrow_upward_white_36.pngbin0 -> 390 bytes
-rw-r--r--material_res/drawable-xxhdpi/quantum_ic_check_circle_white_48.pngbin0 -> 1307 bytes
-rw-r--r--material_res/drawable-xxhdpi/quantum_ic_developer_mode_tv_white_48.pngbin0 -> 865 bytes
-rw-r--r--material_res/drawable-xxhdpi/quantum_ic_error_white_48.pngbin0 -> 1235 bytes
-rw-r--r--material_res/drawable-xxhdpi/quantum_ic_signal_cellular_0_bar_white_24.pngbin0 -> 218 bytes
-rw-r--r--material_res/drawable-xxhdpi/quantum_ic_signal_cellular_1_bar_white_24.pngbin0 -> 258 bytes
-rw-r--r--material_res/drawable-xxhdpi/quantum_ic_signal_cellular_2_bar_white_24.pngbin0 -> 253 bytes
-rw-r--r--material_res/drawable-xxhdpi/quantum_ic_signal_cellular_3_bar_white_24.pngbin0 -> 260 bytes
-rw-r--r--material_res/drawable-xxhdpi/quantum_ic_signal_cellular_4_bar_white_24.pngbin0 -> 217 bytes
-rw-r--r--material_res/drawable-xxhdpi/quantum_ic_warning_white_18.pngbin0 -> 436 bytes
-rw-r--r--material_res/drawable-xxhdpi/quantum_ic_warning_white_96.pngbin0 -> 1485 bytes
-rw-r--r--material_res/drawable-xxxhdpi/quantum_ic_arrow_downward_white_36.pngbin0 -> 491 bytes
-rw-r--r--material_res/drawable-xxxhdpi/quantum_ic_arrow_upward_white_36.pngbin0 -> 506 bytes
-rw-r--r--material_res/drawable-xxxhdpi/quantum_ic_check_circle_white_48.pngbin0 -> 1788 bytes
-rw-r--r--material_res/drawable-xxxhdpi/quantum_ic_developer_mode_tv_white_48.pngbin0 -> 1142 bytes
-rw-r--r--material_res/drawable-xxxhdpi/quantum_ic_error_white_48.pngbin0 -> 1687 bytes
-rw-r--r--material_res/drawable-xxxhdpi/quantum_ic_signal_cellular_0_bar_white_24.pngbin0 -> 250 bytes
-rw-r--r--material_res/drawable-xxxhdpi/quantum_ic_signal_cellular_1_bar_white_24.pngbin0 -> 296 bytes
-rw-r--r--material_res/drawable-xxxhdpi/quantum_ic_signal_cellular_2_bar_white_24.pngbin0 -> 296 bytes
-rw-r--r--material_res/drawable-xxxhdpi/quantum_ic_signal_cellular_3_bar_white_24.pngbin0 -> 293 bytes
-rw-r--r--material_res/drawable-xxxhdpi/quantum_ic_signal_cellular_4_bar_white_24.pngbin0 -> 249 bytes
-rw-r--r--material_res/drawable-xxxhdpi/quantum_ic_warning_white_18.pngbin0 -> 473 bytes
-rw-r--r--material_res/drawable-xxxhdpi/quantum_ic_warning_white_96.pngbin0 -> 1959 bytes
-rw-r--r--open_source_project.README2
-rw-r--r--partner_support/Android.bp32
-rw-r--r--partner_support/Android.mk23
-rw-r--r--partner_support/AndroidManifest.xml2
-rw-r--r--partner_support/g3doc/SeriesIdColumnForPartners.md30
-rw-r--r--partner_support/g3doc/TurnOffEmbeddedTuner.md15
-rw-r--r--partner_support/sample_customization/AndroidManifest.xml4
-rw-r--r--[-rwxr-xr-x]partner_support/sample_customization/res/mipmap-hdpi/ic_launcher.pngbin3925 -> 2815 bytes
-rw-r--r--[-rwxr-xr-x]partner_support/sample_customization/res/mipmap-mdpi/ic_launcher.pngbin2363 -> 1822 bytes
-rw-r--r--[-rwxr-xr-x]partner_support/sample_customization/res/mipmap-xhdpi/ic_launcher.pngbin5185 -> 3470 bytes
-rw-r--r--[-rwxr-xr-x]partner_support/sample_customization/res/mipmap-xxhdpi/ic_launcher.pngbin8427 -> 5402 bytes
-rw-r--r--[-rwxr-xr-x]partner_support/sample_customization/res/mipmap-xxxhdpi/ic_launcher.pngbin11531 -> 7199 bytes
-rw-r--r--partner_support/sample_customization/res/values/bools.xml2
-rw-r--r--partner_support/samples/Android.mk2
-rw-r--r--partner_support/samples/AndroidManifest.xml7
-rw-r--r--[-rwxr-xr-x]partner_support/samples/res/mipmap-hdpi/ic_launcher.pngbin3931 -> 2772 bytes
-rw-r--r--[-rwxr-xr-x]partner_support/samples/res/mipmap-mdpi/ic_launcher.pngbin2382 -> 1829 bytes
-rw-r--r--[-rwxr-xr-x]partner_support/samples/res/mipmap-xhdpi/ic_launcher.pngbin5121 -> 3466 bytes
-rw-r--r--[-rwxr-xr-x]partner_support/samples/res/mipmap-xxhdpi/ic_launcher.pngbin8222 -> 5261 bytes
-rw-r--r--[-rwxr-xr-x]partner_support/samples/res/mipmap-xxxhdpi/ic_launcher.pngbin11381 -> 7147 bytes
-rw-r--r--partner_support/samples/src/com/example/partnersupportsampletvinput/ChannelScanFragment.java4
-rw-r--r--partner_support/src/com/google/android/tv/partner/support/AutoValue_EpgInput.java97
-rw-r--r--partner_support/src/com/google/android/tv/partner/support/AutoValue_Lineup.java114
-rw-r--r--partner_support/src/com/google/android/tv/partner/support/BaseCustomization.java9
-rw-r--r--partner_support/src/com/google/android/tv/partner/support/EpgInput.java3
-rw-r--r--partner_support/src/com/google/android/tv/partner/support/EpgInputs.java2
-rw-r--r--partner_support/src/com/google/android/tv/partner/support/Lineup.java3
-rw-r--r--partner_support/src/com/google/android/tv/partner/support/PartnerCustomizations.java13
-rw-r--r--partner_support/tests/robotests/javatests/com/google/android/tv/partner/support/BaseCustomizationTest.java13
-rw-r--r--proguard.flags3
-rw-r--r--res/drawable-xhdpi/bg_protection.pngbin132623 -> 0 bytes
-rw-r--r--res/drawable-xhdpi/ic_app_store.png (renamed from res/drawable-xhdpi/ic_store.png)bin3894 -> 3894 bytes
-rw-r--r--res/drawable-xhdpi/ic_check_circle_white_48dp.pngbin361 -> 0 bytes
-rw-r--r--res/drawable-xhdpi/ic_developer_mode_tv_white_48dp.pngbin552 -> 0 bytes
-rw-r--r--res/drawable-xhdpi/ic_error_outline_pink_24dp.pngbin0 -> 673 bytes
-rw-r--r--res/drawable-xhdpi/ic_error_white_48dp.pngbin712 -> 0 bytes
-rw-r--r--res/drawable-xhdpi/ic_live_channels.pngbin33740 -> 0 bytes
-rw-r--r--res/drawable-xhdpi/ic_tv_app.pngbin0 -> 27209 bytes
-rw-r--r--res/drawable-xhdpi/ic_tv_app_96x96.png (renamed from res/drawable-xhdpi/ic_live_channels_96x96.png)bin3325 -> 3325 bytes
-rw-r--r--res/drawable-xhdpi/ic_warning_white_18dp.pngbin329 -> 0 bytes
-rw-r--r--res/drawable-xhdpi/ic_warning_white_96dp.pngbin976 -> 0 bytes
-rw-r--r--res/drawable-xhdpi/ic_warning_yellow_24dp.pngbin0 -> 371 bytes
-rw-r--r--res/drawable-xhdpi/live_tv_banner.png (renamed from res/drawable-xhdpi/banner.png)bin4873 -> 4873 bytes
-rw-r--r--res/drawable-xhdpi/usb_antenna.pngbin1639 -> 0 bytes
-rw-r--r--res/drawable/menu_background.xml15
-rw-r--r--res/layout/channel_banner.xml11
-rw-r--r--res/layout/dvr_details_description.xml21
-rw-r--r--res/layout/dvr_recording_card_view.xml27
-rw-r--r--res/layout/dvr_schedules_item.xml25
-rw-r--r--res/layout/menu_card_down.xml50
-rw-r--r--res/layout/menu_card_up.xml50
-rw-r--r--res/layout/pin_dialog.xml35
-rw-r--r--res/layout/pin_number_picker.xml51
-rw-r--r--res/layout/tunable_tv_view.xml21
-rw-r--r--res/values/arrays.xml103
-rw-r--r--res/values/colors.xml2
-rw-r--r--res/values/strings-custom.xml (renamed from tuner/tests/TestManifest.xml)11
-rw-r--r--res/values/strings.xml96
-rw-r--r--res/values/themes.xml3
-rw-r--r--settings.gradle25
-rw-r--r--src/com/android/tv/ChannelChanger.java24
-rw-r--r--src/com/android/tv/ChannelTuner.java8
-rw-r--r--src/com/android/tv/InputSessionManager.java165
-rw-r--r--src/com/android/tv/MainActivity.java521
-rw-r--r--src/com/android/tv/MediaSessionWrapper.java53
-rw-r--r--src/com/android/tv/SetupPassthroughActivity.java52
-rw-r--r--src/com/android/tv/TimeShiftManager.java55
-rw-r--r--src/com/android/tv/TvApplication.java116
-rw-r--r--src/com/android/tv/TvSingletons.java26
-rw-r--r--src/com/android/tv/analytics/SendChannelStatusRunnable.java17
-rw-r--r--src/com/android/tv/app/LiveTvApplication.java109
-rw-r--r--src/com/android/tv/app/LiveTvApplicationComponent.java26
-rw-r--r--src/com/android/tv/app/LiveTvModule.java33
-rw-r--r--src/com/android/tv/audio/AudioManagerHelper.java (renamed from src/com/android/tv/AudioManagerHelper.java)98
-rw-r--r--src/com/android/tv/audiotvservice/AudioOnlyTvService.java102
-rw-r--r--src/com/android/tv/audiotvservice/AudioOnlyTvServiceUtil.java68
-rw-r--r--src/com/android/tv/audiotvservice/README.md18
-rw-r--r--src/com/android/tv/data/BaseProgram.java13
-rw-r--r--src/com/android/tv/data/ChannelDataManager.java30
-rw-r--r--src/com/android/tv/data/ChannelImpl.java30
-rw-r--r--src/com/android/tv/data/PreviewDataManager.java33
-rw-r--r--src/com/android/tv/data/PreviewProgramContent.java2
-rw-r--r--src/com/android/tv/data/Program.java84
-rw-r--r--src/com/android/tv/data/ProgramDataManager.java220
-rw-r--r--src/com/android/tv/data/WatchedHistoryManager.java35
-rw-r--r--src/com/android/tv/data/api/Channel.java4
-rw-r--r--src/com/android/tv/data/epg/AutoValue_EpgReader_EpgChannel.java86
-rw-r--r--src/com/android/tv/data/epg/EpgFetchHelper.java54
-rw-r--r--src/com/android/tv/data/epg/EpgFetcherImpl.java154
-rw-r--r--src/com/android/tv/data/epg/EpgInputWhiteList.java11
-rw-r--r--src/com/android/tv/data/epg/EpgReader.java10
-rw-r--r--src/com/android/tv/dialog/PinDialogFragment.java428
-rw-r--r--src/com/android/tv/dialog/picker/PinPicker.java131
-rw-r--r--src/com/android/tv/dvr/DvrDataManagerImpl.java441
-rw-r--r--src/com/android/tv/dvr/DvrManager.java64
-rw-r--r--src/com/android/tv/dvr/DvrScheduleManager.java8
-rw-r--r--src/com/android/tv/dvr/DvrStorageStatusManager.java8
-rw-r--r--src/com/android/tv/dvr/DvrTvView.java158
-rw-r--r--src/com/android/tv/dvr/data/RecordedProgram.java911
-rw-r--r--src/com/android/tv/dvr/data/ScheduledRecording.java55
-rw-r--r--src/com/android/tv/dvr/data/SeriesRecording.java32
-rw-r--r--src/com/android/tv/dvr/provider/DvrDatabaseHelper.java3
-rw-r--r--src/com/android/tv/dvr/provider/DvrDbFuture.java (renamed from src/com/android/tv/dvr/provider/AsyncDvrDbTask.java)124
-rw-r--r--src/com/android/tv/dvr/provider/DvrDbSync.java3
-rw-r--r--src/com/android/tv/dvr/provider/EpisodicProgramLoadTask.java8
-rw-r--r--src/com/android/tv/dvr/recorder/InputTaskScheduler.java26
-rw-r--r--src/com/android/tv/dvr/recorder/RecordingTask.java64
-rw-r--r--src/com/android/tv/dvr/recorder/SeriesRecordingScheduler.java40
-rw-r--r--src/com/android/tv/dvr/ui/ChangeImageTransformWithScaledParent.java4
-rw-r--r--src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java2
-rw-r--r--src/com/android/tv/dvr/ui/DvrAlreadyScheduledFragment.java2
-rw-r--r--src/com/android/tv/dvr/ui/DvrConflictFragment.java4
-rw-r--r--src/com/android/tv/dvr/ui/DvrFutureProgramInfoFragment.java87
-rw-r--r--src/com/android/tv/dvr/ui/DvrHalfSizedDialogFragment.java27
-rw-r--r--src/com/android/tv/dvr/ui/DvrMissingStorageErrorFragment.java4
-rw-r--r--src/com/android/tv/dvr/ui/DvrScheduleFragment.java8
-rw-r--r--src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java70
-rw-r--r--src/com/android/tv/dvr/ui/DvrSeriesDeletionFragment.java78
-rw-r--r--src/com/android/tv/dvr/ui/DvrSeriesScheduledFragment.java4
-rw-r--r--src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java2
-rw-r--r--src/com/android/tv/dvr/ui/DvrUiHelper.java43
-rw-r--r--src/com/android/tv/dvr/ui/DvrWriteStoragePermissionRationaleFragment.java60
-rw-r--r--src/com/android/tv/dvr/ui/browse/ActionPresenterSelector.java8
-rw-r--r--src/com/android/tv/dvr/ui/browse/CurrentRecordingDetailsFragment.java69
-rw-r--r--src/com/android/tv/dvr/ui/browse/DetailsContent.java94
-rw-r--r--src/com/android/tv/dvr/ui/browse/DetailsContentPresenter.java8
-rw-r--r--src/com/android/tv/dvr/ui/browse/DetailsViewBackgroundHelper.java2
-rw-r--r--src/com/android/tv/dvr/ui/browse/DvrBrowseActivity.java6
-rw-r--r--src/com/android/tv/dvr/ui/browse/DvrBrowseFragment.java213
-rw-r--r--src/com/android/tv/dvr/ui/browse/DvrDetailsActivity.java144
-rw-r--r--src/com/android/tv/dvr/ui/browse/DvrDetailsFragment.java15
-rw-r--r--src/com/android/tv/dvr/ui/browse/RecordedProgramDetailsFragment.java22
-rw-r--r--src/com/android/tv/dvr/ui/browse/RecordingCardView.java38
-rw-r--r--src/com/android/tv/dvr/ui/browse/RecordingDetailsFragment.java3
-rw-r--r--src/com/android/tv/dvr/ui/browse/ScheduledRecordingDetailsFragment.java17
-rw-r--r--src/com/android/tv/dvr/ui/browse/ScheduledRecordingPresenter.java14
-rw-r--r--src/com/android/tv/dvr/ui/browse/SeriesRecordingDetailsFragment.java17
-rw-r--r--src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java147
-rw-r--r--src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java8
-rw-r--r--src/com/android/tv/dvr/ui/playback/DvrPlaybackActivity.java6
-rw-r--r--src/com/android/tv/dvr/ui/playback/DvrPlaybackControlHelper.java35
-rw-r--r--src/com/android/tv/dvr/ui/playback/DvrPlaybackMediaSessionHelper.java11
-rw-r--r--src/com/android/tv/dvr/ui/playback/DvrPlaybackOverlayFragment.java30
-rw-r--r--src/com/android/tv/dvr/ui/playback/DvrPlayer.java43
-rw-r--r--src/com/android/tv/features/PartnerFeatures.java58
-rw-r--r--src/com/android/tv/features/TvFeatures.java (renamed from src/com/android/tv/TvFeatures.java)61
-rw-r--r--src/com/android/tv/guide/ProgramGuide.java109
-rw-r--r--src/com/android/tv/guide/ProgramItemView.java37
-rw-r--r--src/com/android/tv/guide/ProgramManager.java57
-rw-r--r--src/com/android/tv/guide/ProgramRow.java22
-rw-r--r--src/com/android/tv/guide/ProgramRowAccessibilityDelegate.java64
-rw-r--r--src/com/android/tv/guide/ProgramTableAdapter.java52
-rw-r--r--src/com/android/tv/menu/ChannelsRowAdapter.java136
-rw-r--r--src/com/android/tv/menu/ChannelsRowItem.java10
-rw-r--r--src/com/android/tv/menu/Menu.java9
-rw-r--r--src/com/android/tv/menu/MenuAction.java4
-rw-r--r--src/com/android/tv/menu/OptionsRowAdapter.java17
-rw-r--r--src/com/android/tv/menu/PlayControlsRowView.java85
-rw-r--r--src/com/android/tv/menu/TvOptionsRowAdapter.java4
-rw-r--r--src/com/android/tv/modules/TvApplicationModule.java58
-rw-r--r--src/com/android/tv/modules/TvSingletonsModule.java47
-rw-r--r--src/com/android/tv/onboarding/OnboardingActivity.java25
-rw-r--r--src/com/android/tv/onboarding/SetupSourcesFragment.java12
-rw-r--r--src/com/android/tv/parental/ContentRatingSystem.java11
-rw-r--r--src/com/android/tv/parental/ParentalControlSettings.java45
-rw-r--r--src/com/android/tv/perf/EventNames.java28
-rw-r--r--src/com/android/tv/perf/PerformanceMonitor.java13
-rw-r--r--src/com/android/tv/perf/PerformanceMonitorManager.java38
-rw-r--r--src/com/android/tv/perf/PerformanceMonitorManagerFactory.java35
-rw-r--r--src/com/android/tv/perf/StartupMeasure.java42
-rw-r--r--src/com/android/tv/perf/stub/StubPerformanceMonitor.java (renamed from src/com/android/tv/perf/StubPerformanceMonitor.java)9
-rw-r--r--src/com/android/tv/perf/stub/StubPerformanceMonitorManager.java36
-rw-r--r--src/com/android/tv/perf/stub/StubStartupMeasure.java32
-rw-r--r--src/com/android/tv/receiver/BootCompletedReceiver.java4
-rw-r--r--src/com/android/tv/receiver/PackageIntentsReceiver.java2
-rw-r--r--src/com/android/tv/recommendation/ChannelPreviewUpdater.java36
-rw-r--r--src/com/android/tv/recommendation/RecommendationDataManager.java68
-rw-r--r--src/com/android/tv/search/AutoValue_LocalSearchProvider_SearchResult.java363
-rw-r--r--src/com/android/tv/search/DataManagerSearch.java20
-rw-r--r--src/com/android/tv/search/LocalSearchProvider.java7
-rw-r--r--src/com/android/tv/search/ProgramGuideSearchFragment.java4
-rw-r--r--src/com/android/tv/search/SearchInterface.java1
-rw-r--r--src/com/android/tv/search/TvProviderSearch.java7
-rw-r--r--src/com/android/tv/setup/SystemSetupActivity.java8
-rw-r--r--src/com/android/tv/tuner/TunerInputController.java556
-rw-r--r--src/com/android/tv/tunerinputcontroller/BuiltInTunerManager.java23
-rw-r--r--src/com/android/tv/tunerinputcontroller/HasBuiltInTunerManager.java30
-rw-r--r--src/com/android/tv/tunerinputcontroller/TunerInputController.java37
-rw-r--r--src/com/android/tv/ui/AppLayerTvView.java4
-rw-r--r--src/com/android/tv/ui/BlockScreenView.java9
-rw-r--r--src/com/android/tv/ui/ChannelBannerView.java177
-rw-r--r--src/com/android/tv/ui/DetailsActivity.java209
-rw-r--r--src/com/android/tv/ui/FullscreenDialogView.java16
-rw-r--r--src/com/android/tv/ui/InputBannerView.java7
-rw-r--r--src/com/android/tv/ui/IntroView.java8
-rw-r--r--src/com/android/tv/ui/KeypadChannelSwitchView.java11
-rw-r--r--src/com/android/tv/ui/ProgramDetailsFragment.java359
-rw-r--r--src/com/android/tv/ui/TunableTvView.java232
-rw-r--r--src/com/android/tv/ui/TvOverlayManager.java116
-rw-r--r--src/com/android/tv/ui/TvTransitionManager.java53
-rw-r--r--src/com/android/tv/ui/TvViewUiManager.java17
-rw-r--r--src/com/android/tv/ui/api/TunableTvViewPlayingApi.java (renamed from src/com/android/tv/ui/TunableTvViewPlayingApi.java)14
-rw-r--r--src/com/android/tv/ui/hideable/AutoHideScheduler.java15
-rw-r--r--src/com/android/tv/ui/sidepanel/CustomizeChannelListFragment.java20
-rw-r--r--src/com/android/tv/ui/sidepanel/MultiAudioFragment.java7
-rw-r--r--src/com/android/tv/ui/sidepanel/SettingsFragment.java13
-rw-r--r--src/com/android/tv/ui/sidepanel/SideFragment.java9
-rw-r--r--src/com/android/tv/ui/sidepanel/parentalcontrols/ChannelsBlockedFragment.java13
-rw-r--r--src/com/android/tv/ui/sidepanel/parentalcontrols/RatingsFragment.java5
-rw-r--r--src/com/android/tv/util/AsyncDbTask.java105
-rw-r--r--src/com/android/tv/util/RecurringRunner.java17
-rw-r--r--src/com/android/tv/util/SetupUtils.java78
-rw-r--r--src/com/android/tv/util/SqlParams.java11
-rw-r--r--src/com/android/tv/util/TvInputManagerHelper.java176
-rw-r--r--src/com/android/tv/util/TvProviderUtils.java211
-rw-r--r--src/com/android/tv/util/TvTrackInfoUtils.java202
-rw-r--r--src/com/android/tv/util/Utils.java139
-rw-r--r--src/com/android/tv/util/images/BitmapUtils.java16
-rw-r--r--src/com/android/tv/util/images/ImageLoader.java27
-rw-r--r--tests/common/Android.mk8
-rw-r--r--tests/common/AndroidManifest.xml2
-rw-r--r--tests/common/src/com/android/tv/testing/DbTestingUtils.java2
-rw-r--r--tests/common/src/com/android/tv/testing/EpgTestData.java23
-rw-r--r--tests/common/src/com/android/tv/testing/FakeEpgReader.java11
-rw-r--r--tests/common/src/com/android/tv/testing/FakeRemoteConfig.java55
-rw-r--r--tests/common/src/com/android/tv/testing/FakeTvProvider.java16
-rw-r--r--tests/common/src/com/android/tv/testing/TestSingletonApp.java94
-rw-r--r--tests/common/src/com/android/tv/testing/activities/BaseMainActivityTestCase.java4
-rw-r--r--tests/common/src/com/android/tv/testing/data/ProgramInfo.java12
-rw-r--r--tests/common/src/com/android/tv/testing/dvr/DvrDataManagerInMemoryImpl.java6
-rw-r--r--tests/common/src/com/android/tv/testing/robo/ContentProviders.java40
-rw-r--r--tests/common/src/com/android/tv/testing/robo/RobotTestAppHelper.java36
-rw-r--r--tests/common/src/com/android/tv/testing/shadows/ShadowMediaSession.java89
-rw-r--r--tests/func/Android.mk2
-rw-r--r--tests/func/AndroidManifest.xml4
-rw-r--r--tests/func/src/com/android/tv/tests/ui/ChannelBannerViewTest.java2
-rw-r--r--tests/func/src/com/android/tv/tests/ui/ChannelSourcesTest.java2
-rw-r--r--tests/func/src/com/android/tv/tests/ui/LiveChannelsAppTest.java2
-rw-r--r--tests/func/src/com/android/tv/tests/ui/LiveChannelsTestController.java2
-rw-r--r--tests/func/src/com/android/tv/tests/ui/ParentalControlsTest.java2
-rw-r--r--tests/func/src/com/android/tv/tests/ui/PlayControlsRowViewTest.java2
-rw-r--r--tests/func/src/com/android/tv/tests/ui/ProgramGuideTest.java2
-rw-r--r--tests/func/src/com/android/tv/tests/ui/TimeoutTest.java2
-rw-r--r--tests/func/src/com/android/tv/tests/ui/dvr/DvrLibraryTest.java4
-rw-r--r--tests/func/src/com/android/tv/tests/ui/sidepanel/CustomizeChannelListFragmentTest.java2
-rw-r--r--tests/input/AndroidManifest.xml2
-rw-r--r--tests/input/jank.sh4
-rwxr-xr-xtests/input/tools/get_test_logos.sh2
-rw-r--r--tests/jank/Android.mk2
-rw-r--r--tests/jank/AndroidManifest.xml4
-rw-r--r--tests/jank/README.md32
-rw-r--r--tests/jank/src/com/android/tv/tests/jank/ChannelZappingJankTest.java2
-rw-r--r--tests/jank/src/com/android/tv/tests/jank/MenuJankTest.java2
-rw-r--r--tests/jank/src/com/android/tv/tests/jank/ProgramGuideJankTest.java2
-rw-r--r--tests/unit/Android.mk2
-rw-r--r--tests/unit/AndroidManifest.xml4
-rw-r--r--tests/unit/src/com/android/tv/CurrentPositionMediatorTest.java6
-rw-r--r--tests/unit/src/com/android/tv/MainActivityTest.java6
-rw-r--r--tests/unit/src/com/android/tv/TimeShiftManagerTest.java4
-rw-r--r--tests/unit/src/com/android/tv/data/ChannelDataManagerTest.java33
-rw-r--r--tests/unit/src/com/android/tv/data/ChannelImplTest.java4
-rw-r--r--tests/unit/src/com/android/tv/data/TvInputNewComparatorTest.java98
-rw-r--r--tests/unit/src/com/android/tv/data/WatchedHistoryManagerTest.java140
-rw-r--r--tests/unit/src/com/android/tv/dvr/recorder/DvrRecordingServiceTest.java4
-rw-r--r--tests/unit/src/com/android/tv/features/FeaturesTest.java (renamed from tests/unit/src/com/android/tv/FeaturesTest.java)6
-rw-r--r--tests/unit/src/com/android/tv/menu/MenuTest.java6
-rw-r--r--tests/unit/src/com/android/tv/menu/TvOptionsRowAdapterTest.java6
-rw-r--r--tests/unit/src/com/android/tv/recommendation/ChannelRecordTest.java6
-rw-r--r--tests/unit/src/com/android/tv/recommendation/EvaluatorTestCase.java2
-rw-r--r--tests/unit/src/com/android/tv/recommendation/FavoriteChannelEvaluatorTest.java4
-rw-r--r--tests/unit/src/com/android/tv/recommendation/RecentChannelEvaluatorTest.java4
-rw-r--r--tests/unit/src/com/android/tv/recommendation/RecommenderTest.java6
-rw-r--r--tests/unit/src/com/android/tv/recommendation/RoutineWatchEvaluatorTest.java4
-rw-r--r--tests/unit/src/com/android/tv/util/MockTvSingletons.java60
-rw-r--r--tests/unit/src/com/android/tv/util/TvInputManagerHelperTest.java6
-rw-r--r--tests/unit/src/com/android/tv/util/TvTrackInfoUtilsTest.java127
-rw-r--r--tests/unit/src/com/android/tv/util/images/ImageCacheTest.java4
-rw-r--r--tests/unit/src/com/android/tv/util/images/ScaledBitmapInfoTest.java4
-rw-r--r--tuner/Android.bp48
-rw-r--r--tuner/Android.mk44
-rw-r--r--tuner/AndroidManifest.xml6
-rw-r--r--tuner/BuildConfig.java.in8
-rwxr-xr-xtuner/SampleDvbTuner/AndroidManifest.xml10
-rw-r--r--tuner/SampleDvbTuner/ResourceManifest.xml24
-rw-r--r--tuner/SampleDvbTuner/build.gradle91
-rw-r--r--tuner/SampleDvbTuner/res/mipmap-hdpi/ic_launcher.pngbin3750 -> 2577 bytes
-rw-r--r--tuner/SampleDvbTuner/res/mipmap-mdpi/ic_launcher.pngbin2218 -> 1640 bytes
-rw-r--r--tuner/SampleDvbTuner/res/mipmap-xhdpi/ic_launcher.pngbin4847 -> 3155 bytes
-rw-r--r--tuner/SampleDvbTuner/res/mipmap-xxhdpi/ic_launcher.pngbin7784 -> 4984 bytes
-rw-r--r--tuner/SampleDvbTuner/res/mipmap-xxxhdpi/ic_launcher.pngbin11119 -> 6725 bytes
-rw-r--r--tuner/SampleDvbTuner/settings.gradle25
-rw-r--r--tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/AndroidManifest.xml8
-rw-r--r--tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/app/SampleDvbTuner.java68
-rw-r--r--tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/app/SampleDvbTunerComponent.java26
-rw-r--r--tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/app/SampleDvbTunerModule.java52
-rw-r--r--tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/setup/SampleDvbTunerSetupActivity.java72
-rw-r--r--tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/singletons/SampleDvbSingletons.java23
-rw-r--r--tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/tvinput/SampleDvbTunerTvInputService.java14
-rwxr-xr-xtuner/SampleNetworkTuner/AndroidManifest.xml90
-rw-r--r--tuner/SampleNetworkTuner/build.gradle91
-rw-r--r--tuner/SampleNetworkTuner/res/mipmap-hdpi/ic_launcher.pngbin0 -> 3163 bytes
-rw-r--r--tuner/SampleNetworkTuner/res/mipmap-mdpi/ic_launcher.pngbin0 -> 1947 bytes
-rw-r--r--tuner/SampleNetworkTuner/res/mipmap-xhdpi/ic_launcher.pngbin0 -> 4107 bytes
-rw-r--r--tuner/SampleNetworkTuner/res/mipmap-xxhdpi/ic_launcher.pngbin0 -> 6682 bytes
-rw-r--r--tuner/SampleNetworkTuner/res/mipmap-xxxhdpi/ic_launcher.pngbin0 -> 9663 bytes
-rw-r--r--tuner/SampleNetworkTuner/res/values/strings.xml5
-rw-r--r--tuner/SampleNetworkTuner/res/xml/sample_network_tvinputservice.xml39
-rw-r--r--tuner/SampleNetworkTuner/settings.gradle25
-rw-r--r--tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/AndroidManifest.xml45
-rw-r--r--tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/README.md37
-rw-r--r--tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/app/SampleNetworkTuner.java88
-rw-r--r--tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/app/SampleNetworkTunerComponent.java26
-rw-r--r--tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/app/SampleNetworkTunerModule.java52
-rw-r--r--tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/setup/SampleNetworkTunerSetupActivity.java500
-rw-r--r--tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/singletons/SampleNetworkSingletons.java23
-rw-r--r--tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/tvinput/SampleNetworkTunerTvInputService.java34
-rw-r--r--tuner/build.gradle115
-rw-r--r--tuner/buildconfig.mk39
-rw-r--r--tuner/proto/Android.bp27
-rw-r--r--tuner/proto/channel.proto13
-rw-r--r--tuner/proto/track.proto8
-rw-r--r--tuner/res/values/strings.xml32
-rw-r--r--tuner/src/com/android/tv/tuner/DvbTunerHal.java53
-rw-r--r--tuner/src/com/android/tv/tuner/TunerFeatures.java103
-rw-r--r--tuner/src/com/android/tv/tuner/TunerHal.java190
-rw-r--r--tuner/src/com/android/tv/tuner/api/ChannelScanListener.java30
-rw-r--r--tuner/src/com/android/tv/tuner/api/ScanChannel.java55
-rw-r--r--tuner/src/com/android/tv/tuner/api/Tuner.java115
-rw-r--r--tuner/src/com/android/tv/tuner/api/TunerFactory.java31
-rw-r--r--tuner/src/com/android/tv/tuner/builtin/BuiltInTunerHalFactory.java96
-rw-r--r--tuner/src/com/android/tv/tuner/cc/CaptionTrackRenderer.java1
-rw-r--r--tuner/src/com/android/tv/tuner/data/Cea708Data.java1
-rw-r--r--tuner/src/com/android/tv/tuner/data/Cea708Parser.java (renamed from tuner/src/com/android/tv/tuner/cc/Cea708Parser.java)3
-rw-r--r--tuner/src/com/android/tv/tuner/data/PsipData.java1
-rw-r--r--tuner/src/com/android/tv/tuner/data/SectionParser.java (renamed from tuner/src/com/android/tv/tuner/ts/SectionParser.java)2
-rw-r--r--tuner/src/com/android/tv/tuner/exoplayer/Cea708TextTrackRenderer.java2
-rw-r--r--tuner/src/com/android/tv/tuner/exoplayer/ExoPlayerSampleExtractor.java61
-rw-r--r--tuner/src/com/android/tv/tuner/exoplayer/FileSampleExtractor.java11
-rw-r--r--tuner/src/com/android/tv/tuner/exoplayer/MpegTsPlayer.java13
-rw-r--r--tuner/src/com/android/tv/tuner/exoplayer/MpegTsRendererBuilder.java29
-rw-r--r--tuner/src/com/android/tv/tuner/exoplayer/MpegTsSampleExtractor.java25
-rw-r--r--tuner/src/com/android/tv/tuner/exoplayer/MpegTsVideoTrackRenderer.java2
-rw-r--r--tuner/src/com/android/tv/tuner/exoplayer/audio/MpegTsDefaultAudioTrackRenderer.java40
-rw-r--r--tuner/src/com/android/tv/tuner/exoplayer/audio/MpegTsMediaCodecAudioTrackRenderer.java8
-rw-r--r--tuner/src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java44
-rw-r--r--tuner/src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java19
-rw-r--r--tuner/src/com/android/tv/tuner/exoplayer/buffer/PlaybackBufferListener.java (renamed from tuner/src/com/android/tv/tuner/tvinput/PlaybackBufferListener.java)2
-rw-r--r--tuner/src/com/android/tv/tuner/exoplayer/buffer/RecordingSampleBuffer.java16
-rw-r--r--tuner/src/com/android/tv/tuner/exoplayer/buffer/SampleChunkIoHelper.java101
-rw-r--r--tuner/src/com/android/tv/tuner/exoplayer/buffer/SimpleSampleBuffer.java1
-rw-r--r--tuner/src/com/android/tv/tuner/exoplayer/buffer/TrickplayStorageManager.java4
-rw-r--r--tuner/src/com/android/tv/tuner/exoplayer2/VideoRendererExoV2.java143
-rw-r--r--tuner/src/com/android/tv/tuner/features/TunerFeatures.java59
-rw-r--r--tuner/src/com/android/tv/tuner/layout/ScaledLayout.java13
-rw-r--r--tuner/src/com/android/tv/tuner/livetuner/LiveTvTunerTvInputService.java14
-rw-r--r--tuner/src/com/android/tv/tuner/modules/TunerModule.java23
-rw-r--r--tuner/src/com/android/tv/tuner/modules/TunerSingletonsModule.java18
-rw-r--r--tuner/src/com/android/tv/tuner/prefs/TunerPreferences.java (renamed from tuner/src/com/android/tv/tuner/TunerPreferences.java)2
-rw-r--r--tuner/src/com/android/tv/tuner/setup/BaseTunerSetupActivity.java177
-rw-r--r--tuner/src/com/android/tv/tuner/setup/ChannelScanFileParser.java (renamed from tuner/src/com/android/tv/tuner/ChannelScanFileParser.java)48
-rw-r--r--tuner/src/com/android/tv/tuner/setup/LiveTvTunerSetupActivity.java48
-rw-r--r--tuner/src/com/android/tv/tuner/setup/LocationFragment.java235
-rw-r--r--tuner/src/com/android/tv/tuner/setup/PostalCodeFragment.java19
-rw-r--r--tuner/src/com/android/tv/tuner/setup/ScanFragment.java89
-rw-r--r--tuner/src/com/android/tv/tuner/setup/ScanResultFragment.java10
-rw-r--r--tuner/src/com/android/tv/tuner/setup/WelcomeFragment.java14
-rw-r--r--tuner/src/com/android/tv/tuner/singletons/TunerSingletons.java21
-rw-r--r--tuner/src/com/android/tv/tuner/source/FileSourceEventDetector.java (renamed from tuner/src/com/android/tv/tuner/tvinput/FileSourceEventDetector.java)7
-rw-r--r--tuner/src/com/android/tv/tuner/source/FileTsStreamer.java9
-rw-r--r--tuner/src/com/android/tv/tuner/source/TsDataSource.java5
-rw-r--r--tuner/src/com/android/tv/tuner/source/TsDataSourceManager.java61
-rw-r--r--tuner/src/com/android/tv/tuner/source/TsStreamer.java6
-rw-r--r--tuner/src/com/android/tv/tuner/source/TunerSourceModule.java31
-rw-r--r--tuner/src/com/android/tv/tuner/source/TunerTsStreamer.java41
-rw-r--r--tuner/src/com/android/tv/tuner/source/TunerTsStreamerManager.java79
-rw-r--r--tuner/src/com/android/tv/tuner/ts/EventDetector.java (renamed from tuner/src/com/android/tv/tuner/tvinput/EventDetector.java)35
-rw-r--r--tuner/src/com/android/tv/tuner/ts/TsParser.java3
-rw-r--r--tuner/src/com/android/tv/tuner/tvinput/AudioCapabilitiesReceiverV1Wrapper.java80
-rw-r--r--tuner/src/com/android/tv/tuner/tvinput/BaseTunerTvInputService.java55
-rw-r--r--tuner/src/com/android/tv/tuner/tvinput/TunerRecordingSession.java30
-rw-r--r--tuner/src/com/android/tv/tuner/tvinput/TunerRecordingSessionWorker.java235
-rw-r--r--tuner/src/com/android/tv/tuner/tvinput/TunerSession.java200
-rw-r--r--tuner/src/com/android/tv/tuner/tvinput/TunerSessionExoV2.java206
-rw-r--r--tuner/src/com/android/tv/tuner/tvinput/TunerSessionOverlay.java192
-rw-r--r--tuner/src/com/android/tv/tuner/tvinput/TunerSessionWorker.java1220
-rw-r--r--tuner/src/com/android/tv/tuner/tvinput/TunerSessionWorkerExoV2.java2073
-rw-r--r--tuner/src/com/android/tv/tuner/tvinput/TunerStorageCleanUpService.java4
-rw-r--r--tuner/src/com/android/tv/tuner/tvinput/datamanager/ChannelDataManager.java (renamed from tuner/src/com/android/tv/tuner/tvinput/ChannelDataManager.java)43
-rw-r--r--tuner/src/com/android/tv/tuner/tvinput/debug/TunerDebug.java (renamed from tuner/src/com/android/tv/tuner/tvinput/TunerDebug.java)2
-rw-r--r--tuner/src/com/android/tv/tuner/tvinput/factory/TunerSessionFactory.java25
-rw-r--r--tuner/src/com/android/tv/tuner/tvinput/factory/TunerSessionFactoryImpl.java49
-rw-r--r--tuner/src/com/android/tv/tuner/util/TunerInputInfoUtils.java112
-rw-r--r--tuner/tests/testing/Android.mk7
-rw-r--r--tuner/tests/testing/AndroidManifest.xml2
-rw-r--r--tuner/tests/unittests/javatests/AndroidManifest.xml2
-rw-r--r--tuner/tests/unittests/javatests/com/android/tv/tuner/AndroidManifest.xml4
-rw-r--r--tuner/tests/unittests/javatests/com/android/tv/tuner/FakeTunerHal.java6
-rw-r--r--tuner/tests/unittests/javatests/com/android/tv/tuner/FileTunerHal.java6
-rw-r--r--tuner/tests/unittests/javatests/com/android/tv/tuner/ZappingTimeTest.java16
-rw-r--r--tuner/tests/unittests/javatests/com/android/tv/tuner/layout/tests/AndroidManifest.xml2
-rw-r--r--tuner/tests/unittests/javatests/com/android/tv/tuner/layout/tests/ScaledLayoutTest.java6
-rw-r--r--tuner/tests/unittests/javatests/com/android/tv/tuner/setup/TunerHalCreatorTest.java (renamed from tuner/tests/unittests/javatests/com/android/tv/tuner/setup/TunerHalFactoryTest.java)46
586 files changed, 17584 insertions, 8467 deletions
diff --git a/Android.bp b/Android.bp
new file mode 100644
index 00000000..42686365
--- /dev/null
+++ b/Android.bp
@@ -0,0 +1,96 @@
+//
+// 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.
+//
+
+version_name = "1.20-asop"
+version_code = "417000328"
+
+android_app {
+ name: "LiveTv",
+
+ srcs: ["src/**/*.java"],
+
+ // TODO(b/122608868) turn proguard back on
+ optimize: {
+ enabled: false,
+ },
+
+ // It is required for com.android.providers.tv.permission.ALL_EPG_DATA
+ privileged: true,
+
+ sdk_version: "system_current",
+ min_sdk_version: "23", // M
+
+ resource_dirs: [
+ "res",
+ "material_res",
+
+ ],
+
+ libs: ["tv-guava-android-jar"],
+
+ static_libs: [
+ "android-support-annotations",
+ "android-support-compat",
+ "android-support-core-ui",
+ "androidx.tvprovider_tvprovider",
+ "android-support-v4",
+ "android-support-v7-appcompat",
+ "android-support-v7-palette",
+ "android-support-v7-preference",
+ "android-support-v7-recyclerview",
+ "android-support-v14-preference",
+ "android-support-v17-leanback",
+ "android-support-v17-preference-leanback",
+ "jsr330",
+ "live-channels-partner-support",
+ "live-tv-tuner-proto",
+ "live-tv-tuner",
+ "tv-auto-value-jar",
+ "tv-auto-factory-jar",
+ "tv-common",
+ "tv-error-prone-annotations-jar",
+ "tv-lib-dagger",
+ "tv-lib-exoplayer",
+ "tv-lib-exoplayer-v2-core",
+ "tv-lib-dagger-android",
+ ],
+
+ plugins: [
+ "tv-auto-value",
+ "tv-auto-factory",
+ "tv-lib-dagger-android-processor",
+ "tv-lib-dagger-compiler",
+ ],
+
+ javacflags: [
+ "-Xlint:deprecation",
+ "-Xlint:unchecked",
+ ],
+
+ aaptflags: [
+ "--version-name",
+ version_name,
+
+ "--version-code",
+ version_code,
+
+ "--extra-packages",
+ "com.android.tv.tuner",
+
+ "--extra-packages",
+ "com.android.tv.common",
+ ],
+}
diff --git a/Android.mk b/Android.mk
deleted file mode 100644
index 351d8b57..00000000
--- a/Android.mk
+++ /dev/null
@@ -1,102 +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.
-#
-
-LOCAL_PATH:= $(call my-dir)
-
-include $(CLEAR_VARS)
-
-
-LOCAL_MODULE_TAGS := optional
-
-include $(LOCAL_PATH)/version.mk
-
-LOCAL_SRC_FILES := $(call all-java-files-under, src)
-
-LOCAL_PACKAGE_NAME := LiveTv
-
-# It is required for com.android.providers.tv.permission.ALL_EPG_DATA
-LOCAL_PRIVILEGED_MODULE := true
-
-LOCAL_SDK_VERSION := system_current
-LOCAL_MIN_SDK_VERSION := 23 # M
-
-LOCAL_USE_AAPT2 := true
-
-LOCAL_RESOURCE_DIR := \
- $(LOCAL_PATH)/res \
-
-LOCAL_STATIC_JAVA_LIBRARIES := \
- android-support-annotations \
- lib-exoplayer \
- lib-exoplayer-v2-core \
- jsr330 \
-
-LOCAL_STATIC_ANDROID_LIBRARIES := \
- android-support-compat \
- android-support-core-ui \
- android-support-tv-provider \
- android-support-v4 \
- android-support-v7-appcompat \
- android-support-v7-palette \
- android-support-v7-preference \
- android-support-v7-recyclerview \
- android-support-v14-preference \
- android-support-v17-leanback \
- android-support-v17-preference-leanback \
- live-channels-partner-support \
- live-tv-tuner \
- tv-common \
-
-
-LOCAL_JAVACFLAGS := -Xlint:deprecation -Xlint:unchecked
-
-LOCAL_AAPT_FLAGS += \
- --version-name "$(version_name_package)" \
- --version-code $(version_code_package) \
-
-LOCAL_JNI_SHARED_LIBRARIES := libtunertvinput_jni
-LOCAL_AAPT_FLAGS += --extra-packages com.android.tv.tuner
-
-include $(BUILD_PACKAGE)
-
-#############################################################
-# Pre-built dependency jars
-#############################################################
-prebuilts := \
- lib-exoplayer:libs/exoplayer-r1.5.16.aar \
- lib-exoplayer-v2-core:libs/exoplayer-core-2-SNAPHOT-20180114.aar \
- auto-value-jar:../../../prebuilts/tools/common/m2/repository/com/google/auto/value/auto-value/1.5.2/auto-value-1.5.2.jar \
- javax-annotations-jar:../../../prebuilts/tools/common/m2/repository/javax/annotation/javax.annotation-api/1.2/javax.annotation-api-1.2.jar \
- truth-0-36-prebuilt-jar:../../../prebuilts/tools/common/m2/repository/com/google/truth/truth/0.36/truth-0.36.jar \
-
-define define-prebuilt
- $(eval tw := $(subst :, ,$(strip $(1)))) \
- $(eval include $(CLEAR_VARS)) \
- $(eval LOCAL_MODULE := $(word 1,$(tw))) \
- $(eval LOCAL_MODULE_TAGS := optional) \
- $(eval LOCAL_MODULE_CLASS := JAVA_LIBRARIES) \
- $(eval LOCAL_SRC_FILES := $(word 2,$(tw))) \
- $(eval LOCAL_UNINSTALLABLE_MODULE := true) \
- $(eval LOCAL_SDK_VERSION := current) \
- $(eval include $(BUILD_PREBUILT))
-endef
-
-$(foreach p,$(prebuilts),\
- $(call define-prebuilt,$(p)))
-
-prebuilts :=
-
-include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 3456f16b..a3988239 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -14,17 +14,19 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
+<!-- This manifest is for LiveTv -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
package="com.android.tv" >
<uses-sdk
android:minSdkVersion="23"
- android:targetSdkVersion="26" />
+ android:targetSdkVersion="27" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_HDMI_CEC_ACTIVE_SOURCE" />
- <uses-permission android:name="android.permission.GLOBAL_SEARCH" />
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.HDMI_CEC" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.MODIFY_PARENTAL_CONTROLS" />
@@ -32,6 +34,7 @@
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_TV_LISTINGS" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="com.android.providers.tv.permission.READ_EPG_DATA" />
<uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA" />
<uses-permission android:name="com.android.providers.tv.permission.ACCESS_ALL_EPG_DATA" />
@@ -70,22 +73,14 @@
<application
android:name="com.android.tv.app.LiveTvApplication"
android:allowBackup="true"
- android:banner="@drawable/banner"
- android:icon="@drawable/ic_live_channels"
+ android:appComponentFactory="android.support.v4.app.CoreComponentFactory"
+ android:banner="@drawable/live_tv_banner"
+ android:icon="@drawable/ic_tv_app"
android:label="@string/app_name"
android:supportsRtl="true"
- android:theme="@style/Theme.TV" >
- <activity
- android:name="com.android.tv.tuner.setup.LiveTvTunerSetupActivity"
- android:configChanges="keyboard|keyboardHidden"
- android:label="@string/bt_app_name"
- android:launchMode="singleInstance"
- android:process="com.android.tv.tuner"
- android:theme="@style/Theme.Setup.GuidedStep" >
- <intent-filter>
- <action android:name="android.intent.action.MAIN" />
- </intent-filter>
- </activity>
+ android:theme="@style/Theme.TV"
+ tools:replace="android:appComponentFactory">
+ >
<!-- providers are listed here to keep them separate from the internal versions -->
<provider
@@ -103,7 +98,32 @@
android:exported="false"
android:process="com.android.tv.common" />
- <activity android:name="com.android.tv.TvActivity" >
+
+
+ <receiver
+ android:name="com.android.tv.livetv.receiver.GlobalKeyReceiver"
+ android:exported="true" >
+ <intent-filter>
+ <action android:name="android.intent.action.GLOBAL_BUTTON" />
+ </intent-filter>
+
+ <!--
+ Not directly related to GlobalKeyReceiver but needed to be able to provide our
+ content rating definitions to the system service.
+ -->
+ <intent-filter>
+ <action android:name="android.media.tv.action.QUERY_CONTENT_RATING_SYSTEMS" />
+ </intent-filter>
+
+ <meta-data
+ android:name="android.media.tv.metadata.CONTENT_RATING_SYSTEMS"
+ android:resource="@xml/tv_content_rating_systems" />
+ </receiver>
+
+ <activity
+ android:name="com.android.tv.TvActivity"
+ android:exported="true"
+ android:launchMode="singleTask" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@@ -202,8 +222,9 @@
</intent-filter>
</activity>
<activity
- android:name="com.android.tv.dvr.ui.browse.DvrDetailsActivity"
+ android:name="com.android.tv.ui.DetailsActivity"
android:configChanges="keyboard|keyboardHidden"
+ android:exported="true"
android:theme="@style/Theme.TV.Dvr.Browse.Details" />
<activity
android:name="com.android.tv.dvr.ui.DvrSeriesSettingsActivity"
@@ -243,6 +264,7 @@
<action android:name="android.intent.action.PACKAGE_ADDED" />
<!-- PACKAGE_CHANGED for package enabled/disabled notification -->
<action android:name="android.intent.action.PACKAGE_CHANGED" />
+ <action android:name="android.intent.action.PACKAGE_FULLY_REMOVED" />
<action android:name="android.intent.action.PACKAGE_REMOVED" />
<data android:scheme="package" />
@@ -255,7 +277,7 @@
android:name="com.android.tv.setup.SystemSetupActivity"
android:configChanges="keyboard|keyboardHidden"
android:exported="true"
- android:label="@string/bt_app_name"
+ android:label="@string/app_name"
android:launchMode="singleInstance"
android:theme="@style/Theme.Setup.GuidedStep" >
<intent-filter>
@@ -263,23 +285,7 @@
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
- </activity>
- <!--
- TunerInputController should be the same process with MainActivity to check status
- of MainActivity
- -->
- <receiver
- android:name="com.android.tv.tuner.TunerInputController$IntentReceiver"
- android:exported="false" >
- <intent-filter>
- <action android:name="android.intent.action.BOOT_COMPLETED" />
- <action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
- <action android:name="android.hardware.usb.action.USB_DEVICE_DETACHED" />
- <action android:name="com.android.tv.action.APPLICATION_FIRST_LAUNCHED" />
- <action android:name="com.android.tv.action.NETWORK_TUNER_ATTACHED" />
- <action android:name="com.android.tv.action.NETWORK_TUNER_DETACHED" />
- </intent-filter>
- </receiver> <!-- DVR -->
+ </activity> <!-- DVR -->
<service
android:name="com.android.tv.dvr.recorder.DvrRecordingService"
android:label="@string/dvr_service_name" />
@@ -287,48 +293,8 @@
<receiver android:name="com.android.tv.dvr.recorder.DvrStartRecordingReceiver" />
<service
- android:name="com.android.tv.tuner.tvinput.TunerStorageCleanUpService"
- android:exported="false"
- android:permission="android.permission.BIND_JOB_SERVICE"
- android:process="com.android.tv.tuner" />
- <service
android:name="com.android.tv.data.epg.EpgFetchService"
android:permission="android.permission.BIND_JOB_SERVICE" />
-
- <receiver
- android:name="com.android.tv.livetv.receiver.GlobalKeyReceiver"
- android:exported="true" >
- <intent-filter>
- <action android:name="android.intent.action.GLOBAL_BUTTON" />
- </intent-filter>
-
- <!--
- Not directly related to GlobalKeyReceiver but needed to be able to provide our
- content rating definitions to the system service.
- -->
- <intent-filter>
- <action android:name="android.media.tv.action.QUERY_CONTENT_RATING_SYSTEMS" />
- </intent-filter>
-
- <meta-data
- android:name="android.media.tv.metadata.CONTENT_RATING_SYSTEMS"
- android:resource="@xml/tv_content_rating_systems" />
- </receiver>
-
- <service
- android:name="com.android.tv.tuner.livetuner.LiveTvTunerTvInputService"
- android:enabled="false"
- android:label="@string/bt_app_name"
- android:permission="android.permission.BIND_TV_INPUT"
- android:process="com.android.tv.tuner" >
- <intent-filter>
- <action android:name="android.media.tv.TvInputService" />
- </intent-filter>
-
- <meta-data
- android:name="android.media.tv.input"
- android:resource="@xml/ut_tvinputservice" />
- </service>
</application>
-</manifest> \ No newline at end of file
+</manifest>
diff --git a/README.md b/README.md
new file mode 100644
index 00000000..63c1f449
--- /dev/null
+++ b/README.md
@@ -0,0 +1,36 @@
+# Live TV
+
+__Live TV__ is the Open Source reference application for watching TV on Android TVs.
+
+## Source
+
+The source of truth is an internal google repository (aka google3) at
+cs/third_party/java_src/android_app/live_channels
+
+Changes are made in the google3 repository and automatically pushed here.
+
+The following files are only in the android repository and must be changed there.
+
+* *.mk
+* \*\*/lib/\*.\*
+
+## AOSP instructions
+
+To install LiveTv
+
+```bash
+echo "Compiling"
+m -j LiveTv
+echo "Installing"
+adb install -r ${OUT}/system/priv-app/LiveTv/LiveTv.apk
+
+```
+
+If it is your first time installing LiveTv you will need to do
+
+```bash
+adb root
+adb remount
+adb push ${OUT}/system/priv-app/LiveTv/LiveTv.apk /system/priv-app/LiveTv/LiveTv.apk
+adb reboot
+``` \ No newline at end of file
diff --git a/ResourceManifest.xml b/ResourceManifest.xml
deleted file mode 100644
index a859327f..00000000
--- a/ResourceManifest.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright (C) 2018 The Android Open Source Project
- ~
- ~ Licensed under the Apache License, Version 2.0 (the "License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the License at
- ~
- ~ http://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distributed under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License.
- -->
-
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="com.android.tv" xmlns:tools="http://schemas.android.com/tools">
- <uses-sdk android:targetSdkVersion="26" android:minSdkVersion="23"/>
- <application />
-</manifest>
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 00000000..23e3dbd1
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+/*
+ * Experimental gradle configuration. This file may not be up to date.
+ */
+
+buildscript {
+ repositories {
+ mavenCentral()
+ google()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:3.1.4'
+ classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.6'
+ }
+}
+apply plugin: 'com.android.application'
+android {
+ compileSdkVersion 28
+ buildToolsVersion '28.0.3'
+ dexOptions {
+ preDexLibraries = false
+ additionalParameters=['--core-library']
+ javaMaxHeapSize "6g"
+ }
+ android {
+ defaultConfig {
+ resConfigs "en"
+ }
+ }
+ defaultConfig {
+ minSdkVersion 23
+ targetSdkVersion 28
+ versionCode 1
+ versionName "1.0"
+ }
+ buildTypes {
+ debug {
+ minifyEnabled false
+ }
+ release {
+ minifyEnabled true
+ }
+ }
+ compileOptions() {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+ sourceSets {
+ main {
+ res.srcDirs = ['res', 'material_res']
+ java.srcDirs = ['src', 'partner_support/src']
+ manifest.srcFile 'AndroidManifest.xml'
+ }
+ }
+}
+
+repositories {
+ mavenCentral()
+ jcenter()
+ google()
+}
+
+dependencies {
+ implementation 'androidx.appcompat:appcompat:1.0.2'
+ implementation 'androidx.palette:palette:1.0.0'
+ implementation 'androidx.leanback:leanback:1.0.0'
+ implementation "androidx.tvprovider:tvprovider:1.0.0"
+ implementation "androidx.recyclerview:recyclerview:1.0.0"
+ implementation "androidx.recyclerview:recyclerview-selection:1.0.0"
+ implementation "androidx.palette:palette:1.0.0"
+
+ implementation 'com.google.dagger:dagger:2.18'
+ implementation 'com.google.dagger:dagger-android:2.18'
+ annotationProcessor 'com.google.dagger:dagger-compiler:2.18'
+ annotationProcessor 'com.google.dagger:dagger-android-processor:2.18'
+
+ /*Not building with latest one (1.6.3)*/
+ annotationProcessor 'com.google.auto.value:auto-value:1.5.4'
+ implementation 'com.google.auto.value:auto-value:1.5.4'
+ implementation 'javax.inject:javax.inject:1'
+ implementation 'com.google.guava:guava:26.0-android'
+ implementation project(':common')
+} \ No newline at end of file
diff --git a/common/Android.bp b/common/Android.bp
new file mode 100644
index 00000000..63759d40
--- /dev/null
+++ b/common/Android.bp
@@ -0,0 +1,61 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+android_library {
+ name: "tv-common",
+ srcs: [
+ "src/**/*.java",
+ "src/**/*.proto",
+ ],
+
+ sdk_version: "system_current",
+
+ proto: {
+ type: "lite",
+ },
+
+ resource_dirs: ["res"],
+
+ libs: [
+ "tv-auto-value-jar",
+ "tv-auto-factory-jar",
+ "android-support-annotations",
+ "tv-error-prone-annotations-jar",
+ "tv-guava-android-jar",
+ "jsr330",
+ "tv-lib-dagger",
+ "tv-lib-exoplayer",
+ "tv-lib-exoplayer-v2-core",
+ "android-support-compat",
+ "android-support-core-ui",
+ "android-support-v7-recyclerview",
+ "android-support-v17-leanback",
+ ],
+
+ static_libs: ["tv-lib-dagger-android"],
+
+ plugins: [
+ "tv-auto-value",
+ "tv-auto-factory",
+ "tv-lib-dagger-android-processor",
+ "tv-lib-dagger-compiler",
+ ],
+
+
+ min_sdk_version: "23",
+
+ // TODO(b/77284273): generate build config after dagger supports libraries
+ //include $(LOCAL_PATH)/buildconfig.mk
+
+}
diff --git a/common/Android.mk b/common/Android.mk
deleted file mode 100644
index 48f969e2..00000000
--- a/common/Android.mk
+++ /dev/null
@@ -1,31 +0,0 @@
-LOCAL_PATH:= $(call my-dir)
-include $(CLEAR_VARS)
-
-# Include all common java files.
-LOCAL_SRC_FILES := $(call all-java-files-under, src)
-
-LOCAL_MODULE := tv-common
-LOCAL_MODULE_CLASS := STATIC_JAVA_LIBRARIES
-LOCAL_MODULE_TAGS := optional
-LOCAL_SDK_VERSION := system_current
-
-LOCAL_USE_AAPT2 := true
-
-LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
-
-LOCAL_JAVA_LIBRARIES := \
- android-support-annotations
-
-LOCAL_DISABLE_RESOLVE_SUPPORT_LIBRARIES := true
-
-LOCAL_SHARED_ANDROID_LIBRARIES := \
- android-support-compat \
- android-support-core-ui \
- android-support-v7-recyclerview \
- android-support-v17-leanback
-
-LOCAL_MIN_SDK_VERSION := 23
-
-include $(LOCAL_PATH)/buildconfig.mk
-
-include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/common/AndroidManifest.xml b/common/AndroidManifest.xml
index c1c698c5..7002d5fb 100644
--- a/common/AndroidManifest.xml
+++ b/common/AndroidManifest.xml
@@ -17,6 +17,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.tv.common"
android:versionCode="1">
- <uses-sdk android:targetSdkVersion="26" android:minSdkVersion="21"/>
+ <uses-sdk android:targetSdkVersion="27" android:minSdkVersion="23"/>
<application />
</manifest>
diff --git a/common/build.gradle b/common/build.gradle
new file mode 100644
index 00000000..f3714758
--- /dev/null
+++ b/common/build.gradle
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+/*
+ * Experimental gradle configuration. This file may not be up to date.
+ */
+
+apply plugin: 'com.android.library'
+apply plugin: 'com.google.protobuf'
+buildscript {
+ repositories {
+ mavenCentral()
+ google()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:3.1.4'
+ classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.6'
+ }
+}
+android {
+ compileSdkVersion 28
+ buildToolsVersion '28.0.3'
+ dexOptions {
+ preDexLibraries = false
+ additionalParameters = ['--core-library']
+ javaMaxHeapSize "6g"
+ }
+
+ android {
+ defaultConfig {
+ resConfigs "en"
+ }
+ }
+
+ defaultConfig {
+ minSdkVersion 23
+ targetSdkVersion 28
+ versionCode 1
+ versionName "1.0"
+ }
+ buildTypes {
+ debug {
+ minifyEnabled false
+ buildConfigField "boolean", "AOSP", "true"
+ buildConfigField "boolean", "ENG", "true"
+ buildConfigField "boolean", "NO_JNI_TEST", "false"
+ }
+ release {
+ minifyEnabled true
+ buildConfigField "boolean", "AOSP", "true"
+ buildConfigField "boolean", "ENG", "false"
+ buildConfigField "boolean", "NO_JNI_TEST", "false"
+ }
+ }
+ compileOptions() {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
+ sourceSets {
+ main {
+ res.srcDirs = ['res']
+ java.srcDirs = ['src']
+ manifest.srcFile 'AndroidManifest.xml'
+ proto {
+ srcDir 'src/com/android/tv/common/compat/internal'
+ }
+ }
+ }
+}
+
+repositories {
+ mavenCentral()
+ google()
+}
+
+dependencies {
+ implementation 'androidx.appcompat:appcompat:1.0.2'
+ implementation 'androidx.palette:palette:1.0.0'
+ implementation 'androidx.leanback:leanback:1.0.0'
+ implementation "androidx.tvprovider:tvprovider:1.0.0"
+ implementation "androidx.recyclerview:recyclerview:1.0.0"
+ implementation "androidx.recyclerview:recyclerview-selection:1.0.0"
+ implementation "androidx.palette:palette:1.0.0"
+ implementation 'com.google.guava:guava:26.0-android'
+ implementation 'com.google.protobuf:protobuf-java:3.0.0'
+ implementation 'com.google.dagger:dagger:2.18'
+ implementation 'com.google.dagger:dagger-android:2.18'
+ annotationProcessor 'com.google.dagger:dagger-compiler:2.18'
+ annotationProcessor 'com.google.dagger:dagger-android-processor:2.18'
+}
+protobuf {
+ // Configure the protoc executable
+ protoc {
+ artifact = 'com.google.protobuf:protoc:3.0.0'
+
+ plugins {
+ javalite {
+ // The codegen for lite comes as a separate artifact
+ artifact = 'com.google.protobuf:protoc-gen-javalite:3.0.0'
+ }
+ }
+
+ generateProtoTasks {
+ all().each {
+ task -> task.builtins {
+ remove java
+ }
+ task.plugins {
+ javalite {}
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/common/src/com/android/tv/common/BaseApplication.java b/common/src/com/android/tv/common/BaseApplication.java
index 71c9b4d7..45c32567 100644
--- a/common/src/com/android/tv/common/BaseApplication.java
+++ b/common/src/com/android/tv/common/BaseApplication.java
@@ -17,10 +17,7 @@
package com.android.tv.common;
import android.annotation.TargetApi;
-import android.app.Application;
import android.content.Context;
-import android.content.Intent;
-import android.os.AsyncTask;
import android.os.Build;
import android.os.StrictMode;
import android.support.annotation.VisibleForTesting;
@@ -30,9 +27,10 @@ import com.android.tv.common.util.Clock;
import com.android.tv.common.util.CommonUtils;
import com.android.tv.common.util.Debug;
import com.android.tv.common.util.SystemProperties;
+import dagger.android.DaggerApplication;
/** The base application class for Live TV applications. */
-public abstract class BaseApplication extends Application implements BaseSingletons {
+public abstract class BaseApplication extends DaggerApplication implements BaseSingletons {
private RecordingStorageStatusManager mRecordingStorageStatusManager;
/**
@@ -41,7 +39,13 @@ public abstract class BaseApplication extends Application implements BaseSinglet
*/
@VisibleForTesting public static BaseSingletons sSingletons;
- /** Returns the {@link BaseSingletons} using the application context. */
+ /**
+ * Returns the {@link BaseSingletons} using the application context.
+ *
+ * @deprecated use {@link com.android.tv.common.singletons.HasSingletons#get(Class, Context)}
+ * instead
+ */
+ @Deprecated
public static BaseSingletons getSingletons(Context context) {
// STOP-SHIP: changing the method to protected once the Tuner application is created.
// No need to be "synchronized" because this doesn't create any instance.
@@ -65,8 +69,15 @@ public abstract class BaseApplication extends Application implements BaseSinglet
StrictMode.ThreadPolicy.Builder threadPolicyBuilder =
new StrictMode.ThreadPolicy.Builder().detectAll().penaltyLog();
// TODO(b/69565157): Turn penaltyDeath on for VMPolicy when tests are fixed.
+ // TODO(b/120840665): Restore detecting untagged network sockets
StrictMode.VmPolicy.Builder vmPolicyBuilder =
- new StrictMode.VmPolicy.Builder().detectAll().penaltyLog();
+ new StrictMode.VmPolicy.Builder()
+ .detectActivityLeaks()
+ .detectLeakedClosableObjects()
+ .detectLeakedRegistrationObjects()
+ .detectFileUriExposure()
+ .detectContentUriWithoutPermission()
+ .penaltyLog();
if (!CommonUtils.isRunningInTest()) {
threadPolicyBuilder.penaltyDialog();
@@ -77,14 +88,6 @@ public abstract class BaseApplication extends Application implements BaseSinglet
if (CommonFeatures.DVR.isEnabled(this)) {
getRecordingStorageStatusManager();
}
- new AsyncTask<Void, Void, Void>() {
- @Override
- protected Void doInBackground(Void... params) {
- // Fetch remote config
- getRemoteConfig().fetch(null);
- return null;
- }
- }.execute();
}
@Override
@@ -101,7 +104,4 @@ public abstract class BaseApplication extends Application implements BaseSinglet
}
return mRecordingStorageStatusManager;
}
-
- @Override
- public abstract Intent getTunerSetupIntent(Context context);
}
diff --git a/common/src/com/android/tv/common/BaseSingletons.java b/common/src/com/android/tv/common/BaseSingletons.java
index e735cdb4..10530617 100644
--- a/common/src/com/android/tv/common/BaseSingletons.java
+++ b/common/src/com/android/tv/common/BaseSingletons.java
@@ -16,20 +16,17 @@
package com.android.tv.common;
-import android.content.Context;
-import android.content.Intent;
-import com.android.tv.common.config.api.RemoteConfig.HasRemoteConfig;
+import com.android.tv.common.buildtype.HasBuildType;
+import com.android.tv.common.flags.has.HasCloudEpgFlags;
+import com.android.tv.common.flags.has.HasConcurrentDvrPlaybackFlags;
import com.android.tv.common.recording.RecordingStorageStatusManager;
import com.android.tv.common.util.Clock;
/** Injection point for the base app */
-public interface BaseSingletons extends HasRemoteConfig {
+public interface BaseSingletons
+ extends HasCloudEpgFlags, HasBuildType, HasConcurrentDvrPlaybackFlags {
Clock getClock();
RecordingStorageStatusManager getRecordingStorageStatusManager();
-
- Intent getTunerSetupIntent(Context context);
-
- String getEmbeddedTunerInputId();
}
diff --git a/common/src/com/android/tv/common/BuildConfig.java b/common/src/com/android/tv/common/BuildConfig.java
new file mode 100644
index 00000000..b3ad002b
--- /dev/null
+++ b/common/src/com/android/tv/common/BuildConfig.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.common;
+
+/* Hard coded BuildConfig. */
+public final class BuildConfig {
+ public static final boolean DEBUG = true;
+ public static final boolean ENG = false;
+ public static final boolean NO_JNI_TEST = false;
+ public static final boolean AOSP = true;
+
+ private BuildConfig() {}
+} \ No newline at end of file
diff --git a/common/src/com/android/tv/common/CommonConstants.java b/common/src/com/android/tv/common/CommonConstants.java
index ac379d18..43d9851b 100644
--- a/common/src/com/android/tv/common/CommonConstants.java
+++ b/common/src/com/android/tv/common/CommonConstants.java
@@ -19,10 +19,20 @@ package com.android.tv.common;
/** Constants for common use in apps and tests. */
public final class CommonConstants {
+ @Deprecated // TODO(b/121158110) refactor so this is not needed.
public static final String BASE_PACKAGE =
+// AOSP_Comment_Out !BuildConfig.AOSP
+// AOSP_Comment_Out ? "com.google.android.tv"
+// AOSP_Comment_Out :
"com.android.tv";
/** A constant for the key of the extra data for the app linking intent. */
public static final String EXTRA_APP_LINK_CHANNEL_URI = "app_link_channel_uri";
+ /**
+ * Video is unavailable because the source is not physically connected, for example the HDMI
+ * cable is not connected.
+ */
+ public static final int VIDEO_UNAVAILABLE_REASON_NOT_CONNECTED = 5;
+
private CommonConstants() {}
}
diff --git a/common/src/com/android/tv/common/TvContentRatingCache.java b/common/src/com/android/tv/common/TvContentRatingCache.java
index cfdb8e4d..f2fda69c 100644
--- a/common/src/com/android/tv/common/TvContentRatingCache.java
+++ b/common/src/com/android/tv/common/TvContentRatingCache.java
@@ -23,9 +23,8 @@ import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.Log;
import com.android.tv.common.memory.MemoryManageable;
-import java.util.ArrayList;
+import com.google.common.collect.ImmutableList;
import java.util.Collections;
-import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
@@ -42,19 +41,19 @@ public final class TvContentRatingCache implements MemoryManageable {
}
// @GuardedBy("TvContentRatingCache.this")
- private final Map<String, TvContentRating[]> mRatingsMultiMap = new ArrayMap<>();
+ private final Map<String, ImmutableList<TvContentRating>> mRatingsMultiMap = new ArrayMap<>();
/**
* Returns an array TvContentRatings from a string of comma separated set of rating strings
- * creating each from {@link TvContentRating#unflattenFromString(String)} if needed. Returns
- * {@code null} if the string is empty or contains no valid ratings.
+ * creating each from {@link TvContentRating#unflattenFromString(String)} if needed or an empty
+ * list if the string is empty or contains no valid ratings.
*/
- @Nullable
- public synchronized TvContentRating[] getRatings(String commaSeparatedRatings) {
+ public synchronized ImmutableList<TvContentRating> getRatings(
+ @Nullable String commaSeparatedRatings) {
if (TextUtils.isEmpty(commaSeparatedRatings)) {
- return null;
+ return ImmutableList.of();
}
- TvContentRating[] tvContentRatings;
+ ImmutableList<TvContentRating> tvContentRatings;
if (mRatingsMultiMap.containsKey(commaSeparatedRatings)) {
tvContentRatings = mRatingsMultiMap.get(commaSeparatedRatings);
} else {
@@ -76,12 +75,13 @@ public final class TvContentRatingCache implements MemoryManageable {
/** Returns a sorted array of TvContentRatings from a comma separated string of ratings. */
@VisibleForTesting
- static TvContentRating[] stringToContentRatings(String commaSeparatedRatings) {
+ static ImmutableList<TvContentRating> stringToContentRatings(
+ @Nullable String commaSeparatedRatings) {
if (TextUtils.isEmpty(commaSeparatedRatings)) {
- return null;
+ return ImmutableList.of();
}
Set<String> ratingStrings = getSortedSetFromCsv(commaSeparatedRatings);
- List<TvContentRating> contentRatings = new ArrayList<>();
+ ImmutableList.Builder<TvContentRating> contentRatings = ImmutableList.builder();
for (String rating : ratingStrings) {
try {
contentRatings.add(TvContentRating.unflattenFromString(rating));
@@ -89,9 +89,7 @@ public final class TvContentRatingCache implements MemoryManageable {
Log.e(TAG, "Can't parse the content rating: '" + rating + "'", e);
}
}
- return contentRatings.size() == 0
- ? null
- : contentRatings.toArray(new TvContentRating[contentRatings.size()]);
+ return contentRatings.build();
}
private static Set<String> getSortedSetFromCsv(String commaSeparatedRatings) {
@@ -118,19 +116,17 @@ public final class TvContentRatingCache implements MemoryManageable {
* Returns a string of each flattened content rating, sorted and concatenated together with a
* comma.
*/
- public static String contentRatingsToString(TvContentRating[] contentRatings) {
- if (contentRatings == null || contentRatings.length == 0) {
+ @Nullable
+ public static String contentRatingsToString(
+ @Nullable ImmutableList<TvContentRating> contentRatings) {
+ if (contentRatings == null) {
return null;
}
- String[] ratingStrings = new String[contentRatings.length];
- for (int i = 0; i < contentRatings.length; i++) {
- ratingStrings[i] = contentRatings[i].flattenToString();
- }
- if (ratingStrings.length == 1) {
- return ratingStrings[0];
- } else {
- return TextUtils.join(",", toSortedSet(ratingStrings));
+ SortedSet<String> ratingStrings = new TreeSet<>();
+ for (TvContentRating rating : contentRatings) {
+ ratingStrings.add(rating.flattenToString());
}
+ return TextUtils.join(",", ratingStrings);
}
@Override
diff --git a/common/src/com/android/tv/common/buildtype/AospBuildTypeProvider.java b/common/src/com/android/tv/common/buildtype/AospBuildTypeProvider.java
new file mode 100644
index 00000000..8d39b3a6
--- /dev/null
+++ b/common/src/com/android/tv/common/buildtype/AospBuildTypeProvider.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.common.buildtype;
+
+/** {@code AOSP} {@link HasBuildType}. */
+public class AospBuildTypeProvider implements HasBuildType {
+
+ @Override
+ public BuildType getBuildType() {
+ return BuildType.AOSP;
+ }
+}
diff --git a/common/src/com/android/tv/common/buildtype/EngBuildTypeProvider.java b/common/src/com/android/tv/common/buildtype/EngBuildTypeProvider.java
new file mode 100644
index 00000000..5f18794c
--- /dev/null
+++ b/common/src/com/android/tv/common/buildtype/EngBuildTypeProvider.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.common.buildtype;
+
+/** {@code ENG} {@link HasBuildType}. */
+public class EngBuildTypeProvider implements HasBuildType {
+
+ @Override
+ public BuildType getBuildType() {
+ return BuildType.ENG;
+ }
+}
diff --git a/common/src/com/android/tv/common/buildtype/HasBuildType.java b/common/src/com/android/tv/common/buildtype/HasBuildType.java
new file mode 100644
index 00000000..7d5677c9
--- /dev/null
+++ b/common/src/com/android/tv/common/buildtype/HasBuildType.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.common.buildtype;
+
+/**
+ * The provides a {@link BuildType} for selecting features in code.
+ *
+ * <p>This is considered an anti-pattern and new usages should be discouraged.
+ */
+public interface HasBuildType {
+
+ /** Compile time constant for build type. */
+ enum BuildType {
+ AOSP,
+ ENG,
+ NO_JNI_TEST,
+ PROD
+ }
+
+ BuildType getBuildType();
+}
diff --git a/common/src/com/android/tv/common/buildtype/NoJniTestBuildTypeProvider.java b/common/src/com/android/tv/common/buildtype/NoJniTestBuildTypeProvider.java
new file mode 100644
index 00000000..1620af20
--- /dev/null
+++ b/common/src/com/android/tv/common/buildtype/NoJniTestBuildTypeProvider.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.common.buildtype;
+
+/** {@code NO_JNI_TEST} {@link HasBuildType}. */
+public class NoJniTestBuildTypeProvider implements HasBuildType {
+
+ @Override
+ public BuildType getBuildType() {
+ return BuildType.NO_JNI_TEST;
+ }
+}
diff --git a/common/src/com/android/tv/common/buildtype/ProdBuildTypeProvider.java b/common/src/com/android/tv/common/buildtype/ProdBuildTypeProvider.java
new file mode 100644
index 00000000..16db3263
--- /dev/null
+++ b/common/src/com/android/tv/common/buildtype/ProdBuildTypeProvider.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.common.buildtype;
+
+/** {@code Prod} {@link HasBuildType}. */
+public class ProdBuildTypeProvider implements HasBuildType {
+
+ @Override
+ public BuildType getBuildType() {
+ return BuildType.PROD;
+ }
+}
diff --git a/common/src/com/android/tv/common/compat/README.md b/common/src/com/android/tv/common/compat/README.md
new file mode 100644
index 00000000..8d87d83c
--- /dev/null
+++ b/common/src/com/android/tv/common/compat/README.md
@@ -0,0 +1,7 @@
+# TIF Compatibility Library
+
+This is the temporary location for the of Compatibility Library while it is under development.
+It will eventually move to a support library location.
+
+
+See go/tif-compat-proposal \ No newline at end of file
diff --git a/common/src/com/android/tv/common/compat/RecordingSessionCompat.java b/common/src/com/android/tv/common/compat/RecordingSessionCompat.java
new file mode 100644
index 00000000..6941e47b
--- /dev/null
+++ b/common/src/com/android/tv/common/compat/RecordingSessionCompat.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.tv.common.compat;
+
+import android.content.Context;
+import android.media.tv.TvInputService.RecordingSession;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.support.annotation.RequiresApi;
+import com.android.tv.common.compat.api.RecordingSessionCompatCommands;
+import com.android.tv.common.compat.api.RecordingSessionCompatEvents;
+import com.android.tv.common.compat.api.SessionEventNotifier;
+import com.android.tv.common.compat.internal.RecordingSessionCompatProcessor;
+
+/**
+ * TIF Compatibility for {@link RecordingSession}.
+ *
+ * <p>Extends {@code RecordingSession} in a backwards compatible way.
+ */
+@RequiresApi(api = VERSION_CODES.N)
+public abstract class RecordingSessionCompat extends RecordingSession
+ implements SessionEventNotifier,
+ RecordingSessionCompatCommands,
+ RecordingSessionCompatEvents {
+
+ private final RecordingSessionCompatProcessor mProcessor;
+
+ public RecordingSessionCompat(Context context) {
+ super(context);
+ mProcessor = new RecordingSessionCompatProcessor(this, this);
+ }
+
+ @Override
+ public void onAppPrivateCommand(String action, Bundle data) {
+ if (!mProcessor.handleAppPrivateCommand(action, data)) {
+ super.onAppPrivateCommand(action, data);
+ }
+ }
+
+ /** Display a debug message to the session for display on dev builds only */
+ @Override
+ public void onDevMessage(String message) {}
+
+ /** Notify the client to Display a message in the application as a toast on dev builds only. */
+ @Override
+ public void notifyDevToast(String message) {
+ mProcessor.notifyDevToast(message);
+ }
+
+ /** Notify the client Recording started. */
+ @Override
+ public void notifyRecordingStarted(String uri) {
+ mProcessor.notifyRecordingStarted(uri);
+ }
+}
diff --git a/common/src/com/android/tv/common/compat/TisSessionCompat.java b/common/src/com/android/tv/common/compat/TisSessionCompat.java
new file mode 100644
index 00000000..97f4fb32
--- /dev/null
+++ b/common/src/com/android/tv/common/compat/TisSessionCompat.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.tv.common.compat;
+
+import android.content.Context;
+import android.media.tv.TvInputService.Session;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.support.annotation.RequiresApi;
+import com.android.tv.common.compat.api.SessionCompatCommands;
+import com.android.tv.common.compat.api.SessionCompatEvents;
+import com.android.tv.common.compat.api.SessionEventNotifier;
+import com.android.tv.common.compat.internal.TifSessionCompatProcessor;
+
+/**
+ * TIF Compatibility for {@link Session}.
+ *
+ * <p>Extends {@code Session} in a backwards compatible way.
+ */
+@RequiresApi(api = VERSION_CODES.LOLLIPOP)
+public abstract class TisSessionCompat extends Session
+ implements SessionEventNotifier, SessionCompatCommands, SessionCompatEvents {
+
+ private final TifSessionCompatProcessor mTifCompatProcessor;
+
+ public TisSessionCompat(Context context) {
+ super(context);
+ mTifCompatProcessor = new TifSessionCompatProcessor(this, this);
+ }
+
+ @Override
+ public void onAppPrivateCommand(String action, Bundle data) {
+ if (!mTifCompatProcessor.handleAppPrivateCommand(action, data)) {
+ super.onAppPrivateCommand(action, data);
+ }
+ }
+
+ @Override
+ public void onDevMessage(String message) {}
+
+ @Override
+ public void notifyDevToast(String message) {
+ mTifCompatProcessor.notifyDevToast(message);
+ }
+
+ /**
+ * Notify the application with current signal strength.
+ *
+ * <p>At each {MainActivity#tune(boolean)}, the signal strength is implicitly reset to {@link
+ * TvInputConstantCompat#SIGNAL_STRENGTH_NOT_USED}. If a TV input supports reporting signal
+ * strength, it should set the signal strength to {@link
+ * TvInputConstantCompat#SIGNAL_STRENGTH_UNKNOWN} in
+ * {TunerSessionWorker#prepareTune(TunerChannel, String)}, until a valid strength is available.
+ *
+ * @param value The current signal strength. Valid values are {@link
+ * TvInputConstantCompat#SIGNAL_STRENGTH_NOT_USED}, {@link
+ * TvInputConstantCompat#SIGNAL_STRENGTH_UNKNOWN}, and 0 - 100 inclusive.
+ */
+ @Override
+ public void notifySignalStrength(int value) {
+ mTifCompatProcessor.notifySignalStrength(value);
+ }
+}
diff --git a/common/src/com/android/tv/common/compat/TvInputConstantCompat.java b/common/src/com/android/tv/common/compat/TvInputConstantCompat.java
new file mode 100644
index 00000000..251e8481
--- /dev/null
+++ b/common/src/com/android/tv/common/compat/TvInputConstantCompat.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.tv.common.compat;
+
+/** Temp TIF Compatibility for {@link TvInputManager} constants. */
+public class TvInputConstantCompat {
+
+ /**
+ * Status for {@link TisSessionCompat#notifySignalStrength(int)} and
+ * {@link TvViewCompat.TvInputCallback#onTimeShiftStatusChanged(String, int)}:
+ *
+ * <p>SIGNAL_STRENGTH_NOT_USED means the TV Input does not report signal strength. Each onTune
+ * command implicitly resets the TV App's signal strength state to SIGNAL_STRENGTH_NOT_USED.
+ */
+ public static final int SIGNAL_STRENGTH_NOT_USED = -3;
+
+ /**
+ * Status for {@link TisSessionCompat#notifySignalStrength(int)} and
+ * {@link TvViewCompat.TvInputCallback#onTimeShiftStatusChanged(String, int)}:
+ *
+ * <p>SIGNAL_STRENGTH_ERROR means exception/error when handling signal strength.
+ */
+ public static final int SIGNAL_STRENGTH_ERROR = -2;
+
+ /**
+ * Status for {@link TisSessionCompat#notifySignalStrength(int)} and
+ * {@link TvViewCompat.TvInputCallback#onTimeShiftStatusChanged(String, int)}:
+ *
+ * <p>SIGNAL_STRENGTH_UNKNOWN means the TV Input supports signal strength, but does not
+ * currently know what the strength is.
+ */
+ public static final int SIGNAL_STRENGTH_UNKNOWN = -1;
+}
diff --git a/common/src/com/android/tv/common/compat/TvInputInfoCompat.java b/common/src/com/android/tv/common/compat/TvInputInfoCompat.java
new file mode 100644
index 00000000..685a3ed9
--- /dev/null
+++ b/common/src/com/android/tv/common/compat/TvInputInfoCompat.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.tv.common.compat;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.pm.ServiceInfo;
+import android.media.tv.TvInputInfo;
+import android.media.tv.TvInputService;
+import android.support.annotation.VisibleForTesting;
+import android.util.Log;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ * TIF Compatibility for {@link TvInputInfo}.
+ */
+public class TvInputInfoCompat {
+ private static final String TAG = "TvInputInfoCompat";
+ private static final String ATTRIBUTE_NAMESPACE_ANDROID =
+ "http://schemas.android.com/apk/res/android";
+ private static final String TV_INPUT_XML_START_TAG_NAME = "tv-input";
+ private static final String TV_INPUT_EXTRA_XML_START_TAG_NAME = "extra";
+ private static final String ATTRIBUTE_NAME = "name";
+ private static final String ATTRIBUTE_VALUE = "value";
+ private static final String ATTRIBUTE_NAME_AUDIO_ONLY =
+ "com.android.tv.common.compat.tvinputinfocompat.audioOnly";
+
+ private final Context mContext;
+ private final TvInputInfo mTvInputInfo;
+ private final boolean mAudioOnly;
+
+ public TvInputInfoCompat(Context context, TvInputInfo tvInputInfo) {
+ mContext = context;
+ mTvInputInfo = tvInputInfo;
+ // TODO(b/112938832): use tvInputInfo.isAudioOnly() when SDK is updated
+ mAudioOnly = Boolean.parseBoolean(getExtras().get(ATTRIBUTE_NAME_AUDIO_ONLY));
+ }
+
+ public TvInputInfo getTvInputInfo() {
+ return mTvInputInfo;
+ }
+
+ public boolean isAudioOnly() {
+ return mAudioOnly;
+ }
+
+ public int getType() {
+ return mTvInputInfo.getType();
+ }
+
+ @VisibleForTesting
+ public Map<String, String> getExtras() {
+ ServiceInfo si = mTvInputInfo.getServiceInfo();
+
+ try {
+ XmlPullParser parser = getXmlResourceParser();
+ int type;
+ while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+ && type != XmlPullParser.START_TAG) {
+ }
+
+ if (!TV_INPUT_XML_START_TAG_NAME.equals(parser.getName())) {
+ Log.w(TAG, "Meta-data does not start with " + TV_INPUT_XML_START_TAG_NAME
+ + " tag for " + si.name);
+ return Collections.emptyMap();
+ }
+ // <tv-input> start tag found
+ Map<String, String> extras = new HashMap<>();
+ while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
+ if (type == XmlPullParser.END_TAG
+ && TV_INPUT_XML_START_TAG_NAME.equals(parser.getName())) {
+ // </tv-input> end tag found
+ return extras;
+ }
+ if (type == XmlPullParser.START_TAG
+ && TV_INPUT_EXTRA_XML_START_TAG_NAME.equals(parser.getName())) {
+ String extraName =
+ parser.getAttributeValue(ATTRIBUTE_NAMESPACE_ANDROID, ATTRIBUTE_NAME);
+ String extraValue =
+ parser.getAttributeValue(ATTRIBUTE_NAMESPACE_ANDROID, ATTRIBUTE_VALUE);
+ if (extraName != null && extraValue != null) {
+ extras.put(extraName, extraValue);
+ }
+ }
+ }
+
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to get extras of " + mTvInputInfo.getId() , e);
+ }
+ return Collections.emptyMap();
+ }
+
+ @VisibleForTesting
+ XmlPullParser getXmlResourceParser() {
+ ServiceInfo si = mTvInputInfo.getServiceInfo();
+ PackageManager pm = mContext.getPackageManager();
+ return si.loadXmlMetaData(pm, TvInputService.SERVICE_META_DATA);
+ }
+}
diff --git a/common/src/com/android/tv/common/compat/TvRecordingClientCompat.java b/common/src/com/android/tv/common/compat/TvRecordingClientCompat.java
new file mode 100644
index 00000000..143ff25c
--- /dev/null
+++ b/common/src/com/android/tv/common/compat/TvRecordingClientCompat.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.tv.common.compat;
+
+import android.content.Context;
+import android.media.tv.TvRecordingClient;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.annotation.RequiresApi;
+import android.util.ArrayMap;
+import com.android.tv.common.compat.api.PrivateCommandSender;
+import com.android.tv.common.compat.api.RecordingClientCallbackCompatEvents;
+import com.android.tv.common.compat.api.TvRecordingClientCompatCommands;
+import com.android.tv.common.compat.internal.RecordingClientCompatProcessor;
+
+/**
+ * TIF Compatibility for {@link TvRecordingClient}.
+ *
+ * <p>Extends {@code TvRecordingClient} in a backwards compatible way.
+ */
+@RequiresApi(api = VERSION_CODES.N)
+public class TvRecordingClientCompat extends TvRecordingClient
+ implements TvRecordingClientCompatCommands, PrivateCommandSender {
+
+ private final RecordingClientCompatProcessor mProcessor;
+
+ /**
+ * Creates a new TvRecordingClient object.
+ *
+ * @param context The application context to create a TvRecordingClient with.
+ * @param tag A short name for debugging purposes.
+ * @param callback The callback to receive recording status changes.
+ * @param handler The handler to invoke the callback on.
+ */
+ public TvRecordingClientCompat(
+ Context context, String tag, RecordingCallback callback, Handler handler) {
+ super(context, tag, callback, handler);
+ RecordingCallbackCompat compatEvents =
+ callback instanceof RecordingCallbackCompat
+ ? (RecordingCallbackCompat) callback
+ : null;
+ mProcessor = new RecordingClientCompatProcessor(this, compatEvents);
+ if (compatEvents != null) {
+ compatEvents.mClientCompatProcessor = mProcessor;
+ }
+ }
+
+ /** Tell the session to Display a debug message dev builds only. */
+ @Override
+ public void devMessage(String message) {
+ mProcessor.devMessage(message);
+ }
+
+ /**
+ * TIF Compatibility for {@link RecordingCallback}.
+ *
+ * <p>Extends {@code RecordingCallback} in a backwards compatible way.
+ */
+ public static class RecordingCallbackCompat extends RecordingCallback
+ implements RecordingClientCallbackCompatEvents {
+ private final ArrayMap<String, Integer> inputCompatVersionMap = new ArrayMap<>();
+ private RecordingClientCompatProcessor mClientCompatProcessor;
+
+ @Override
+ public void onEvent(String inputId, String eventType, Bundle eventArgs) {
+ if (mClientCompatProcessor != null
+ && !mClientCompatProcessor.handleEvent(inputId, eventType, eventArgs)) {
+ super.onEvent(inputId, eventType, eventArgs);
+ }
+ }
+
+ public int getTifCompatVersionForInput(String inputId) {
+ return inputCompatVersionMap.containsKey(inputId)
+ ? inputCompatVersionMap.get(inputId)
+ : 0;
+ }
+
+ /** Display a message as a toast on dev builds only. */
+ @Override
+ public void onDevToast(String inputId, String message) {}
+
+ /** Recording started. */
+ @Override
+ public void onRecordingStarted(String inputId, String recUri) {}
+ }
+}
diff --git a/common/src/com/android/tv/common/compat/TvViewCompat.java b/common/src/com/android/tv/common/compat/TvViewCompat.java
new file mode 100644
index 00000000..f44564d3
--- /dev/null
+++ b/common/src/com/android/tv/common/compat/TvViewCompat.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.tv.common.compat;
+
+import android.content.Context;
+import android.media.tv.TvView;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.support.annotation.RequiresApi;
+import android.util.ArrayMap;
+import android.util.AttributeSet;
+import com.android.tv.common.compat.api.PrivateCommandSender;
+import com.android.tv.common.compat.api.TvInputCallbackCompatEvents;
+import com.android.tv.common.compat.api.TvViewCompatCommands;
+import com.android.tv.common.compat.internal.TvViewCompatProcessor;
+
+/**
+ * TIF Compatibility for {@link TvView}.
+ *
+ * <p>Extends {@code TvView} in a backwards compatible way.
+ */
+@RequiresApi(api = VERSION_CODES.LOLLIPOP)
+public class TvViewCompat extends TvView implements TvViewCompatCommands, PrivateCommandSender {
+
+ private final TvViewCompatProcessor mTvViewCompatProcessor;
+
+ public TvViewCompat(Context context) {
+ this(context, null);
+ }
+
+ public TvViewCompat(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public TvViewCompat(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ mTvViewCompatProcessor = new TvViewCompatProcessor(this);
+ }
+
+ @Override
+ public void setCallback(TvInputCallback callback) {
+ super.setCallback(callback);
+ if (callback instanceof TvInputCallbackCompat) {
+ TvInputCallbackCompat compatEvents = (TvInputCallbackCompat) callback;
+ mTvViewCompatProcessor.setCallback(compatEvents);
+ compatEvents.mTvViewCompatProcessor = mTvViewCompatProcessor;
+ }
+ }
+
+ @Override
+ public void devMessage(String message) {
+ mTvViewCompatProcessor.devMessage(message);
+ }
+
+ /**
+ * TIF Compatibility for {@link TvInputCallback}.
+ *
+ * <p>Extends {@code TvInputCallback} in a backwards compatible way.
+ */
+ public static class TvInputCallbackCompat extends TvInputCallback
+ implements TvInputCallbackCompatEvents {
+ private final ArrayMap<String, Integer> inputCompatVersionMap = new ArrayMap<>();
+ private TvViewCompatProcessor mTvViewCompatProcessor;
+
+ @Override
+ public void onEvent(String inputId, String eventType, Bundle eventArgs) {
+ if (mTvViewCompatProcessor != null
+ && !mTvViewCompatProcessor.handleEvent(inputId, eventType, eventArgs)) {
+ super.onEvent(inputId, eventType, eventArgs);
+ }
+ }
+
+ public int getTifCompatVersionForInput(String inputId) {
+ return inputCompatVersionMap.containsKey(inputId)
+ ? inputCompatVersionMap.get(inputId)
+ : 0;
+ }
+
+ @Override
+ public void onDevToast(String inputId, String message) {}
+
+ /**
+ * This is called when the signal strength is notified.
+ *
+ * @param inputId The ID of the TV input bound to this view.
+ * @param value The current signal strength. Should be one of the followings.
+ * <ul>
+ * <li>{@link TvInputConstantCompat#SIGNAL_STRENGTH_NOT_USED}
+ * <li>{@link TvInputConstantCompat#SIGNAL_STRENGTH_ERROR}
+ * <li>{@link TvInputConstantCompat#SIGNAL_STRENGTH_UNKNOWN}
+ * <li>{int [0, 100]}
+ * </ul>
+ */
+ @Override
+ public void onSignalStrength(String inputId, int value) {}
+ }
+}
diff --git a/common/src/com/android/tv/common/compat/api/PrivateCommandSender.java b/common/src/com/android/tv/common/compat/api/PrivateCommandSender.java
new file mode 100644
index 00000000..11de970f
--- /dev/null
+++ b/common/src/com/android/tv/common/compat/api/PrivateCommandSender.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.tv.common.compat.api;
+
+import android.os.Bundle;
+
+/** Sends a command from the TV App to a {@link android.media.tv.TvInputService.Session} */
+public interface PrivateCommandSender {
+
+ void sendAppPrivateCommand(String action, Bundle data);
+}
diff --git a/common/src/com/android/tv/common/compat/api/RecordingClientCallbackCompatEvents.java b/common/src/com/android/tv/common/compat/api/RecordingClientCallbackCompatEvents.java
new file mode 100644
index 00000000..753703c4
--- /dev/null
+++ b/common/src/com/android/tv/common/compat/api/RecordingClientCallbackCompatEvents.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.tv.common.compat.api;
+
+/**
+ * {@link android.media.tv.TvRecordingClient} implements this to receive notification from a {@link
+ * android.media.tv.TvInputService.RecordingSession}
+ */
+public interface RecordingClientCallbackCompatEvents {
+ /** Display a message in the application as a toast on dev builds only */
+ void onDevToast(String inputId, String message);
+
+ void onRecordingStarted(String inputId, String recUri);
+}
diff --git a/common/src/com/android/tv/common/compat/api/RecordingSessionCompatCommands.java b/common/src/com/android/tv/common/compat/api/RecordingSessionCompatCommands.java
new file mode 100644
index 00000000..9deaa41f
--- /dev/null
+++ b/common/src/com/android/tv/common/compat/api/RecordingSessionCompatCommands.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.tv.common.compat.api;
+
+/** Commands sent from the TV App to {@link android.media.tv.TvInputService.RecordingSession} */
+public interface RecordingSessionCompatCommands {
+
+ void onDevMessage(String message);
+}
diff --git a/common/src/com/android/tv/common/compat/api/RecordingSessionCompatEvents.java b/common/src/com/android/tv/common/compat/api/RecordingSessionCompatEvents.java
new file mode 100644
index 00000000..812bba62
--- /dev/null
+++ b/common/src/com/android/tv/common/compat/api/RecordingSessionCompatEvents.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.tv.common.compat.api;
+
+/** Events sent from the {@link android.media.tv.TvInputService.RecordingSession} to the TV App. */
+public interface RecordingSessionCompatEvents {
+
+ void notifyDevToast(String message);
+
+ void notifyRecordingStarted(String value);
+}
diff --git a/common/src/com/android/tv/common/compat/api/SessionCompatCommands.java b/common/src/com/android/tv/common/compat/api/SessionCompatCommands.java
new file mode 100644
index 00000000..bef4ad27
--- /dev/null
+++ b/common/src/com/android/tv/common/compat/api/SessionCompatCommands.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.tv.common.compat.api;
+
+/** Commands sent from the TV App to {@link android.media.tv.TvInputService.Session} */
+public interface SessionCompatCommands {
+
+ void onDevMessage(String message);
+}
diff --git a/common/src/com/android/tv/common/compat/api/SessionCompatEvents.java b/common/src/com/android/tv/common/compat/api/SessionCompatEvents.java
new file mode 100644
index 00000000..a3af8f3c
--- /dev/null
+++ b/common/src/com/android/tv/common/compat/api/SessionCompatEvents.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.tv.common.compat.api;
+
+/** Events sent from the {@link android.media.tv.TvInputService.Session} to the TV App. */
+public interface SessionCompatEvents {
+
+ void notifyDevToast(String message);
+
+ void notifySignalStrength(int value);
+}
diff --git a/common/src/com/android/tv/common/compat/api/SessionEventNotifier.java b/common/src/com/android/tv/common/compat/api/SessionEventNotifier.java
new file mode 100644
index 00000000..66c5c3a8
--- /dev/null
+++ b/common/src/com/android/tv/common/compat/api/SessionEventNotifier.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.tv.common.compat.api;
+
+import android.os.Bundle;
+
+/** Sends events from a {@link android.media.tv.TvInputService.Session} to the TV App. */
+public interface SessionEventNotifier {
+
+ void notifySessionEvent(String event, Bundle data);
+}
diff --git a/common/src/com/android/tv/common/compat/api/TvInputCallbackCompatEvents.java b/common/src/com/android/tv/common/compat/api/TvInputCallbackCompatEvents.java
new file mode 100644
index 00000000..e6b241b7
--- /dev/null
+++ b/common/src/com/android/tv/common/compat/api/TvInputCallbackCompatEvents.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.tv.common.compat.api;
+
+/**
+ * {@link android.media.tv.TvView.TvInputCallback} implements this to receive notification from a
+ * {@link android.media.tv.TvInputService.Session}
+ */
+public interface TvInputCallbackCompatEvents {
+ void onDevToast(String inputId, String message);
+
+ void onSignalStrength(String inputId, int value);
+}
diff --git a/common/src/com/android/tv/common/compat/api/TvRecordingClientCompatCommands.java b/common/src/com/android/tv/common/compat/api/TvRecordingClientCompatCommands.java
new file mode 100644
index 00000000..c1852165
--- /dev/null
+++ b/common/src/com/android/tv/common/compat/api/TvRecordingClientCompatCommands.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.tv.common.compat.api;
+
+/** Commands sent from the TV App to {@link android.media.tv.TvInputService.RecordingSession} */
+public interface TvRecordingClientCompatCommands {
+
+ void devMessage(String message);
+}
diff --git a/common/src/com/android/tv/common/compat/api/TvViewCompatCommands.java b/common/src/com/android/tv/common/compat/api/TvViewCompatCommands.java
new file mode 100644
index 00000000..5abc6bce
--- /dev/null
+++ b/common/src/com/android/tv/common/compat/api/TvViewCompatCommands.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.tv.common.compat.api;
+
+/** Commands sent from the TV App to {@link android.media.tv.TvInputService.Session} */
+public interface TvViewCompatCommands {
+
+ void devMessage(String message);
+}
diff --git a/common/src/com/android/tv/common/compat/internal/Constants.java b/common/src/com/android/tv/common/compat/internal/Constants.java
new file mode 100644
index 00000000..993822c1
--- /dev/null
+++ b/common/src/com/android/tv/common/compat/internal/Constants.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.tv.common.compat.internal;
+/** Static constants use by the TIF compat library */
+final class Constants {
+ static final String ACTION_GET_VERSION = "com.android.tv.common.compat.action.GET_VERSION";
+ static final String EVENT_GET_VERSION = "com.android.tv.common.compat.event.GET_VERSION";
+ static final String ACTION_COMPAT_ON = "com.android.tv.common.compat.action.COMPAT_ON";
+ static final String EVENT_COMPAT_NOTIFY = "com.android.tv.common.compat.event.COMPAT_NOTIFY";
+ static final String EVENT_COMPAT_NOTIFY_ERROR =
+ "com.android.tv.common.compat.event.COMPAT_NOTIFY_ERROR";
+ static final int TIF_COMPAT_VERSION = 1;
+
+ private Constants() {}
+}
diff --git a/common/src/com/android/tv/common/compat/internal/RecordingClientCompatProcessor.java b/common/src/com/android/tv/common/compat/internal/RecordingClientCompatProcessor.java
new file mode 100644
index 00000000..f83228c8
--- /dev/null
+++ b/common/src/com/android/tv/common/compat/internal/RecordingClientCompatProcessor.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.tv.common.compat.internal;
+
+import android.support.annotation.Nullable;
+import android.util.Log;
+import com.android.tv.common.compat.api.PrivateCommandSender;
+import com.android.tv.common.compat.api.RecordingClientCallbackCompatEvents;
+import com.android.tv.common.compat.api.TvViewCompatCommands;
+import com.android.tv.common.compat.internal.Commands.OnDevMessage;
+import com.android.tv.common.compat.internal.Commands.PrivateCommand;
+import com.android.tv.common.compat.internal.RecordingEvents.NotifyDevToast;
+import com.android.tv.common.compat.internal.RecordingEvents.RecordingSessionEvent;
+
+/**
+ * Sends {@link RecordingCommands} to the {@link android.media.tv.TvInputService.RecordingSession}
+ * via {@link PrivateCommandSender} and receives notification events from the session forwarding
+ * them to {@link RecordingClientCallbackCompatEvents}
+ */
+public final class RecordingClientCompatProcessor
+ extends ViewCompatProcessor<PrivateCommand, RecordingSessionEvent>
+ implements TvViewCompatCommands {
+ private static final String TAG = "RecordingClientCompatProcessor";
+
+ @Nullable private final RecordingClientCallbackCompatEvents mCallback;
+
+ public RecordingClientCompatProcessor(
+ PrivateCommandSender commandSender,
+ @Nullable RecordingClientCallbackCompatEvents callback) {
+ super(commandSender, RecordingSessionEvent.parser());
+ mCallback = callback;
+ }
+
+ @Override
+ public void devMessage(String message) {
+ OnDevMessage devMessage = OnDevMessage.newBuilder().setMessage(message).build();
+ PrivateCommand privateCommand =
+ createPrivateCommandCommand().setOnDevMessage(devMessage).build();
+ sendCompatCommand(privateCommand);
+ }
+
+ private PrivateCommand.Builder createPrivateCommandCommand() {
+ return PrivateCommand.newBuilder().setCompatVersion(Constants.TIF_COMPAT_VERSION);
+ }
+
+ @Override
+ protected final void handleSessionEvent(String inputId, RecordingSessionEvent sessionEvent) {
+ switch (sessionEvent.getEventCase()) {
+ case NOTIFY_DEV_MESSAGE:
+ handle(inputId, sessionEvent.getNotifyDevMessage());
+ break;
+ case RECORDING_STARTED:
+ handle(inputId, sessionEvent.getRecordingStarted());
+ break;
+
+ case EVENT_NOT_SET:
+ Log.w(TAG, "Error event not set compat notify ");
+ }
+ }
+
+ private void handle(String inputId, NotifyDevToast devToast) {
+ if (devToast != null && mCallback != null) {
+ mCallback.onDevToast(inputId, devToast.getMessage());
+ }
+ }
+
+ private void handle(String inputId, RecordingEvents.RecordingStarted recStart) {
+ if (recStart != null && mCallback != null) {
+ mCallback.onRecordingStarted(inputId, recStart.getUri());
+ }
+ }
+}
diff --git a/common/src/com/android/tv/common/compat/internal/RecordingSessionCompatProcessor.java b/common/src/com/android/tv/common/compat/internal/RecordingSessionCompatProcessor.java
new file mode 100644
index 00000000..84ec5503
--- /dev/null
+++ b/common/src/com/android/tv/common/compat/internal/RecordingSessionCompatProcessor.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.tv.common.compat.internal;
+
+import android.util.Log;
+import com.android.tv.common.compat.api.RecordingSessionCompatCommands;
+import com.android.tv.common.compat.api.RecordingSessionCompatEvents;
+import com.android.tv.common.compat.api.SessionEventNotifier;
+import com.android.tv.common.compat.internal.RecordingCommands.PrivateRecordingCommand;
+import com.android.tv.common.compat.internal.RecordingEvents.NotifyDevToast;
+import com.android.tv.common.compat.internal.RecordingEvents.RecordingSessionEvent;
+import com.android.tv.common.compat.internal.RecordingEvents.RecordingStarted;
+
+/**
+ * Sends {@link RecordingSessionCompatEvents} to the TV App via {@link SessionEventNotifier} and
+ * receives Commands from TV App forwarding them to {@link RecordingSessionCompatProcessor}
+ */
+public final class RecordingSessionCompatProcessor
+ extends SessionCompatProcessor<PrivateRecordingCommand, RecordingSessionEvent>
+ implements RecordingSessionCompatEvents {
+
+ private static final String TAG = "RecordingSessionCompatProc";
+
+ private final RecordingSessionCompatCommands mRecordingSessionOnCompat;
+
+ public RecordingSessionCompatProcessor(
+ SessionEventNotifier sessionEventNotifier,
+ RecordingSessionCompatCommands recordingSessionOnCompat) {
+ super(sessionEventNotifier, PrivateRecordingCommand.parser());
+ mRecordingSessionOnCompat = recordingSessionOnCompat;
+ }
+
+ @Override
+ protected void onCompat(PrivateRecordingCommand privateCommand) {
+ switch (privateCommand.getCommandCase()) {
+ case ON_DEV_MESSAGE:
+ mRecordingSessionOnCompat.onDevMessage(
+ privateCommand.getOnDevMessage().getMessage());
+ break;
+ case COMMAND_NOT_SET:
+ Log.w(TAG, "Command not set ");
+ }
+ }
+
+ @Override
+ public void notifyDevToast(String message) {
+ NotifyDevToast devMessage = NotifyDevToast.newBuilder().setMessage(message).build();
+ RecordingSessionEvent sessionEvent =
+ createSessionEvent().setNotifyDevMessage(devMessage).build();
+ notifyCompat(sessionEvent);
+ }
+
+ @Override
+ public void notifyRecordingStarted(String uri) {
+ RecordingStarted event = RecordingStarted.newBuilder().setUri(uri).build();
+ RecordingSessionEvent sessionEvent =
+ createSessionEvent().setRecordingStarted(event).build();
+ notifyCompat(sessionEvent);
+ }
+
+ private RecordingSessionEvent.Builder createSessionEvent() {
+
+ return RecordingSessionEvent.newBuilder().setCompatVersion(Constants.TIF_COMPAT_VERSION);
+ }
+}
diff --git a/common/src/com/android/tv/common/compat/internal/SessionCompatProcessor.java b/common/src/com/android/tv/common/compat/internal/SessionCompatProcessor.java
new file mode 100644
index 00000000..7f27a243
--- /dev/null
+++ b/common/src/com/android/tv/common/compat/internal/SessionCompatProcessor.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.tv.common.compat.internal;
+
+import android.os.Bundle;
+import android.util.Log;
+import com.android.tv.common.compat.api.SessionEventNotifier;
+import com.google.protobuf.GeneratedMessageLite;
+import com.google.protobuf.InvalidProtocolBufferException;
+import com.google.protobuf.Parser;
+
+/**
+ * Sends {@code events} to the TV App via {@link SessionEventNotifier} and receives {@code commands}
+ * from TV App.
+ */
+abstract class SessionCompatProcessor<
+ C extends GeneratedMessageLite<C, ?>, E extends GeneratedMessageLite<E, ?>> {
+ private static final String TAG = "SessionCompatProcessor";
+ private final SessionEventNotifier mSessionEventNotifier;
+ private final Parser<C> mCommandParser;
+
+ SessionCompatProcessor(SessionEventNotifier sessionEventNotifier, Parser<C> commandParser) {
+ mSessionEventNotifier = sessionEventNotifier;
+ mCommandParser = commandParser;
+ }
+
+ public final boolean handleAppPrivateCommand(String action, Bundle data) {
+ switch (action) {
+ case Constants.ACTION_GET_VERSION:
+ Bundle response = new Bundle();
+ response.putInt(Constants.EVENT_GET_VERSION, Constants.TIF_COMPAT_VERSION);
+ mSessionEventNotifier.notifySessionEvent(Constants.EVENT_GET_VERSION, response);
+ return true;
+ case Constants.ACTION_COMPAT_ON:
+ byte[] bytes = data.getByteArray(Constants.ACTION_COMPAT_ON);
+ try {
+ C privateCommand = mCommandParser.parseFrom(bytes);
+ onCompat(privateCommand);
+ } catch (InvalidProtocolBufferException e) {
+ Log.w(TAG, "Error parsing compat data", e);
+ }
+
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ abstract void onCompat(C privateCommand);
+
+ final void notifyCompat(E event) {
+ Bundle response = new Bundle();
+ try {
+ byte[] bytes = event.toByteArray();
+ response.putByteArray(Constants.EVENT_COMPAT_NOTIFY, bytes);
+ } catch (Exception e) {
+ Log.w(TAG, "Failed to send " + event, e);
+ response.putString(Constants.EVENT_COMPAT_NOTIFY_ERROR, e.getMessage());
+ }
+ mSessionEventNotifier.notifySessionEvent(Constants.EVENT_COMPAT_NOTIFY, response);
+ }
+}
diff --git a/common/src/com/android/tv/common/compat/internal/TifSessionCompatProcessor.java b/common/src/com/android/tv/common/compat/internal/TifSessionCompatProcessor.java
new file mode 100644
index 00000000..dd7a3b3e
--- /dev/null
+++ b/common/src/com/android/tv/common/compat/internal/TifSessionCompatProcessor.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.tv.common.compat.internal;
+
+import android.util.Log;
+import com.android.tv.common.compat.api.SessionCompatCommands;
+import com.android.tv.common.compat.api.SessionCompatEvents;
+import com.android.tv.common.compat.api.SessionEventNotifier;
+import com.android.tv.common.compat.internal.Commands.PrivateCommand;
+import com.android.tv.common.compat.internal.Events.NotifyDevToast;
+import com.android.tv.common.compat.internal.Events.NotifySignalStrength;
+import com.android.tv.common.compat.internal.Events.SessionEvent;
+
+/**
+ * Sends {@link SessionCompatEvents} to the TV App via {@link SessionEventNotifier} and receives
+ * Commands from TV App forwarding them to {@link SessionCompatCommands}
+ */
+public final class TifSessionCompatProcessor
+ extends SessionCompatProcessor<PrivateCommand, SessionEvent>
+ implements SessionCompatEvents {
+
+ private static final String TAG = "TifSessionCompatProcessor";
+
+ private final SessionCompatCommands mSessionOnCompat;
+
+ public TifSessionCompatProcessor(
+ SessionEventNotifier sessionEventNotifier, SessionCompatCommands sessionOnCompat) {
+ super(sessionEventNotifier, PrivateCommand.parser());
+ mSessionOnCompat = sessionOnCompat;
+ }
+
+ @Override
+ protected void onCompat(Commands.PrivateCommand privateCommand) {
+ switch (privateCommand.getCommandCase()) {
+ case ON_DEV_MESSAGE:
+ mSessionOnCompat.onDevMessage(privateCommand.getOnDevMessage().getMessage());
+ break;
+ case COMMAND_NOT_SET:
+ Log.w(TAG, "Command not set ");
+ }
+ }
+
+ @Override
+ public void notifyDevToast(String message) {
+ NotifyDevToast devMessage = NotifyDevToast.newBuilder().setMessage(message).build();
+ SessionEvent sessionEvent = createSessionEvent().setNotifyDevMessage(devMessage).build();
+ notifyCompat(sessionEvent);
+ }
+
+ @Override
+ public void notifySignalStrength(int value) {
+ NotifySignalStrength signalStrength =
+ NotifySignalStrength.newBuilder().setSignalStrength(value).build();
+ SessionEvent sessionEvent =
+ createSessionEvent().setNotifySignalStrength(signalStrength).build();
+ notifyCompat(sessionEvent);
+ }
+
+ private SessionEvent.Builder createSessionEvent() {
+ return SessionEvent.newBuilder().setCompatVersion(Constants.TIF_COMPAT_VERSION);
+ }
+}
diff --git a/common/src/com/android/tv/common/compat/internal/TvViewCompatProcessor.java b/common/src/com/android/tv/common/compat/internal/TvViewCompatProcessor.java
new file mode 100644
index 00000000..382f8d8a
--- /dev/null
+++ b/common/src/com/android/tv/common/compat/internal/TvViewCompatProcessor.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.tv.common.compat.internal;
+
+import android.support.annotation.NonNull;
+import android.util.Log;
+import com.android.tv.common.compat.api.PrivateCommandSender;
+import com.android.tv.common.compat.api.TvInputCallbackCompatEvents;
+import com.android.tv.common.compat.api.TvViewCompatCommands;
+import com.android.tv.common.compat.internal.Commands.OnDevMessage;
+import com.android.tv.common.compat.internal.Commands.PrivateCommand;
+import com.android.tv.common.compat.internal.Events.NotifyDevToast;
+import com.android.tv.common.compat.internal.Events.NotifySignalStrength;
+import com.android.tv.common.compat.internal.Events.SessionEvent;
+
+/**
+ * Sends {@link TvViewCompatCommands} to the {@link android.media.tv.TvInputService.Session} via
+ * {@link PrivateCommandSender} and receives notification events from the session forwarding them to
+ * {@link TvInputCallbackCompatEvents}
+ */
+public final class TvViewCompatProcessor extends ViewCompatProcessor<PrivateCommand, SessionEvent>
+ implements TvViewCompatCommands {
+ private static final String TAG = "TvViewCompatProcessor";
+
+ private TvInputCallbackCompatEvents mCallback;
+
+ public TvViewCompatProcessor(PrivateCommandSender commandSender) {
+ super(commandSender, SessionEvent.parser());
+ }
+
+ @Override
+ public void devMessage(String message) {
+ OnDevMessage devMessage = Commands.OnDevMessage.newBuilder().setMessage(message).build();
+ Commands.PrivateCommand privateCommand =
+ createPrivateCommandCommand().setOnDevMessage(devMessage).build();
+ sendCompatCommand(privateCommand);
+ }
+
+ @NonNull
+ private PrivateCommand.Builder createPrivateCommandCommand() {
+ return PrivateCommand.newBuilder().setCompatVersion(Constants.TIF_COMPAT_VERSION);
+ }
+
+ public void onDevToast(String inputId, String message) {}
+
+ public void onSignalStrength(String inputId, int value) {}
+
+ @Override
+ protected final void handleSessionEvent(String inputId, Events.SessionEvent sessionEvent) {
+ switch (sessionEvent.getEventCase()) {
+ case NOTIFY_DEV_MESSAGE:
+ handle(inputId, sessionEvent.getNotifyDevMessage());
+ break;
+ case NOTIFY_SIGNAL_STRENGTH:
+ handle(inputId, sessionEvent.getNotifySignalStrength());
+ break;
+ case EVENT_NOT_SET:
+ Log.w(TAG, "Error event not set compat notify ");
+ }
+ }
+
+ private void handle(String inputId, NotifyDevToast devToast) {
+ if (devToast != null && mCallback != null) {
+ mCallback.onDevToast(inputId, devToast.getMessage());
+ }
+ }
+
+ private void handle(String inputId, NotifySignalStrength signalStrength) {
+ if (signalStrength != null && mCallback != null) {
+ mCallback.onSignalStrength(inputId, signalStrength.getSignalStrength());
+ }
+ }
+
+ public void setCallback(TvInputCallbackCompatEvents callback) {
+ this.mCallback = callback;
+ }
+}
diff --git a/common/src/com/android/tv/common/compat/internal/ViewCompatProcessor.java b/common/src/com/android/tv/common/compat/internal/ViewCompatProcessor.java
new file mode 100644
index 00000000..dc6bbfa5
--- /dev/null
+++ b/common/src/com/android/tv/common/compat/internal/ViewCompatProcessor.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.tv.common.compat.internal;
+
+import android.os.Bundle;
+import android.util.ArrayMap;
+import android.util.Log;
+import com.android.tv.common.compat.api.PrivateCommandSender;
+import com.google.protobuf.GeneratedMessageLite;
+import com.google.protobuf.InvalidProtocolBufferException;
+import com.google.protobuf.Parser;
+
+/**
+ * Sends {@code commands} to the {@code session} via {@link PrivateCommandSender} and receives
+ * notification events from the session forwarding them to {@link
+ * com.android.tv.common.compat.api.TvInputCallbackCompatEvents}
+ */
+abstract class ViewCompatProcessor<
+ C extends GeneratedMessageLite<C, ?>, E extends GeneratedMessageLite<E, ?>> {
+ private static final String TAG = "ViewCompatProcessor";
+ private final ArrayMap<String, Integer> inputCompatVersionMap = new ArrayMap<>();
+
+ private final Parser<E> mEventParser;
+ private final PrivateCommandSender mCommandSender;
+
+ ViewCompatProcessor(PrivateCommandSender commandSender, Parser<E> eventParser) {
+ mCommandSender = commandSender;
+ mEventParser = eventParser;
+ }
+
+ private final E sessionEventFromBundle(Bundle eventArgs) throws InvalidProtocolBufferException {
+
+ byte[] protoBytes = eventArgs.getByteArray(Constants.EVENT_COMPAT_NOTIFY);
+ return protoBytes == null || protoBytes.length == 0
+ ? null
+ : mEventParser.parseFrom(protoBytes);
+ }
+
+ final void sendCompatCommand(C privateCommand) {
+ try {
+ Bundle data = new Bundle();
+ data.putByteArray(Constants.ACTION_COMPAT_ON, privateCommand.toByteArray());
+ mCommandSender.sendAppPrivateCommand(Constants.ACTION_COMPAT_ON, data);
+ } catch (Exception e) {
+ Log.w(TAG, "Error sending compat action " + privateCommand, e);
+ }
+ }
+
+ public boolean handleEvent(String inputId, String eventType, Bundle eventArgs) {
+ switch (eventType) {
+ case Constants.EVENT_GET_VERSION:
+ int version = eventArgs.getInt(Constants.EVENT_GET_VERSION, 0);
+ inputCompatVersionMap.put(inputId, version);
+ return true;
+ case Constants.EVENT_COMPAT_NOTIFY:
+ try {
+ E sessionEvent = sessionEventFromBundle(eventArgs);
+ if (sessionEvent != null) {
+ handleSessionEvent(inputId, sessionEvent);
+ } else {
+ String errorMessage =
+ eventArgs.getString(Constants.EVENT_COMPAT_NOTIFY_ERROR);
+ Log.w(TAG, "Error sent in compat notify " + errorMessage);
+ }
+
+ } catch (InvalidProtocolBufferException e) {
+ Log.w(TAG, "Error parsing in compat notify for " + inputId);
+ }
+
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ protected abstract void handleSessionEvent(String inputId, E sessionEvent);
+}
diff --git a/common/src/com/android/tv/common/compat/internal/recording_commands.proto b/common/src/com/android/tv/common/compat/internal/recording_commands.proto
new file mode 100644
index 00000000..ce59bfa0
--- /dev/null
+++ b/common/src/com/android/tv/common/compat/internal/recording_commands.proto
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// A set of Private Commands to send to a TVInputService.Session, in particular
+// support new features on older devices. NOTE: this proto is internal to this
+// package and should not be used outside it.
+
+syntax = "proto3";
+package android.tv.common.compat.internal;
+
+option java_outer_classname = "RecordingCommands";
+option java_package = "com.android.tv.common.compat.internal";
+
+// Wraps messages for sending to a session a private command.
+message PrivateRecordingCommand {
+ uint32 compat_version = 1;
+
+ oneof command {
+ OnDevMessage on_dev_message = 2;
+ }
+}
+
+// Display a debug message dev builds only.
+message OnDevMessage {
+ string message = 1;
+}
diff --git a/common/src/com/android/tv/common/compat/internal/recording_events.proto b/common/src/com/android/tv/common/compat/internal/recording_events.proto
new file mode 100644
index 00000000..68db5ddf
--- /dev/null
+++ b/common/src/com/android/tv/common/compat/internal/recording_events.proto
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// A set of Session events to send from a TVInputService.Session, in particular
+// support new features on older devices. NOTE: this proto is internal to this
+// package and should not be used outside it.
+syntax = "proto3";
+package android.tv.common.compat.internal;
+
+option java_outer_classname = "RecordingEvents";
+option java_package = "com.android.tv.common.compat.internal";
+
+// Wraps messages for sending from a session as an Event.
+// RecordingSessionCompat will have a notify{EventMessageName} for each event
+// TvRecordingClientCompat will have a on{EventMessageName} for each event
+
+message RecordingSessionEvent {
+ uint32 compat_version = 1;
+
+ oneof event {
+ NotifyDevToast notify_dev_message = 2;
+ RecordingStarted recording_started = 3;
+ }
+}
+
+// Display a message as a toast on dev builds only
+message NotifyDevToast {
+ string message = 1;
+}
+
+// Recording started.
+message RecordingStarted {
+ // Recording URI.
+ string uri = 1;
+}
+
diff --git a/common/src/com/android/tv/common/compat/internal/tif_commands.proto b/common/src/com/android/tv/common/compat/internal/tif_commands.proto
new file mode 100644
index 00000000..d5867703
--- /dev/null
+++ b/common/src/com/android/tv/common/compat/internal/tif_commands.proto
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// A set of Private Commands to send to a TVInputService.Session, in particular
+// support new features on older devices. NOTE: this proto is internal to this
+// package and should not be used outside it.
+
+syntax = "proto3";
+package android.tv.common.compat.internal;
+
+option java_outer_classname = "Commands";
+option java_package = "com.android.tv.common.compat.internal";
+
+// Wraps messages for sending to a session a private command.
+message PrivateCommand {
+ uint32 compat_version = 1;
+
+ oneof command {
+ OnDevMessage on_dev_message = 2;
+ }
+}
+
+// Sends a debug message to the session for display on dev builds only
+message OnDevMessage {
+ string message = 1;
+}
diff --git a/common/src/com/android/tv/common/compat/internal/tif_events.proto b/common/src/com/android/tv/common/compat/internal/tif_events.proto
new file mode 100644
index 00000000..6e71ae11
--- /dev/null
+++ b/common/src/com/android/tv/common/compat/internal/tif_events.proto
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// A set of Session events to send from a TVInputService.Session, in particular
+// support new features on older devices. NOTE: this proto is internal to this
+// package and should not be used outside it.
+syntax = "proto3";
+package android.tv.common.compat.internal;
+
+option java_outer_classname = "Events";
+option java_package = "com.android.tv.common.compat.internal";
+
+// Wraps messages for sending from a session as an Event.
+message SessionEvent {
+ uint32 compat_version = 1;
+
+ oneof event {
+ NotifyDevToast notify_dev_message = 2;
+ NotifySignalStrength notify_signal_strength = 3;
+ }
+}
+
+// Send a message to the application to be displayed as a toast on dev builds
+// only
+message NotifyDevToast {
+ string message = 1;
+}
+
+// Notifies the TV Application the current signal strength.
+message NotifySignalStrength {
+ // The signal strength as a percent (0 to 100),
+ // with -1 meaning unknown, -2 meaning not used.
+ int32 signal_strength = 1;
+}
diff --git a/common/src/com/android/tv/common/config/DefaultConfigManager.java b/common/src/com/android/tv/common/config/DefaultConfigManager.java
deleted file mode 100644
index ae240855..00000000
--- a/common/src/com/android/tv/common/config/DefaultConfigManager.java
+++ /dev/null
@@ -1,60 +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.common.config;
-
-import android.content.Context;
-import com.android.tv.common.config.api.RemoteConfig;
-
-/** Stub Remote Config. */
-public class DefaultConfigManager {
- public static final long DEFAULT_LONG_VALUE = 0;
-
- public static DefaultConfigManager createInstance(Context context) {
- return new DefaultConfigManager();
- }
-
- private StubRemoteConfig mRemoteConfig = new StubRemoteConfig();
-
- public RemoteConfig getRemoteConfig() {
- return mRemoteConfig;
- }
-
- private static class StubRemoteConfig implements RemoteConfig {
- @Override
- public void fetch(OnRemoteConfigUpdatedListener listener) {}
-
- @Override
- public String getString(String key) {
- return null;
- }
-
- @Override
- public boolean getBoolean(String key) {
- return false;
- }
-
- @Override
- public long getLong(String key) {
- return DEFAULT_LONG_VALUE;
- }
-
- @Override
- public long getLong(String key, long defaultValue) {
- return defaultValue;
- }
- }
-}
diff --git a/common/src/com/android/tv/common/config/RemoteConfigFeature.java b/common/src/com/android/tv/common/config/RemoteConfigFeature.java
deleted file mode 100644
index 2ea381f0..00000000
--- a/common/src/com/android/tv/common/config/RemoteConfigFeature.java
+++ /dev/null
@@ -1,42 +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.common.config;
-
-import android.content.Context;
-import com.android.tv.common.BaseApplication;
-import com.android.tv.common.feature.Feature;
-
-/**
- * A {@link Feature} controlled by a {@link com.android.tv.common.config.api.RemoteConfig} boolean.
- */
-public class RemoteConfigFeature implements Feature {
- private final String mKey;
-
- /** Creates a {@link RemoteConfigFeature for the {@code key}. */
- public static RemoteConfigFeature fromKey(String key) {
- return new RemoteConfigFeature(key);
- }
-
- private RemoteConfigFeature(String key) {
- mKey = key;
- }
-
- @Override
- public boolean isEnabled(Context context) {
- return BaseApplication.getSingletons(context).getRemoteConfig().getBoolean(mKey);
- }
-}
diff --git a/common/src/com/android/tv/common/config/api/RemoteConfig.java b/common/src/com/android/tv/common/config/api/RemoteConfig.java
deleted file mode 100644
index 74597f9d..00000000
--- a/common/src/com/android/tv/common/config/api/RemoteConfig.java
+++ /dev/null
@@ -1,54 +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.common.config.api;
-
-/**
- * Manages Live TV Configuration, allowing remote updates.
- *
- * <p>This is a thin wrapper around <a
- * href="https://firebase.google.com/docs/remote-config/"></a>Firebase Remote Config</a>
- */
-public interface RemoteConfig {
-
- /** Used to inject a remote config */
- interface HasRemoteConfig {
- RemoteConfig getRemoteConfig();
- }
-
- /** Notified on successful completion of a {@link #fetch)} */
- interface OnRemoteConfigUpdatedListener {
- void onRemoteConfigUpdated();
- }
-
- /** Starts a fetch and notifies {@code listener} on successful completion. */
- void fetch(OnRemoteConfigUpdatedListener listener);
-
- /** Gets value as a string corresponding to the specified key. */
- String getString(String key);
-
- /** Gets value as a boolean corresponding to the specified key. */
- boolean getBoolean(String key);
-
- /** Gets value as a long corresponding to the specified key. */
- long getLong(String key);
-
- /**
- * Gets value as a long corresponding to the specified key. Returns the defaultValue if no value
- * is found.
- */
- long getLong(String key, long defaultValue);
-}
diff --git a/common/src/com/android/tv/common/config/api/RemoteConfigValue.java b/common/src/com/android/tv/common/config/api/RemoteConfigValue.java
deleted file mode 100644
index 6da89fb9..00000000
--- a/common/src/com/android/tv/common/config/api/RemoteConfigValue.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-package com.android.tv.common.config.api;
-
-/** Wrapper for a RemoteConfig key and default value. */
-public abstract class RemoteConfigValue<T> {
- private final T defaultValue;
- private final String key;
-
- private RemoteConfigValue(String key, T defaultValue) {
- this.defaultValue = defaultValue;
- this.key = key;
- }
-
- /** Create with the given key and default value */
- public static RemoteConfigValue<Long> create(String key, long defaultValue) {
- return new RemoteConfigValue<Long>(key, defaultValue) {
- @Override
- public Long get(RemoteConfig remoteConfig) {
- return remoteConfig.getLong(key, defaultValue);
- }
- };
- }
-
- public abstract T get(RemoteConfig remoteConfig);
-
- public final T getDefaultValue() {
- return defaultValue;
- }
-
- @Override
- public final String toString() {
- return "RemoteConfigValue(key=" + key + ", defalutValue=" + defaultValue + "]";
- }
-}
diff --git a/common/src/com/android/tv/common/dagger/ApplicationModule.java b/common/src/com/android/tv/common/dagger/ApplicationModule.java
new file mode 100644
index 00000000..4655f777
--- /dev/null
+++ b/common/src/com/android/tv/common/dagger/ApplicationModule.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.common.dagger;
+
+import android.app.Application;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.os.Looper;
+import com.android.tv.common.dagger.annotations.ApplicationContext;
+import com.android.tv.common.dagger.annotations.MainLooper;
+import dagger.Module;
+import dagger.Provides;
+
+/**
+ * Provides application-scope qualifiers for the {@link Application}, the application context, and
+ * the application's main looper.
+ */
+@Module
+public final class ApplicationModule {
+ private final Application mApplication;
+
+ public ApplicationModule(Application application) {
+ mApplication = application;
+ }
+
+ @Provides
+ Application provideApplication() {
+ return mApplication;
+ }
+
+ @Provides
+ @ApplicationContext
+ Context provideContext() {
+ return mApplication.getApplicationContext();
+ }
+
+ @Provides
+ @MainLooper
+ static Looper provideMainLooper() {
+ return Looper.getMainLooper();
+ }
+
+ @Provides
+ ContentResolver provideContentResolver() {
+ return mApplication.getContentResolver();
+ }
+}
diff --git a/common/src/com/android/tv/common/dagger/annotations/ApplicationContext.java b/common/src/com/android/tv/common/dagger/annotations/ApplicationContext.java
new file mode 100644
index 00000000..86318156
--- /dev/null
+++ b/common/src/com/android/tv/common/dagger/annotations/ApplicationContext.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.common.dagger.annotations;
+
+import javax.inject.Qualifier;
+
+/** Annotation for requesting the application's context. */
+@Qualifier
+public @interface ApplicationContext {}
diff --git a/src/com/android/tv/util/Filter.java b/common/src/com/android/tv/common/dagger/annotations/MainLooper.java
index 3e24a496..a8b4100f 100644
--- a/src/com/android/tv/util/Filter.java
+++ b/common/src/com/android/tv/common/dagger/annotations/MainLooper.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -13,11 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+package com.android.tv.common.dagger.annotations;
-package com.android.tv.util;
+import javax.inject.Qualifier;
-/** Interface to decide whether an input is filtered out or not. */
-public interface Filter<T> {
- /** Returns true, if {@code input} is acceptable. */
- boolean filter(T input);
-}
+/** Annotation for requesting a Looper that is on the UI thread. */
+@Qualifier
+public @interface MainLooper {}
diff --git a/common/src/com/android/tv/common/data/RecordedProgramState.java b/common/src/com/android/tv/common/data/RecordedProgramState.java
new file mode 100644
index 00000000..3bfad88d
--- /dev/null
+++ b/common/src/com/android/tv/common/data/RecordedProgramState.java
@@ -0,0 +1,14 @@
+package com.android.tv.common.data;
+
+/** The recording state. */
+// TODO(b/25023911): Use @SimpleEnum when it is supported by AutoValue
+public enum RecordedProgramState {
+ // TODO(b/71717809): Document each state.
+ NOT_SET,
+ STARTED,
+ FINISHED,
+ PARTIAL,
+ FAILED,
+ DELETE,
+ DELETED,
+}
diff --git a/common/src/com/android/tv/common/experiments/ExperimentFlag.java b/common/src/com/android/tv/common/experiments/ExperimentFlag.java
index c9bacac4..b8370ad6 100644
--- a/common/src/com/android/tv/common/experiments/ExperimentFlag.java
+++ b/common/src/com/android/tv/common/experiments/ExperimentFlag.java
@@ -19,41 +19,63 @@ package com.android.tv.common.experiments;
import android.support.annotation.VisibleForTesting;
import com.android.tv.common.BuildConfig;
+import com.google.common.base.Supplier;
/** Experiments return values based on user, device and other criteria. */
public final class ExperimentFlag<T> {
+ // NOTE: sAllowOverrides IS NEVER USED in the non AOSP version.
private static boolean sAllowOverrides = false;
@VisibleForTesting
public static void initForTest() {
+ /* Begin_AOSP_Comment_Out
+ if (!BuildConfig.AOSP) {
+ PhenotypeFlag.initForTest();
+ return;
+ }
+ End_AOSP_Comment_Out */
sAllowOverrides = true;
}
/** Returns a boolean experiment */
public static ExperimentFlag<Boolean> createFlag(
+// AOSP_Comment_Out Supplier<Boolean> phenotypeFlag,
boolean defaultValue) {
return new ExperimentFlag<>(
+// AOSP_Comment_Out phenotypeFlag,
defaultValue);
}
private final T mDefaultValue;
+// AOSP_Comment_Out private final Supplier<T> mPhenotypeFlag;
+// AOSP_Comment_Out // NOTE: mOverrideValue IS NEVER USED in the non AOSP version.
private T mOverrideValue = null;
+ // mOverridden IS NEVER USED in the non AOSP version.
private boolean mOverridden = false;
private ExperimentFlag(
+// AOSP_Comment_Out Supplier<T> phenotypeFlag,
+ // NOTE: defaultValue IS NEVER USED in the non AOSP version.
T defaultValue) {
mDefaultValue = defaultValue;
+// AOSP_Comment_Out mPhenotypeFlag = phenotypeFlag;
}
/** Returns value for this experiment */
public T get() {
+ /* Begin_AOSP_Comment_Out
+ if (!BuildConfig.AOSP) {
+ return mPhenotypeFlag.get();
+ }
+ End_AOSP_Comment_Out */
return sAllowOverrides && mOverridden ? mOverrideValue : mDefaultValue;
}
@VisibleForTesting
public void override(T t) {
+
if (sAllowOverrides) {
mOverridden = true;
mOverrideValue = t;
@@ -64,4 +86,11 @@ public final class ExperimentFlag<T> {
public void resetOverride() {
mOverridden = false;
}
+
+ /* Begin_AOSP_Comment_Out
+ @VisibleForTesting
+ T getAospDefaultValueForTesting() {
+ return mDefaultValue;
+ }
+ End_AOSP_Comment_Out */
}
diff --git a/common/src/com/android/tv/common/experiments/Experiments.java b/common/src/com/android/tv/common/experiments/Experiments.java
index 96b15e53..9bfdb547 100644
--- a/common/src/com/android/tv/common/experiments/Experiments.java
+++ b/common/src/com/android/tv/common/experiments/Experiments.java
@@ -19,6 +19,7 @@ package com.android.tv.common.experiments;
import static com.android.tv.common.experiments.ExperimentFlag.createFlag;
import com.android.tv.common.BuildConfig;
+// AOSP_Comment_Out import com.android.tv.common.flags.LiveChannels;
/**
* Set of experiments visible in AOSP.
@@ -26,17 +27,15 @@ import com.android.tv.common.BuildConfig;
* <p>This file is maintained by hand.
*/
public final class Experiments {
- public static final ExperimentFlag<Boolean> CLOUD_EPG =
- ExperimentFlag.createFlag(
- true);
-
public static final ExperimentFlag<Boolean> ENABLE_UNRATED_CONTENT_SETTINGS =
ExperimentFlag.createFlag(
+// AOSP_Comment_Out LiveChannels::enableUnratedContentSettings,
false);
/** Turn analytics on or off based on the System Checkbox for logging. */
public static final ExperimentFlag<Boolean> ENABLE_ANALYTICS_VIA_CHECKBOX =
createFlag(
+// AOSP_Comment_Out LiveChannels::enableAnalyticsViaCheckbox,
false);
/**
@@ -46,6 +45,7 @@ public final class Experiments {
*/
public static final ExperimentFlag<Boolean> ENABLE_DEVELOPER_FEATURES =
ExperimentFlag.createFlag(
+// AOSP_Comment_Out LiveChannels::enableDeveloperFeatures,
BuildConfig.ENG);
/**
@@ -57,6 +57,7 @@ public final class Experiments {
*/
public static final ExperimentFlag<Boolean> ENABLE_QA_FEATURES =
ExperimentFlag.createFlag(
+// AOSP_Comment_Out LiveChannels::enableQaFeatures,
false);
private Experiments() {}
diff --git a/common/src/com/android/tv/common/feature/EngOnlyFeature.java b/common/src/com/android/tv/common/feature/BuildTypeFeature.java
index 5feb5481..9e1704e8 100644
--- a/common/src/com/android/tv/common/feature/EngOnlyFeature.java
+++ b/common/src/com/android/tv/common/feature/BuildTypeFeature.java
@@ -20,18 +20,23 @@ import android.content.Context;
import com.android.tv.common.BuildConfig;
/** A feature that is only available on {@link BuildConfig#ENG} builds. */
-public final class EngOnlyFeature implements Feature {
- public static final Feature ENG_ONLY_FEATURE = new EngOnlyFeature();
+public final class BuildTypeFeature implements Feature {
+ public static final Feature ENG_ONLY_FEATURE = new BuildTypeFeature(BuildConfig.ENG);
+ public static final Feature ASOP_FEATURE = new BuildTypeFeature(BuildConfig.AOSP);
- private EngOnlyFeature() {}
+ private final boolean mIsBuildType;
+
+ private BuildTypeFeature(boolean isBuildType) {
+ mIsBuildType = isBuildType;
+ }
@Override
public boolean isEnabled(Context context) {
- return BuildConfig.ENG;
+ return mIsBuildType;
}
@Override
public String toString() {
- return "EngOnlyFeature(" + BuildConfig.ENG + ")";
+ return getClass().getSimpleName() + "(" + mIsBuildType + ")";
}
}
diff --git a/common/src/com/android/tv/common/feature/CommonFeatures.java b/common/src/com/android/tv/common/feature/CommonFeatures.java
index 1fceabb3..04052a7c 100644
--- a/common/src/com/android/tv/common/feature/CommonFeatures.java
+++ b/common/src/com/android/tv/common/feature/CommonFeatures.java
@@ -16,18 +16,16 @@
package com.android.tv.common.feature;
-import static com.android.tv.common.feature.FeatureUtils.AND;
+import static com.android.tv.common.feature.BuildTypeFeature.ENG_ONLY_FEATURE;
+import static com.android.tv.common.feature.FeatureUtils.and;
+import static com.android.tv.common.feature.FeatureUtils.or;
import static com.android.tv.common.feature.TestableFeature.createTestableFeature;
import android.content.Context;
-import android.os.Build;
-import android.text.TextUtils;
import android.util.Log;
-import com.android.tv.common.config.api.RemoteConfig.HasRemoteConfig;
-import com.android.tv.common.experiments.Experiments;
-
-import com.android.tv.common.util.CommonUtils;
+import com.android.tv.common.flags.has.HasCloudEpgFlags;
import com.android.tv.common.util.LocationUtils;
+import com.android.tv.common.flags.CloudEpgFlags;
/**
* List of {@link Feature} that affect more than just the Live TV app.
@@ -46,7 +44,7 @@ public class CommonFeatures {
* <p>DVR API is introduced in N, it only works when app runs as a system app.
*/
public static final TestableFeature DVR =
- createTestableFeature(AND(Sdk.AT_LEAST_N, SystemAppFeature.SYSTEM_APP_FEATURE));
+ createTestableFeature(and(Sdk.AT_LEAST_N, SystemAppFeature.SYSTEM_APP_FEATURE));
/**
* ENABLE_RECORDING_REGARDLESS_OF_STORAGE_STATUS
@@ -56,44 +54,32 @@ public class CommonFeatures {
public static final Feature FORCE_RECORDING_UNTIL_NO_SPACE =
PropertyFeature.create("force_recording_until_no_space", false);
- public static final Feature TUNER =
- new Feature() {
- @Override
- public boolean isEnabled(Context context) {
-
- if (CommonUtils.isDeveloper()) {
- // we enable tuner for developers to test tuner in any platform.
- return true;
- }
-
- // This is special handling just for USB Tuner.
- // It does not require any N API's but relies on a improvements in N for AC3
- // support
- return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N;
- }
- };
-
/** Show postal code fragment before channel scan. */
public static final Feature ENABLE_CLOUD_EPG_REGION =
- new Feature() {
- private final String[] supportedRegions = {
- };
+ or(
+ FlagFeature.from(HasCloudEpgFlags::fromContext, CloudEpgFlags::supportedRegion),
+ new Feature() {
+ private final String[] supportedRegions = {
+// AOSP_Comment_Out "US", "GB"
+ };
-
- @Override
- public boolean isEnabled(Context context) {
- if (!Experiments.CLOUD_EPG.get()) {
- if (DEBUG) Log.d(TAG, "Experiments.CLOUD_EPG is false");
- return false;
- }
- String country = LocationUtils.getCurrentCountry(context);
- for (int i = 0; i < supportedRegions.length; i++) {
- if (supportedRegions[i].equalsIgnoreCase(country)) {
- return true;
+ @Override
+ public boolean isEnabled(Context context) {
+ String country = LocationUtils.getCurrentCountry(context);
+ for (int i = 0; i < supportedRegions.length; i++) {
+ if (supportedRegions[i].equalsIgnoreCase(country)) {
+ return true;
+ }
+ }
+ if (DEBUG) Log.d(TAG, "EPG flag false after country check");
+ return false;
}
- }
- if (DEBUG) Log.d(TAG, "EPG flag false after country check");
- return false;
- }
- };
+ });
+
+ // TODO(b/74197177): remove when UI and API finalized.
+ /** Show channel signal strength. */
+ public static final Feature TUNER_SIGNAL_STRENGTH = ENG_ONLY_FEATURE;
+
+ /** Use AudioOnlyTvService for audio-only inputs. */
+ public static final Feature ENABLE_TV_SERVICE = ENG_ONLY_FEATURE;
}
diff --git a/common/src/com/android/tv/common/feature/FeatureUtils.java b/common/src/com/android/tv/common/feature/FeatureUtils.java
index 8650d151..aaed6c82 100644
--- a/common/src/com/android/tv/common/feature/FeatureUtils.java
+++ b/common/src/com/android/tv/common/feature/FeatureUtils.java
@@ -17,6 +17,7 @@
package com.android.tv.common.feature;
import android.content.Context;
+import com.android.tv.common.BuildConfig;
import com.android.tv.common.util.CommonUtils;
import java.util.Arrays;
@@ -28,7 +29,7 @@ public class FeatureUtils {
*
* @param features the features to or
*/
- public static Feature OR(final Feature... features) {
+ public static Feature or(final Feature... features) {
return new Feature() {
@Override
public boolean isEnabled(Context context) {
@@ -52,7 +53,7 @@ public class FeatureUtils {
*
* @param features the features to and
*/
- public static Feature AND(final Feature... features) {
+ public static Feature and(final Feature... features) {
return new Feature() {
@Override
public boolean isEnabled(Context context) {
@@ -70,6 +71,42 @@ public class FeatureUtils {
}
};
}
+ /**
+ * A feature available in AOSP.
+ *
+ * @param googleFeature the feature used in non AOSP builds
+ * @param aospFeature the feature used in AOSP builds
+ */
+ public static Feature aospFeature(
+// AOSP_Comment_Out final Feature googleFeature,
+ final Feature aospFeature) {
+ /* Begin_AOSP_Comment_Out
+ if (!BuildConfig.AOSP) {
+ return googleFeature;
+ } else {
+ End_AOSP_Comment_Out */
+ return aospFeature;
+// AOSP_Comment_Out }
+ }
+
+ /**
+ * Returns a feature that is opposite of the given {@code feature}.
+ *
+ * @param feature the feature to invert
+ */
+ public static Feature not(final Feature feature) {
+ return new Feature() {
+ @Override
+ public boolean isEnabled(Context context) {
+ return !feature.isEnabled(context);
+ }
+
+ @Override
+ public String toString() {
+ return "not(" + feature + ")";
+ }
+ };
+ }
/** A feature that is always enabled. */
public static final Feature ON =
diff --git a/common/src/com/android/tv/common/feature/GServiceFeature.java b/common/src/com/android/tv/common/feature/FlagFeature.java
index 1d7d1156..8da470ef 100644
--- a/common/src/com/android/tv/common/feature/GServiceFeature.java
+++ b/common/src/com/android/tv/common/feature/FlagFeature.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2015 The Android Open Source Project
+ * Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -13,31 +13,34 @@
* See the License for the specific language governing permissions and
* limitations under the License
*/
-
package com.android.tv.common.feature;
import android.content.Context;
+import com.google.common.base.Function;
+
+/** Feature from a Flag */
+public class FlagFeature<T> implements Feature {
-/** A feature controlled by a GServices flag. */
-public class GServiceFeature implements Feature {
- private static final String LIVECHANNELS_PREFIX = "livechannels:";
- private final String mKey;
- private final boolean mDefaultValue;
+ private final Function<Context, T> mToFlag;
+ private final Function<T, Boolean> mToBoolean;
- public GServiceFeature(String key, boolean defaultValue) {
- mKey = LIVECHANNELS_PREFIX + key;
- mDefaultValue = defaultValue;
+ public static <T> FlagFeature<T> from(
+ Function<Context, T> toFlag, Function<T, Boolean> toBoolean) {
+ return new FlagFeature<T>(toFlag, toBoolean);
+ }
+
+ private FlagFeature(Function<Context, T> toFlag, Function<T, Boolean> toBoolean) {
+ mToFlag = toFlag;
+ mToBoolean = toBoolean;
}
@Override
public boolean isEnabled(Context context) {
-
- // GServices is not available outside of Google.
- return mDefaultValue;
+ return mToBoolean.apply(mToFlag.apply(context));
}
@Override
public String toString() {
- return "GService[hash=" + mKey.hashCode() + "]";
+ return mToBoolean.toString();
}
}
diff --git a/common/src/com/android/tv/common/feature/Sdk.java b/common/src/com/android/tv/common/feature/Sdk.java
index 155b391d..4b0a925f 100644
--- a/common/src/com/android/tv/common/feature/Sdk.java
+++ b/common/src/com/android/tv/common/feature/Sdk.java
@@ -17,25 +17,33 @@
package com.android.tv.common.feature;
import android.content.Context;
-import android.os.Build;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
/** Holder for SDK version features */
public final class Sdk {
- public static final Feature AT_LEAST_N =
- new Feature() {
- @Override
- public boolean isEnabled(Context context) {
- return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N;
- }
- };
-
- public static final Feature AT_LEAST_O =
- new Feature() {
- @Override
- public boolean isEnabled(Context context) {
- return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
- }
- };
+
+ public static final Feature AT_LEAST_M = new AtLeast(VERSION_CODES.M);
+
+ public static final Feature AT_LEAST_N = new AtLeast(VERSION_CODES.N);
+
+ public static final Feature AT_LEAST_O = new AtLeast(VERSION_CODES.O);
+
+ public static final Feature AT_LEAST_P = new AtLeast(VERSION_CODES.P); // AOSP_OC:strip_line
+
+ private static final class AtLeast implements Feature {
+
+ private final int versionCode;
+
+ private AtLeast(int versionCode) {
+ this.versionCode = versionCode;
+ }
+
+ @Override
+ public boolean isEnabled(Context unused) {
+ return VERSION.SDK_INT >= versionCode;
+ }
+ }
private Sdk() {}
}
diff --git a/common/src/com/android/tv/common/flags/BackendKnobsFlags.java b/common/src/com/android/tv/common/flags/BackendKnobsFlags.java
new file mode 100644
index 00000000..69bac7a0
--- /dev/null
+++ b/common/src/com/android/tv/common/flags/BackendKnobsFlags.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.tv.common.flags;
+
+/** Flags for tuning non ui behavior */
+public interface BackendKnobsFlags {
+
+ /**
+ * Whether or not this feature is compiled into this build.
+ *
+ * <p>This returns true by default, unless the is_compiled_selector parameter was set during
+ * code generation.
+ */
+ boolean compiled();
+
+ /** Enable fetching only part of the program data. */
+ boolean enablePartialProgramFetch();
+
+ /** EPG fetcher interval in hours */
+ long epgFetcherIntervalHour();
+
+ /** Target channel count for EPG. It is used to adjust the EPG length */
+ long epgTargetChannelCount();
+
+ /** Enables fetching a few hours of programs only when the epg is scrolled to that time. */
+ boolean fetchProgramsAsNeeded();
+
+ /** How many hours of programs are loaded in the program guide for during the initial fetch */
+ long programGuideInitialFetchHours();
+
+ /** How many hours of programs are loaded in the program guide */
+ long programGuideMaxHours();
+}
diff --git a/common/src/com/android/tv/common/flags/CloudEpgFlags.java b/common/src/com/android/tv/common/flags/CloudEpgFlags.java
new file mode 100755
index 00000000..ab4c6a17
--- /dev/null
+++ b/common/src/com/android/tv/common/flags/CloudEpgFlags.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.tv.common.flags;
+
+/** Flags for Cloud EPG */
+public interface CloudEpgFlags {
+
+ /**
+ * Whether or not this feature is compiled into this build.
+ *
+ * <p>This returns true by default, unless the is_compiled_selector parameter was set during
+ * code generation.
+ */
+ boolean compiled();
+
+ /** Is the device in a region supported by Cloud Epg */
+ boolean supportedRegion();
+
+ /** List of input ids that Live TV will update their EPG. */
+ String thirdPartyEpgInputsCsv();
+}
diff --git a/common/src/com/android/tv/common/flags/ConcurrentDvrPlaybackFlags.java b/common/src/com/android/tv/common/flags/ConcurrentDvrPlaybackFlags.java
new file mode 100755
index 00000000..1afff793
--- /dev/null
+++ b/common/src/com/android/tv/common/flags/ConcurrentDvrPlaybackFlags.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.tv.common.flags;
+
+/** Flags allowing concurrent DVR playback */
+public interface ConcurrentDvrPlaybackFlags {
+
+ /**
+ * Whether or not this feature is compiled into this build.
+ *
+ * <p>This returns true by default, unless the is_compiled_selector parameter was set during
+ * code generation.
+ */
+ boolean compiled();
+
+ /** Enable playback of DVR playback during recording */
+ boolean enabled();
+
+ /** Enable tuner using recording data for playback in onTune */
+ boolean onTuneUsesRecording();
+}
diff --git a/common/src/com/android/tv/common/flags/TunerFlags.java b/common/src/com/android/tv/common/flags/TunerFlags.java
new file mode 100755
index 00000000..5f899b90
--- /dev/null
+++ b/common/src/com/android/tv/common/flags/TunerFlags.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.tv.common.flags;
+
+/** Flags for tuner */
+public interface TunerFlags {
+
+ /**
+ * Whether or not this feature is compiled into this build.
+ *
+ * <p>This returns true by default, unless the is_compiled_selector parameter was set during
+ * code generation.
+ */
+ boolean compiled();
+
+ /** Tune using current recording if available. */
+ boolean tuneUsingRecording();
+
+ /** Enable using exoplayer V2 */
+ boolean useExoplayerV2();
+}
diff --git a/common/src/com/android/tv/common/flags/UiFlags.java b/common/src/com/android/tv/common/flags/UiFlags.java
new file mode 100755
index 00000000..4c88d08a
--- /dev/null
+++ b/common/src/com/android/tv/common/flags/UiFlags.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.tv.common.flags;
+
+/** Flags for Live TV UI */
+public interface UiFlags {
+
+ /**
+ * Whether or not this feature is compiled into this build.
+ *
+ * <p>This returns true by default, unless the is_compiled_selector parameter was set during
+ * code generation.
+ */
+ boolean compiled();
+
+ /**
+ * Number of days to be shown by Recording History.
+ *
+ * <p>Set to 0 for all recordings.
+ */
+ long maxHistoryDays();
+
+ /** Unhide the launcher all the time */
+ boolean uhideLauncher();
+
+ /** Use the Leanback Pin Picker */
+ boolean useLeanbackPinPicker();
+}
diff --git a/common/src/com/android/tv/common/flags/has/HasCloudEpgFlags.java b/common/src/com/android/tv/common/flags/has/HasCloudEpgFlags.java
new file mode 100644
index 00000000..c33c5528
--- /dev/null
+++ b/common/src/com/android/tv/common/flags/has/HasCloudEpgFlags.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.tv.common.flags.has;
+
+import android.content.Context;
+import com.android.tv.common.flags.CloudEpgFlags;
+
+/** Has {@link CloudEpgFlags} */
+public interface HasCloudEpgFlags {
+
+ static CloudEpgFlags fromContext(Context context) {
+ return ((HasCloudEpgFlags) HasUtils.getApplicationContext(context)).getCloudEpgFlags();
+ }
+
+ CloudEpgFlags getCloudEpgFlags();
+}
diff --git a/common/src/com/android/tv/common/flags/has/HasConcurrentDvrPlaybackFlags.java b/common/src/com/android/tv/common/flags/has/HasConcurrentDvrPlaybackFlags.java
new file mode 100644
index 00000000..b4710875
--- /dev/null
+++ b/common/src/com/android/tv/common/flags/has/HasConcurrentDvrPlaybackFlags.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.tv.common.flags.has;
+
+import android.content.Context;
+import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags;
+
+/** Has {@link ConcurrentDvrPlaybackFlags} */
+public interface HasConcurrentDvrPlaybackFlags {
+
+ static ConcurrentDvrPlaybackFlags fromContext(Context context) {
+ return ((HasConcurrentDvrPlaybackFlags) HasUtils.getApplicationContext(context))
+ .getConcurrentDvrPlaybackFlags();
+ }
+
+ ConcurrentDvrPlaybackFlags getConcurrentDvrPlaybackFlags();
+}
diff --git a/common/src/com/android/tv/common/flags/has/HasUiFlags.java b/common/src/com/android/tv/common/flags/has/HasUiFlags.java
new file mode 100644
index 00000000..72cc84f2
--- /dev/null
+++ b/common/src/com/android/tv/common/flags/has/HasUiFlags.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.tv.common.flags.has;
+
+import com.android.tv.common.flags.UiFlags;
+
+/** Has {@link UiFlags} */
+public interface HasUiFlags {
+
+ UiFlags getUiFlags();
+}
diff --git a/common/src/com/android/tv/common/flags/has/HasUtils.java b/common/src/com/android/tv/common/flags/has/HasUtils.java
new file mode 100644
index 00000000..1c6126dc
--- /dev/null
+++ b/common/src/com/android/tv/common/flags/has/HasUtils.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.tv.common.flags.has;
+
+import android.content.Context;
+
+/** Static utilities for Has interfaces. */
+public final class HasUtils {
+
+ /** Returns the application context. */
+ public static Context getApplicationContext(Context context) {
+ Context appContext = context.getApplicationContext();
+ return appContext != null ? appContext : context;
+ }
+
+ private HasUtils() {}
+}
diff --git a/common/src/com/android/tv/common/flags/impl/DefaultBackendKnobsFlags.java b/common/src/com/android/tv/common/flags/impl/DefaultBackendKnobsFlags.java
new file mode 100644
index 00000000..a189e473
--- /dev/null
+++ b/common/src/com/android/tv/common/flags/impl/DefaultBackendKnobsFlags.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.common.flags.impl;
+
+/** Flags for tuning non ui behavior. */
+public final class DefaultBackendKnobsFlags
+ implements com.android.tv.common.flags.BackendKnobsFlags {
+
+ @Override
+ public boolean compiled() {
+ return true;
+ }
+
+ @Override
+ public boolean enablePartialProgramFetch() {
+ return false;
+ }
+
+ @Override
+ public long epgFetcherIntervalHour() {
+ return 25;
+ }
+
+ @Override
+ public boolean fetchProgramsAsNeeded() {
+ return false;
+ }
+
+ @Override
+ public long programGuideInitialFetchHours() {
+ return 8;
+ }
+
+ @Override
+ public long programGuideMaxHours() {
+ return 336;
+ }
+
+ @Override
+ public long epgTargetChannelCount() {
+ return 100;
+ }
+}
diff --git a/common/src/com/android/tv/common/flags/impl/DefaultCloudEpgFlags.java b/common/src/com/android/tv/common/flags/impl/DefaultCloudEpgFlags.java
new file mode 100644
index 00000000..34c4fc4b
--- /dev/null
+++ b/common/src/com/android/tv/common/flags/impl/DefaultCloudEpgFlags.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.common.flags.impl;
+
+import com.android.tv.common.flags.CloudEpgFlags;
+
+/** Default flags for Cloud EPG */
+public final class DefaultCloudEpgFlags implements CloudEpgFlags {
+
+ private String mThirdPartyEpgInputCsv =
+ "com.google.android.tv/.tuner.tvinput.TunerTvInputService,"
+ + "com.technicolor.skipper.tuner/.tvinput.TunerTvInputService,"
+ + "com.silicondust.view/.tif.SDTvInputService";
+
+ @Override
+ public boolean compiled() {
+ return true;
+ }
+
+ @Override
+ public boolean supportedRegion() {
+ return false;
+ }
+
+ public void setThirdPartyEpgInputCsv(String value) {
+ mThirdPartyEpgInputCsv = value;
+ }
+
+ @Override
+ public String thirdPartyEpgInputsCsv() {
+ return mThirdPartyEpgInputCsv;
+ }
+}
diff --git a/common/src/com/android/tv/common/flags/impl/DefaultConcurrentDvrPlaybackFlags.java b/common/src/com/android/tv/common/flags/impl/DefaultConcurrentDvrPlaybackFlags.java
new file mode 100644
index 00000000..8d8c584a
--- /dev/null
+++ b/common/src/com/android/tv/common/flags/impl/DefaultConcurrentDvrPlaybackFlags.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.common.flags.impl;
+
+import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags;
+
+/** Default flags for Concurrent DVR Playback */
+public final class DefaultConcurrentDvrPlaybackFlags implements ConcurrentDvrPlaybackFlags {
+
+ @Override
+ public boolean compiled() {
+ return true;
+ }
+
+ @Override
+ public boolean enabled() {
+ return false;
+ }
+
+ @Override
+ public boolean onTuneUsesRecording() {
+ return false;
+ }
+}
diff --git a/common/src/com/android/tv/common/flags/impl/DefaultFlagsModule.java b/common/src/com/android/tv/common/flags/impl/DefaultFlagsModule.java
new file mode 100644
index 00000000..49352364
--- /dev/null
+++ b/common/src/com/android/tv/common/flags/impl/DefaultFlagsModule.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.common.flags.impl;
+
+import dagger.Module;
+import dagger.Provides;
+import dagger.Reusable;
+import com.android.tv.common.flags.BackendKnobsFlags;
+import com.android.tv.common.flags.CloudEpgFlags;
+import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags;
+import com.android.tv.common.flags.TunerFlags;
+import com.android.tv.common.flags.UiFlags;
+
+/** Provides default flags. */
+@Module
+public class DefaultFlagsModule {
+
+ @Provides
+ @Reusable
+ BackendKnobsFlags provideBackendKnobsFlags() {
+ return new DefaultBackendKnobsFlags();
+ }
+
+ @Provides
+ @Reusable
+ CloudEpgFlags provideCloudEpgFlags() {
+ return new DefaultCloudEpgFlags();
+ }
+
+ @Provides
+ @Reusable
+ ConcurrentDvrPlaybackFlags provideConcurrentDvrPlaybackFlags() {
+ return new DefaultConcurrentDvrPlaybackFlags();
+ }
+
+ @Provides
+ @Reusable
+ TunerFlags provideTunerFlags() {
+ return new DefaultTunerFlags();
+ }
+
+ @Provides
+ @Reusable
+ UiFlags provideUiFlags() {
+ return new DefaultUiFlags();
+ }
+}
diff --git a/common/src/com/android/tv/common/flags/impl/DefaultTunerFlags.java b/common/src/com/android/tv/common/flags/impl/DefaultTunerFlags.java
new file mode 100644
index 00000000..195953bc
--- /dev/null
+++ b/common/src/com/android/tv/common/flags/impl/DefaultTunerFlags.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.common.flags.impl;
+
+import com.android.tv.common.flags.TunerFlags;
+
+/** Default Flags for Tuner */
+public class DefaultTunerFlags implements TunerFlags {
+
+ @Override
+ public boolean compiled() {
+ return true;
+ }
+
+ @Override
+ public boolean tuneUsingRecording() {
+ return false;
+ }
+
+ @Override
+ public boolean useExoplayerV2() {
+ return false;
+ }
+}
diff --git a/common/src/com/android/tv/common/flags/impl/DefaultUiFlags.java b/common/src/com/android/tv/common/flags/impl/DefaultUiFlags.java
new file mode 100644
index 00000000..fce45853
--- /dev/null
+++ b/common/src/com/android/tv/common/flags/impl/DefaultUiFlags.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.common.flags.impl;
+
+import com.android.tv.common.flags.UiFlags;
+
+/** Default Flags for Live TV UI */
+public class DefaultUiFlags implements UiFlags {
+
+ @Override
+ public boolean compiled() {
+ return true;
+ }
+
+ @Override
+ public boolean uhideLauncher() {
+ return false;
+ }
+
+ @Override
+ public boolean useLeanbackPinPicker() {
+ return false;
+ }
+
+ @Override
+ public long maxHistoryDays() {
+ return 7;
+ }
+}
diff --git a/common/src/com/android/tv/common/recording/RecordingStorageStatusManager.java b/common/src/com/android/tv/common/recording/RecordingStorageStatusManager.java
index 8b45a730..0fb864bd 100644
--- a/common/src/com/android/tv/common/recording/RecordingStorageStatusManager.java
+++ b/common/src/com/android/tv/common/recording/RecordingStorageStatusManager.java
@@ -217,6 +217,7 @@ public class RecordingStorageStatusManager {
}
} catch (IllegalArgumentException e) {
// In rare cases, storage status change was not notified yet.
+ Log.w(TAG, "Error getting Dvr Storage Status.", e);
SoftPreconditions.checkState(false);
return STORAGE_STATUS_FREE_SPACE_INSUFFICIENT;
}
@@ -246,7 +247,7 @@ public class RecordingStorageStatusManager {
StatFs statFs = new StatFs(storageMountedDir.toString());
storageMountedCapacity = statFs.getTotalBytes();
} catch (IllegalArgumentException e) {
- Log.e(TAG, "Storage mount status was changed.");
+ Log.w(TAG, "Storage mount status was changed.", e);
storageMounted = false;
storageMountedDir = null;
}
diff --git a/common/src/com/android/tv/common/singletons/HasSingletons.java b/common/src/com/android/tv/common/singletons/HasSingletons.java
new file mode 100644
index 00000000..193aed3a
--- /dev/null
+++ b/common/src/com/android/tv/common/singletons/HasSingletons.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.common.singletons;
+
+import android.content.Context;
+
+/**
+ * A type that can know about and supply a singleton, typically a type t such as an android activity
+ * or application.
+ */
+public interface HasSingletons<C> {
+
+ @SuppressWarnings("unchecked") // injection
+ static <C> C get(Class<C> clazz, Context context) {
+ return ((HasSingletons<C>) context).singletons();
+ }
+
+ /** Returns the strongly typed singleton. */
+ C singletons();
+}
diff --git a/common/src/com/android/tv/common/singletons/HasTvInputId.java b/common/src/com/android/tv/common/singletons/HasTvInputId.java
new file mode 100644
index 00000000..4bc0a21c
--- /dev/null
+++ b/common/src/com/android/tv/common/singletons/HasTvInputId.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.common.singletons;
+
+/**
+ * Has TunerInputId.
+ *
+ * <p>This is used buy both the tuner to get its input id and by the Live TV to get the
+ * embedded tuner input id.
+ */
+public interface HasTvInputId {
+
+ String getEmbeddedTunerInputId();
+}
diff --git a/common/src/com/android/tv/common/support/README.md b/common/src/com/android/tv/common/support/README.md
new file mode 100644
index 00000000..67993f37
--- /dev/null
+++ b/common/src/com/android/tv/common/support/README.md
@@ -0,0 +1,8 @@
+# Support Libraries
+
+Packages here are destined to become support libraries.
+
+Each package should be self contained and only have dependencies on public libraries.
+
+It if becomes clear a package should not or will not be part of a support library move it to a
+different location. \ No newline at end of file
diff --git a/common/src/com/android/tv/common/support/tis/BaseTvInputService.java b/common/src/com/android/tv/common/support/tis/BaseTvInputService.java
new file mode 100644
index 00000000..7791550b
--- /dev/null
+++ b/common/src/com/android/tv/common/support/tis/BaseTvInputService.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.common.support.tis;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.media.tv.TvInputManager;
+import android.media.tv.TvInputService;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import com.android.tv.common.support.tis.TifSession.TifSessionFactory;
+
+/** Abstract TVInputService. */
+public abstract class BaseTvInputService extends TvInputService {
+
+ private static final IntentFilter INTENT_FILTER = new IntentFilter();
+
+ static {
+ INTENT_FILTER.addAction(TvInputManager.ACTION_PARENTAL_CONTROLS_ENABLED_CHANGED);
+ INTENT_FILTER.addAction(TvInputManager.ACTION_BLOCKED_RATINGS_CHANGED);
+ }
+
+ @VisibleForTesting
+ protected final BroadcastReceiver broadcastReceiver =
+ new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ switch (intent.getAction()) {
+ case TvInputManager.ACTION_PARENTAL_CONTROLS_ENABLED_CHANGED:
+ case TvInputManager.ACTION_BLOCKED_RATINGS_CHANGED:
+ for (Session session : getSessionManager().getSessions()) {
+ if (session instanceof WrappedSession) {
+ ((WrappedSession) session).onParentalControlsChanged();
+ }
+ }
+ break;
+ default:
+ // do nothing
+ }
+ }
+ };
+
+ @Nullable
+ @Override
+ public final WrappedSession onCreateSession(String inputId) {
+ SessionManager sessionManager = getSessionManager();
+ if (sessionManager.canCreateNewSession()) {
+ WrappedSession session =
+ new WrappedSession(
+ getApplicationContext(),
+ sessionManager,
+ getTifSessionFactory(),
+ inputId);
+ sessionManager.addSession(session);
+ return session;
+ }
+ return null;
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ registerReceiver(broadcastReceiver, INTENT_FILTER);
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ unregisterReceiver(broadcastReceiver);
+ }
+
+ protected abstract TifSessionFactory getTifSessionFactory();
+
+ protected abstract SessionManager getSessionManager();
+}
diff --git a/common/src/com/android/tv/common/support/tis/SessionManager.java b/common/src/com/android/tv/common/support/tis/SessionManager.java
new file mode 100644
index 00000000..5eeebc80
--- /dev/null
+++ b/common/src/com/android/tv/common/support/tis/SessionManager.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.common.support.tis;
+
+import android.media.tv.TvInputService.Session;
+import com.google.common.collect.ImmutableSet;
+
+/** Manages the number of concurrent sessions, keeping track of when sessions are released. */
+public interface SessionManager {
+
+ void removeSession(Session session);
+
+ void addSession(Session session);
+
+ boolean canCreateNewSession();
+
+ ImmutableSet<Session> getSessions();
+}
diff --git a/common/src/com/android/tv/common/support/tis/SimpleSessionManager.java b/common/src/com/android/tv/common/support/tis/SimpleSessionManager.java
new file mode 100644
index 00000000..f0636ccc
--- /dev/null
+++ b/common/src/com/android/tv/common/support/tis/SimpleSessionManager.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.common.support.tis;
+
+import android.media.tv.TvInputService.Session;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.support.annotation.VisibleForTesting;
+import android.util.ArraySet;
+import com.google.common.collect.ImmutableSet;
+import java.util.HashSet;
+import java.util.Set;
+
+/** A simple session manager that allows a maximum number of concurrent session. */
+public final class SimpleSessionManager implements SessionManager {
+
+ private final Set<Session> sessions;
+ private final int max;
+
+ public SimpleSessionManager(int max) {
+ this.max = max;
+ sessions = VERSION.SDK_INT >= VERSION_CODES.M ? new ArraySet<>() : new HashSet<>();
+ }
+
+ @Override
+ public void removeSession(Session session) {
+ sessions.remove(session);
+ }
+
+ @Override
+ public void addSession(Session session) {
+ sessions.add(session);
+ }
+
+ @Override
+ public boolean canCreateNewSession() {
+ return sessions.size() < max;
+ }
+
+ @Override
+ public ImmutableSet<Session> getSessions() {
+ return ImmutableSet.copyOf(sessions);
+ }
+
+ @VisibleForTesting
+ int getSessionCount() {
+ return sessions.size();
+ }
+}
diff --git a/common/src/com/android/tv/common/support/tis/TifSession.java b/common/src/com/android/tv/common/support/tis/TifSession.java
new file mode 100644
index 00000000..61cfe767
--- /dev/null
+++ b/common/src/com/android/tv/common/support/tis/TifSession.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.common.support.tis;
+
+import android.annotation.TargetApi;
+import android.media.PlaybackParams;
+import android.media.tv.TvContentRating;
+import android.media.tv.TvInputManager;
+import android.media.tv.TvInputService.Session;
+import android.media.tv.TvTrackInfo;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Build.VERSION_CODES;
+import android.support.annotation.FloatRange;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.view.Surface;
+import android.view.View;
+import java.util.List;
+
+/**
+ * Custom {@link android.media.tv.TvInputService.Session} class that uses delegation and a callback
+ * to separate it from the TvInputService for easier testing.
+ */
+public abstract class TifSession {
+
+ private final TifSessionCallbacks callback;
+
+ /**
+ * Creates TV Input Framework Session with the given callback.
+ *
+ * <p>The callback is used to pass notification to the actual {@link
+ * android.media.tv.TvInputService.Session}.
+ *
+ * <p>Pass a mock callback for tests.
+ */
+ protected TifSession(TifSessionCallbacks callback) {
+ this.callback = callback;
+ }
+
+ /**
+ * Called after this session had been created and the callback is attached.
+ *
+ * <p>Do not call notify methods in the constructor, instead call them here if needed at
+ * creation time. eg @{@link Session#notifyTimeShiftStatusChanged(int)}.
+ */
+ public void onSessionCreated() {}
+
+ /** @see Session#onRelease() */
+ public void onRelease() {}
+
+ /** @see Session#onSetSurface(Surface) */
+ public abstract boolean onSetSurface(@Nullable Surface surface);
+
+ /** @see Session#onSurfaceChanged(int, int, int) */
+ public abstract void onSurfaceChanged(int format, int width, int height);
+
+ /** @see Session#onSetStreamVolume(float) */
+ public abstract void onSetStreamVolume(@FloatRange(from = 0.0, to = 1.0) float volume);
+
+ /** @see Session#onTune(Uri) */
+ public abstract boolean onTune(Uri channelUri);
+
+ /** @see Session#onSetCaptionEnabled(boolean) */
+ public abstract void onSetCaptionEnabled(boolean enabled);
+
+ /** @see Session#onUnblockContent(TvContentRating) */
+ public abstract void onUnblockContent(TvContentRating unblockedRating);
+
+ /** @see Session#onTimeShiftGetCurrentPosition() */
+ @TargetApi(Build.VERSION_CODES.M)
+ public long onTimeShiftGetCurrentPosition() {
+ return TvInputManager.TIME_SHIFT_INVALID_TIME;
+ }
+
+ /** @see Session#onTimeShiftGetStartPosition() */
+ @TargetApi(Build.VERSION_CODES.M)
+ public long onTimeShiftGetStartPosition() {
+ return TvInputManager.TIME_SHIFT_INVALID_TIME;
+ }
+
+ /** @see Session#onTimeShiftPause() */
+ @TargetApi(Build.VERSION_CODES.M)
+ public void onTimeShiftPause() {}
+
+ /** @see Session#onTimeShiftResume() */
+ @TargetApi(Build.VERSION_CODES.M)
+ public void onTimeShiftResume() {}
+
+ /** @see Session#onTimeShiftSeekTo(long) */
+ @TargetApi(Build.VERSION_CODES.M)
+ public void onTimeShiftSeekTo(long timeMs) {}
+
+ /** @see Session#onTimeShiftSetPlaybackParams(PlaybackParams) */
+ @TargetApi(Build.VERSION_CODES.M)
+ public void onTimeShiftSetPlaybackParams(PlaybackParams params) {}
+
+ public void onParentalControlsChanged() {}
+
+ /** @see Session#notifyChannelRetuned(Uri) */
+ public final void notifyChannelRetuned(final Uri channelUri) {
+ callback.notifyChannelRetuned(channelUri);
+ }
+
+ /** @see Session#notifyTracksChanged(List) */
+ public final void notifyTracksChanged(final List<TvTrackInfo> tracks) {
+ callback.notifyTracksChanged(tracks);
+ }
+
+ /** @see Session#notifyTrackSelected(int, String) */
+ public final void notifyTrackSelected(final int type, final String trackId) {
+ callback.notifyTrackSelected(type, trackId);
+ }
+
+ /** @see Session#notifyVideoAvailable() */
+ public final void notifyVideoAvailable() {
+ callback.notifyVideoAvailable();
+ }
+
+ /** @see Session#notifyVideoUnavailable(int) */
+ public final void notifyVideoUnavailable(final int reason) {
+ callback.notifyVideoUnavailable(reason);
+ }
+
+ /** @see Session#notifyContentAllowed() */
+ public final void notifyContentAllowed() {
+ callback.notifyContentAllowed();
+ }
+
+ /** @see Session#notifyContentBlocked(TvContentRating) */
+ public final void notifyContentBlocked(@NonNull final TvContentRating rating) {
+ callback.notifyContentBlocked(rating);
+ }
+
+ /** @see Session#notifyTimeShiftStatusChanged(int) */
+ @TargetApi(VERSION_CODES.M)
+ public final void notifyTimeShiftStatusChanged(final int status) {
+ callback.notifyTimeShiftStatusChanged(status);
+ }
+
+ /** @see Session#setOverlayViewEnabled(boolean) */
+ public void setOverlayViewEnabled(boolean enabled) {
+ callback.setOverlayViewEnabled(enabled);
+ }
+
+ /** @see Session#onCreateOverlayView() */
+ public View onCreateOverlayView() {
+ return null;
+ }
+
+ /** @see Session#onOverlayViewSizeChanged(int, int) */
+ public void onOverlayViewSizeChanged(int width, int height) {}
+
+ /**
+ * Callbacks used to notify the {@link android.media.tv.TvInputService.Session}.
+ *
+ * <p>This is implemented internally by {@link WrappedSession}, and can be mocked for tests.
+ */
+ public interface TifSessionCallbacks {
+ /** @see Session#notifyChannelRetuned(Uri) */
+ void notifyChannelRetuned(final Uri channelUri);
+ /** @see Session#notifyTracksChanged(List) */
+ void notifyTracksChanged(final List<TvTrackInfo> tracks);
+ /** @see Session#notifyTrackSelected(int, String) */
+ void notifyTrackSelected(final int type, final String trackId);
+ /** @see Session#notifyVideoAvailable() */
+ void notifyVideoAvailable();
+ /** @see Session#notifyVideoUnavailable(int) */
+ void notifyVideoUnavailable(final int reason);
+ /** @see Session#notifyContentAllowed() */
+ void notifyContentAllowed();
+ /** @see Session#notifyContentBlocked(TvContentRating) */
+ void notifyContentBlocked(@NonNull final TvContentRating rating);
+ /** @see Session#notifyTimeShiftStatusChanged(int) */
+ @TargetApi(VERSION_CODES.M)
+ void notifyTimeShiftStatusChanged(final int status);
+ /** @see Session#setOverlayViewEnabled(boolean) */
+ void setOverlayViewEnabled(boolean enabled);
+ }
+
+ /**
+ * Creates a {@link TifSession}.
+ *
+ * <p>This is used by {@link WrappedSession} to create the desired {@code TifSession}. Should be
+ * used with <a href="http://go/autofactory">go/autofactory</a>.
+ */
+ public interface TifSessionFactory {
+ TifSession create(TifSessionCallbacks callbacks, String inputId);
+ }
+}
diff --git a/common/src/com/android/tv/common/support/tis/WrappedSession.java b/common/src/com/android/tv/common/support/tis/WrappedSession.java
new file mode 100644
index 00000000..f4a71dda
--- /dev/null
+++ b/common/src/com/android/tv/common/support/tis/WrappedSession.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.common.support.tis;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.media.PlaybackParams;
+import android.media.tv.TvContentRating;
+import android.media.tv.TvInputService.Session;
+import android.net.Uri;
+import android.os.Build;
+import android.support.annotation.FloatRange;
+import android.support.annotation.Nullable;
+import android.view.Surface;
+import android.view.View;
+import com.android.tv.common.support.tis.TifSession.TifSessionCallbacks;
+import com.android.tv.common.support.tis.TifSession.TifSessionFactory;
+
+/**
+ * Delegates all call to a {@link TifSession} and removes the session from the {@link
+ * SessionManager} when {@link Session#onRelease()} is called.
+ */
+final class WrappedSession extends Session implements TifSessionCallbacks {
+
+ private final SessionManager listener;
+ private final TifSession delegate;
+
+ WrappedSession(
+ Context context,
+ SessionManager sessionManager,
+ TifSessionFactory sessionFactory,
+ String inputId) {
+ super(context);
+ this.listener = sessionManager;
+ this.delegate = sessionFactory.create(this, inputId);
+ }
+
+ @Override
+ public void onRelease() {
+ delegate.onRelease();
+ listener.removeSession(this);
+ }
+
+ @Override
+ public boolean onSetSurface(@Nullable Surface surface) {
+ return delegate.onSetSurface(surface);
+ }
+
+ @Override
+ public void onSurfaceChanged(int format, int width, int height) {
+ delegate.onSurfaceChanged(format, width, height);
+ }
+
+ @Override
+ public void onSetStreamVolume(@FloatRange(from = 0.0, to = 1.0) float volume) {
+ delegate.onSetStreamVolume(volume);
+ }
+
+ @Override
+ public boolean onTune(Uri channelUri) {
+ return delegate.onTune(channelUri);
+ }
+
+ @Override
+ public void onSetCaptionEnabled(boolean enabled) {
+ delegate.onSetCaptionEnabled(enabled);
+ }
+
+ @Override
+ public void onUnblockContent(TvContentRating unblockedRating) {
+ delegate.onUnblockContent(unblockedRating);
+ }
+
+ @Override
+ @TargetApi(Build.VERSION_CODES.M)
+ public long onTimeShiftGetCurrentPosition() {
+ return delegate.onTimeShiftGetCurrentPosition();
+ }
+
+ @Override
+ @TargetApi(Build.VERSION_CODES.M)
+ public long onTimeShiftGetStartPosition() {
+ return delegate.onTimeShiftGetStartPosition();
+ }
+
+ @Override
+ @TargetApi(Build.VERSION_CODES.M)
+ public void onTimeShiftPause() {
+ delegate.onTimeShiftPause();
+ }
+
+ @Override
+ @TargetApi(Build.VERSION_CODES.M)
+ public void onTimeShiftResume() {
+ delegate.onTimeShiftResume();
+ }
+
+ @Override
+ @TargetApi(Build.VERSION_CODES.M)
+ public void onTimeShiftSeekTo(long timeMs) {
+ delegate.onTimeShiftSeekTo(timeMs);
+ }
+
+ @Override
+ @TargetApi(Build.VERSION_CODES.M)
+ public void onTimeShiftSetPlaybackParams(PlaybackParams params) {
+ delegate.onTimeShiftSetPlaybackParams(params);
+ }
+
+ public void onParentalControlsChanged() {
+ delegate.onParentalControlsChanged();
+ }
+
+ @Override
+ @TargetApi(Build.VERSION_CODES.M)
+ public void notifyTimeShiftStatusChanged(int status) {
+ // TODO(nchalko): why is the required for call from TisSession.onSessionCreated to work
+ super.notifyTimeShiftStatusChanged(status);
+ }
+
+ @Override
+ public void setOverlayViewEnabled(boolean enabled) {
+ super.setOverlayViewEnabled(enabled);
+ }
+
+ @Override
+ public View onCreateOverlayView() {
+ return delegate.onCreateOverlayView();
+ }
+
+ @Override
+ public void onOverlayViewSizeChanged(int width, int height) {
+ delegate.onOverlayViewSizeChanged(width, height);
+ }
+}
diff --git a/common/src/com/android/tv/common/ui/setup/SetupActivity.java b/common/src/com/android/tv/common/ui/setup/SetupActivity.java
index 67418ce0..1a3ddbda 100644
--- a/common/src/com/android/tv/common/ui/setup/SetupActivity.java
+++ b/common/src/com/android/tv/common/ui/setup/SetupActivity.java
@@ -16,7 +16,6 @@
package com.android.tv.common.ui.setup;
-import android.app.Activity;
import android.app.Fragment;
import android.app.FragmentTransaction;
import android.os.Bundle;
@@ -27,10 +26,10 @@ import android.support.annotation.NonNull;
import android.transition.Transition;
import android.transition.TransitionInflater;
import android.view.View;
-import android.view.ViewTreeObserver.OnPreDrawListener;
import com.android.tv.common.R;
import com.android.tv.common.WeakHandler;
import com.android.tv.common.ui.setup.animation.SetupAnimationHelper;
+import dagger.android.DaggerActivity;
/**
* Setup activity for onboarding screens or TIS.
@@ -38,7 +37,7 @@ import com.android.tv.common.ui.setup.animation.SetupAnimationHelper;
* <p>The inherited class should add theme {@code Theme.Setup.GuidedStep} to its definition in
* AndroidManifest.xml.
*/
-public abstract class SetupActivity extends Activity implements OnActionClickListener {
+public abstract class SetupActivity extends DaggerActivity implements OnActionClickListener {
private static final int MSG_EXECUTE_ACTION = 1;
private boolean mShowInitialFragment = true;
@@ -55,23 +54,7 @@ public abstract class SetupActivity extends Activity implements OnActionClickLis
// Show initial fragment only when the saved state is not restored, because the last
// fragment is restored if savesInstanceState is not null.
if (savedInstanceState == null) {
- // This is the workaround to show the first fragment with delay to show the fragment
- // enter transition. See http://b/26255145
- getWindow()
- .getDecorView()
- .getViewTreeObserver()
- .addOnPreDrawListener(
- new OnPreDrawListener() {
- @Override
- public boolean onPreDraw() {
- getWindow()
- .getDecorView()
- .getViewTreeObserver()
- .removeOnPreDrawListener(this);
- showInitialFragment();
- return true;
- }
- });
+ showInitialFragment();
} else {
mShowInitialFragment = false;
}
diff --git a/common/src/com/android/tv/common/util/CommonUtils.java b/common/src/com/android/tv/common/util/CommonUtils.java
index 305431d3..4513a879 100644
--- a/common/src/com/android/tv/common/util/CommonUtils.java
+++ b/common/src/com/android/tv/common/util/CommonUtils.java
@@ -138,14 +138,23 @@ public final class CommonUtils {
return ISO_8601.get().format(new Date(timeMillis));
}
- /** Deletes a file or a directory. */
- public static void deleteDirOrFile(File fileOrDirectory) {
+ /**
+ * Deletes a file or a directory.
+ *
+ * @return <code>true</code> if and only if the file or directory is successfully deleted;
+ * <code>false</code> otherwise
+ */
+ public static boolean deleteDirOrFile(File fileOrDirectory) {
if (fileOrDirectory.isDirectory()) {
- for (File child : fileOrDirectory.listFiles()) {
- deleteDirOrFile(child);
+ File[] files = fileOrDirectory.listFiles();
+ if (files != null) {
+ for (File child : files) {
+ deleteDirOrFile(child);
+ }
}
}
- fileOrDirectory.delete();
+ // If earlier deletes failed this will also
+ return fileOrDirectory.delete();
}
public static boolean isRoboTest() {
diff --git a/common/src/com/android/tv/common/util/LocationUtils.java b/common/src/com/android/tv/common/util/LocationUtils.java
index 53155298..ee5119eb 100644
--- a/common/src/com/android/tv/common/util/LocationUtils.java
+++ b/common/src/com/android/tv/common/util/LocationUtils.java
@@ -34,14 +34,20 @@ import com.android.tv.common.BuildConfig;
import java.io.IOException;
+import java.util.Collections;
+import java.util.HashSet;
import java.util.List;
import java.util.Locale;
+import java.util.Set;
/** A utility class to get the current location. */
public class LocationUtils {
private static final String TAG = "LocationUtils";
private static final boolean DEBUG = false;
+ private static final Set<OnUpdateAddressListener> sOnUpdateAddressListeners =
+ Collections.synchronizedSet(new HashSet<>());
+
private static Context sApplicationContext;
private static Address sAddress;
private static String sCountry;
@@ -63,6 +69,39 @@ public class LocationUtils {
return null;
}
+ /** The listener used when address is updated. */
+ public interface OnUpdateAddressListener {
+ /**
+ * Called when address is updated.
+ *
+ * This listener is removed when this method returns true.
+ *
+ * @return {@code true} if the job has been finished and the listener needs to be removed;
+ * {@code false} otherwise.
+ */
+ boolean onUpdateAddress(Address address);
+ }
+
+ /**
+ * Add an {@link OnUpdateAddressListener} instance.
+ *
+ * Note that the listener is removed automatically when
+ * {@link OnUpdateAddressListener#onUpdateAddress(Address)} is called and returns {@code true}.
+ */
+ public static void addOnUpdateAddressListener(OnUpdateAddressListener listener) {
+ sOnUpdateAddressListeners.add(listener);
+ }
+
+ /**
+ * Remove an {@link OnUpdateAddressListener} instance if it exists.
+ *
+ * Note that the listener will be removed automatically when
+ * {@link OnUpdateAddressListener#onUpdateAddress(Address)} is called and returns {@code true}.
+ */
+ public static void removeOnUpdateAddressListener(OnUpdateAddressListener listener) {
+ sOnUpdateAddressListeners.remove(listener);
+ }
+
/** Returns the current country. */
@NonNull
public static synchronized String getCurrentCountry(Context context) {
@@ -92,6 +131,17 @@ public class LocationUtils {
} catch (Exception e) {
// Do nothing
}
+ Set<OnUpdateAddressListener> listenersToRemove = new HashSet<>();
+ synchronized (sOnUpdateAddressListeners) {
+ for (OnUpdateAddressListener listener : sOnUpdateAddressListeners) {
+ if (listener.onUpdateAddress(sAddress)) {
+ listenersToRemove.add(listener);
+ }
+ }
+ for (OnUpdateAddressListener listener : listenersToRemove) {
+ removeOnUpdateAddressListener(listener);
+ }
+ }
} else {
if (DEBUG) Log.d(TAG, "No address returned");
}
diff --git a/common/src/com/android/tv/common/util/NetworkTrafficTags.java b/common/src/com/android/tv/common/util/NetworkTrafficTags.java
index 91f2bcd1..3c94aed6 100644
--- a/common/src/com/android/tv/common/util/NetworkTrafficTags.java
+++ b/common/src/com/android/tv/common/util/NetworkTrafficTags.java
@@ -43,19 +43,16 @@ public final class NetworkTrafficTags {
@Override
public void execute(final @NonNull Runnable command) {
- // TODO(b/62038127): robolectric does not support lamdas in unbundled apps
- delegateExecutor.execute(
- new Runnable() {
- @Override
- public void run() {
- TrafficStats.setThreadStatsTag(tag);
- try {
- command.run();
- } finally {
- TrafficStats.clearThreadStatsTag();
- }
- }
- });
+ // TODO(b/62038127): robolectric does not support lamdas in unbundled apps
+ delegateExecutor.execute(
+ () -> {
+ TrafficStats.setThreadStatsTag(tag);
+ try {
+ command.run();
+ } finally {
+ TrafficStats.clearThreadStatsTag();
+ }
+ });
}
}
diff --git a/common/src/com/android/tv/common/util/PermissionUtils.java b/common/src/com/android/tv/common/util/PermissionUtils.java
index 8d409e50..ca1abdc4 100644
--- a/common/src/com/android/tv/common/util/PermissionUtils.java
+++ b/common/src/com/android/tv/common/util/PermissionUtils.java
@@ -65,4 +65,9 @@ public class PermissionUtils {
return context.checkSelfPermission("android.permission.INTERNET")
== PackageManager.PERMISSION_GRANTED;
}
+
+ public static boolean hasWriteExternalStorage(Context context) {
+ return context.checkSelfPermission("android.permission.WRITE_EXTERNAL_STORAGE")
+ == PackageManager.PERMISSION_GRANTED;
+ }
}
diff --git a/common/src/com/android/tv/common/util/StringUtils.java b/common/src/com/android/tv/common/util/StringUtils.java
index b9461426..bc826208 100644
--- a/common/src/com/android/tv/common/util/StringUtils.java
+++ b/common/src/com/android/tv/common/util/StringUtils.java
@@ -31,4 +31,9 @@ public final class StringUtils {
}
return a.compareTo(b);
}
+
+ /** Returns {@code s} or {@code ""} if {@code s} is {@code null} */
+ public static final String nullToEmpty(String s) {
+ return s == null ? "" : s;
+ }
}
diff --git a/common/src/com/android/tv/common/util/SystemProperties.java b/common/src/com/android/tv/common/util/SystemProperties.java
index a9f18d4b..6ac2907b 100644
--- a/common/src/com/android/tv/common/util/SystemProperties.java
+++ b/common/src/com/android/tv/common/util/SystemProperties.java
@@ -40,6 +40,10 @@ public final class SystemProperties {
public static final BooleanSystemProperty USE_TRACKER =
new BooleanSystemProperty("tv_use_tracker", true);
+ /** Allow third party inputs. */
+ public static final BooleanSystemProperty ALLOW_THIRD_PARTY_INPUTS =
+ new BooleanSystemProperty("ro.tv_allow_third_party_inputs", true);
+
static {
updateSystemProperties();
}
diff --git a/jni/gen_jni.sh b/gradle.properties
index c06b7b91..62082346 100755..100644
--- a/jni/gen_jni.sh
+++ b/gradle.properties
@@ -1,6 +1,4 @@
-#!/bin/bash
-#
-# Copyright (C) 2017 The Android Open Source Project
+# Copyright (C) 2018 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -14,5 +12,13 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+#
+# Experimental gradle configuration. This file may not be up to date.
+#
+
+
+org.gradle.jvmargs=-Xmx6144m -XX:MaxPermSize=6144m -XX:+HeapDumpOnOutOfMemoryError
-javah -jni -classpath ../../bin/classes:../../../../../../prebuilts/sdk/current/public/android.jar -o tunertvinput_jni.h com.android.tv.tuner.TunerHal
+org.gradle.daemon=true
+org.gradle.parallel=true
+org.gradle.configureondemand=true \ No newline at end of file
diff --git a/jni/Android.bp b/jni/Android.bp
new file mode 100644
index 00000000..bbf27787
--- /dev/null
+++ b/jni/Android.bp
@@ -0,0 +1,30 @@
+//
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+cc_library_shared {
+ name: "libtunertvinput_jni",
+ srcs: [
+ "tunertvinput_jni.cpp",
+ "DvbManager.cpp",
+ ],
+ cflags: [
+ "-Wall",
+ "-Werror",
+ ],
+ sdk_version: "23",
+ stl: "c++_static",
+ shared_libs: ["liblog"],
+}
diff --git a/jni/Android.mk b/jni/Android.mk
deleted file mode 100644
index cfc8623e..00000000
--- a/jni/Android.mk
+++ /dev/null
@@ -1,30 +0,0 @@
-#
-# Copyright (C) 2017 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-
-LOCAL_PATH := $(call my-dir)
-
-# --------------------------------------------------------------
-include $(CLEAR_VARS)
-
-LOCAL_MODULE := libtunertvinput_jni
-LOCAL_SRC_FILES += tunertvinput_jni.cpp DvbManager.cpp
-LOCAL_CFLAGS := -Wall -Werror
-LOCAL_SDK_VERSION := 23
-LOCAL_NDK_STL_VARIANT := c++_static
-LOCAL_LDLIBS := -llog
-
-include $(BUILD_SHARED_LIBRARY)
-include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/jni/DvbManager.cpp b/jni/DvbManager.cpp
index 8e51999c..f9dff59b 100644
--- a/jni/DvbManager.cpp
+++ b/jni/DvbManager.cpp
@@ -82,6 +82,37 @@ bool DvbManager::isFeLocked() {
return false;
}
+// This function gets the signal strength from tuner.
+// Output can be:
+// -3 means File Descriptor invalid,
+// or DVB version is not supported,
+// or ERROR while communicate with hardware via ioctl.
+// int signal returns the raw signal strength value.
+int DvbManager::getSignalStrength() {
+ // TODO(b/74197177): add support for DVB V5.
+ if (mFeFd == -1 || mDvbApiVersion != DVB_API_VERSION3) {
+ return -3;
+ }
+ uint16_t strength = 0;
+ // ERROR code from ioctl can be:
+ // EBADF means fd is not a valid open file descriptor
+ // EFAULT means status points to invalid address
+ // ENOSIGNAL means there is no signal, thus no meaningful signal strength
+ // ENOSYS means function not available for this device
+ //
+ // The function used to communicate with tuner in DVB v3 is
+ // ioctl(fd, request, &strength)
+ // int fd is the File Descriptor, can't be -1
+ // int request is the request type,
+ // FE_READ_SIGNAL_STRENGTH for getting signal strength
+ // uint16_t *strength stores the strength value returned from tuner
+ if (ioctl(mFeFd, FE_READ_SIGNAL_STRENGTH, &strength) == -1) {
+ ALOGD("FE_READ_SIGNAL_STRENGTH failed, %s", strerror(errno));
+ return -3;
+ }
+ return strength;
+}
+
int DvbManager::tune(JNIEnv *env, jobject thiz,
const int frequency, const char *modulationStr, int timeout_ms) {
resetExceptFe();
diff --git a/jni/DvbManager.h b/jni/DvbManager.h
index 124fa943..b01113e1 100644
--- a/jni/DvbManager.h
+++ b/jni/DvbManager.h
@@ -49,7 +49,7 @@ class DvbManager {
static const int DELIVERY_SYSTEM_ATSC =
com_android_tv_tuner_TunerHal_DELIVERY_SYSTEM_ATSC;
static const int DELIVERY_SYSTEM_DVBC =
- com_android_tv_tuner_TunerHal_DDELIVERY_SYSTEM_DVBC;
+ com_android_tv_tuner_TunerHal_DELIVERY_SYSTEM_DVBC;
static const int DELIVERY_SYSTEM_DVBS =
com_android_tv_tuner_TunerHal_DELIVERY_SYSTEM_DVBS;
static const int DELIVERY_SYSTEM_DVBS2 =
@@ -85,6 +85,7 @@ public:
void closeAllDvbPidFilter();
void setHasPendingTune(bool hasPendingTune);
int getDeliverySystemType(JNIEnv *env, jobject thiz);
+ int getSignalStrength();
private:
int openDvbFe(JNIEnv *env, jobject thiz);
diff --git a/jni/tunertvinput_jni.cpp b/jni/tunertvinput_jni.cpp
index 368e2d5b..030f9617 100644
--- a/jni/tunertvinput_jni.cpp
+++ b/jni/tunertvinput_jni.cpp
@@ -96,6 +96,23 @@ JNIEXPORT void JNICALL Java_com_android_tv_tuner_TunerHal_nativeStopTune(
/*
* Class: com_android_tv_tuner_TunerHal
+ * Method: nativeGetSignalStrength
+ * Signature: (J)V
+ */
+JNIEXPORT int JNICALL
+Java_com_android_tv_tuner_TunerHal_nativeGetSignalStrength(
+ JNIEnv *, jobject, jlong deviceId) {
+ std::map<jlong, DvbManager *>::iterator it = sDvbManagers.find(deviceId);
+ if (it != sDvbManagers.end()) {
+ return it->second->getSignalStrength();
+ }
+ // If DvbManager can't be found,
+ // return -3 as signal strength not supported.
+ return -3;
+}
+
+/*
+ * Class: com_android_tv_tuner_TunerHal
* Method: nativeAddPidFilter
* Signature: (JII)V
*/
diff --git a/jni/tunertvinput_jni.h b/jni/tunertvinput_jni.h
index d299c304..36e631f5 100644..100755
--- a/jni/tunertvinput_jni.h
+++ b/jni/tunertvinput_jni.h
@@ -17,20 +17,12 @@ extern "C" {
#define com_android_tv_tuner_TunerHal_FILTER_TYPE_VIDEO 2L
#undef com_android_tv_tuner_TunerHal_FILTER_TYPE_PCR
#define com_android_tv_tuner_TunerHal_FILTER_TYPE_PCR 3L
-#undef com_android_tv_tuner_TunerHal_PID_PAT
-#define com_android_tv_tuner_TunerHal_PID_PAT 0L
-#undef com_android_tv_tuner_TunerHal_PID_ATSC_SI_BASE
-#define com_android_tv_tuner_TunerHal_PID_ATSC_SI_BASE 8187L
-#undef com_android_tv_tuner_TunerHal_DEFAULT_VSB_TUNE_TIMEOUT_MS
-#define com_android_tv_tuner_TunerHal_DEFAULT_VSB_TUNE_TIMEOUT_MS 2000L
-#undef com_android_tv_tuner_TunerHal_DEFAULT_QAM_TUNE_TIMEOUT_MS
-#define com_android_tv_tuner_TunerHal_DEFAULT_QAM_TUNE_TIMEOUT_MS 4000L
#undef com_android_tv_tuner_TunerHal_DELIVERY_SYSTEM_UNDEFINED
#define com_android_tv_tuner_TunerHal_DELIVERY_SYSTEM_UNDEFINED 0L
#undef com_android_tv_tuner_TunerHal_DELIVERY_SYSTEM_ATSC
#define com_android_tv_tuner_TunerHal_DELIVERY_SYSTEM_ATSC 1L
#undef com_android_tv_tuner_TunerHal_DELIVERY_SYSTEM_DVBC
-#define com_android_tv_tuner_TunerHal_DDELIVERY_SYSTEM_DVBC 2L
+#define com_android_tv_tuner_TunerHal_DELIVERY_SYSTEM_DVBC 2L
#undef com_android_tv_tuner_TunerHal_DELIVERY_SYSTEM_DVBS
#define com_android_tv_tuner_TunerHal_DELIVERY_SYSTEM_DVBS 3L
#undef com_android_tv_tuner_TunerHal_DELIVERY_SYSTEM_DVBS2
@@ -39,29 +31,51 @@ extern "C" {
#define com_android_tv_tuner_TunerHal_DELIVERY_SYSTEM_DVBT 5L
#undef com_android_tv_tuner_TunerHal_DELIVERY_SYSTEM_DVBT2
#define com_android_tv_tuner_TunerHal_DELIVERY_SYSTEM_DVBT2 6L
+#undef com_android_tv_tuner_TunerHal_TUNER_TYPE_BUILT_IN
+#define com_android_tv_tuner_TunerHal_TUNER_TYPE_BUILT_IN 1L
+#undef com_android_tv_tuner_TunerHal_TUNER_TYPE_USB
+#define com_android_tv_tuner_TunerHal_TUNER_TYPE_USB 2L
+#undef com_android_tv_tuner_TunerHal_TUNER_TYPE_NETWORK
+#define com_android_tv_tuner_TunerHal_TUNER_TYPE_NETWORK 3L
+#undef com_android_tv_tuner_TunerHal_PID_PAT
+#define com_android_tv_tuner_TunerHal_PID_PAT 0L
+#undef com_android_tv_tuner_TunerHal_PID_ATSC_SI_BASE
+#define com_android_tv_tuner_TunerHal_PID_ATSC_SI_BASE 8187L
+#undef com_android_tv_tuner_TunerHal_PID_DVB_SDT
+#define com_android_tv_tuner_TunerHal_PID_DVB_SDT 17L
+#undef com_android_tv_tuner_TunerHal_PID_DVB_EIT
+#define com_android_tv_tuner_TunerHal_PID_DVB_EIT 18L
+#undef com_android_tv_tuner_TunerHal_DEFAULT_VSB_TUNE_TIMEOUT_MS
+#define com_android_tv_tuner_TunerHal_DEFAULT_VSB_TUNE_TIMEOUT_MS 2000L
+#undef com_android_tv_tuner_TunerHal_DEFAULT_QAM_TUNE_TIMEOUT_MS
+#define com_android_tv_tuner_TunerHal_DEFAULT_QAM_TUNE_TIMEOUT_MS 4000L
+#undef com_android_tv_tuner_TunerHal_BUILT_IN_TUNER_TYPE_LINUX_DVB
+#define com_android_tv_tuner_TunerHal_BUILT_IN_TUNER_TYPE_LINUX_DVB 1L
+#undef com_android_tv_tuner_TunerHal_BUILT_IN_TUNER_TYPE_ARCHER
+#define com_android_tv_tuner_TunerHal_BUILT_IN_TUNER_TYPE_ARCHER 100L
/*
* Class: com_android_tv_tuner_TunerHal
* Method: nativeFinalize
* Signature: (J)V
*/
-JNIEXPORT void JNICALL
-Java_com_android_tv_tuner_TunerHal_nativeFinalize(JNIEnv *, jobject, jlong);
+JNIEXPORT void JNICALL Java_com_android_tv_tuner_TunerHal_nativeFinalize
+ (JNIEnv *, jobject, jlong);
/*
* Class: com_android_tv_tuner_TunerHal
* Method: nativeTune
* Signature: (JILjava/lang/String;I)Z
*/
-JNIEXPORT jboolean JNICALL Java_com_android_tv_tuner_TunerHal_nativeTune(
- JNIEnv *, jobject, jlong, jint, jstring, jint);
+JNIEXPORT jboolean JNICALL Java_com_android_tv_tuner_TunerHal_nativeTune
+ (JNIEnv *, jobject, jlong, jint, jstring, jint);
/*
* Class: com_android_tv_tuner_TunerHal
* Method: nativeAddPidFilter
* Signature: (JII)V
*/
-JNIEXPORT void JNICALL Java_com_android_tv_tuner_TunerHal_nativeAddPidFilter(
- JNIEnv *, jobject, jlong, jint, jint);
+JNIEXPORT void JNICALL Java_com_android_tv_tuner_TunerHal_nativeAddPidFilter
+ (JNIEnv *, jobject, jlong, jint, jint);
/*
* Class: com_android_tv_tuner_TunerHal
@@ -69,54 +83,52 @@ JNIEXPORT void JNICALL Java_com_android_tv_tuner_TunerHal_nativeAddPidFilter(
* Signature: (J)V
*/
JNIEXPORT void JNICALL
-Java_com_android_tv_tuner_TunerHal_nativeCloseAllPidFilters(JNIEnv *, jobject,
- jlong);
+Java_com_android_tv_tuner_TunerHal_nativeCloseAllPidFilters
+ (JNIEnv *, jobject, jlong);
/*
* Class: com_android_tv_tuner_TunerHal
- * Method: nativeStopTune
- * Signature: (J)V
+ * Method: nativeSetHasPendingTune
+ * Signature: (JZ)V
*/
JNIEXPORT void JNICALL
-Java_com_android_tv_tuner_TunerHal_nativeStopTune(JNIEnv *, jobject, jlong);
+Java_com_android_tv_tuner_TunerHal_nativeSetHasPendingTune
+ (JNIEnv *, jobject, jlong, jboolean);
/*
* Class: com_android_tv_tuner_TunerHal
- * Method: nativeWriteInBuffer
- * Signature: (J[BI)I
+ * Method: nativeGetDeliverySystemType
+ * Signature: (J)I
*/
-JNIEXPORT jint JNICALL Java_com_android_tv_tuner_TunerHal_nativeWriteInBuffer(
- JNIEnv *, jobject, jlong, jbyteArray, jint);
+JNIEXPORT jint JNICALL
+Java_com_android_tv_tuner_TunerHal_nativeGetDeliverySystemType
+ (JNIEnv *, jobject, jlong);
/*
* Class: com_android_tv_tuner_TunerHal
- * Method: nativeSetHasPendingTune
- * Signature: (JZ)V
+ * Method: nativeGetSignalStrength
+ * Signature: (J)I
*/
-JNIEXPORT void JNICALL
-Java_com_android_tv_tuner_TunerHal_nativeSetHasPendingTune(JNIEnv *, jobject,
- jlong, jboolean);
+JNIEXPORT jint JNICALL
+Java_com_android_tv_tuner_TunerHal_nativeGetSignalStrength
+ (JNIEnv *, jobject, jlong);
/*
* Class: com_android_tv_tuner_TunerHal
- * Method: nativeGetDeliverySystemType
- * Signature: (J)I
+ * Method: nativeStopTune
+ * Signature: (J)V
*/
-JNIEXPORT int JNICALL
-Java_com_android_tv_tuner_TunerHal_nativeGetDeliverySystemType(JNIEnv *,
- jobject, jlong);
+JNIEXPORT void JNICALL Java_com_android_tv_tuner_TunerHal_nativeStopTune
+ (JNIEnv *, jobject, jlong);
-#ifdef __cplusplus
-}
-#endif
-#endif
-/* Header for class com_android_tv_tuner_TunerHal_FilterType */
+/*
+ * Class: com_android_tv_tuner_TunerHal
+ * Method: nativeWriteInBuffer
+ * Signature: (J[BI)I
+ */
+JNIEXPORT jint JNICALL Java_com_android_tv_tuner_TunerHal_nativeWriteInBuffer
+ (JNIEnv *, jobject, jlong, jbyteArray, jint);
-#ifndef _Included_com_android_tv_tuner_TunerHal_FilterType
-#define _Included_com_android_tv_tuner_TunerHal_FilterType
-#ifdef __cplusplus
-extern "C" {
-#endif
#ifdef __cplusplus
}
#endif
diff --git a/libs/Android.bp b/libs/Android.bp
new file mode 100644
index 00000000..fea94875
--- /dev/null
+++ b/libs/Android.bp
@@ -0,0 +1,165 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+java_import {
+ name: "tv-auto-factory-jar",
+ jars: ["auto-factory-1.0-beta2.jar"],
+ host_supported: true,
+ sdk_version: "current",
+}
+
+java_plugin {
+ name: "tv-auto-factory",
+ static_libs: [
+ "jsr330",
+ "tv-auto-factory-jar",
+ "tv-guava-jre-jar",
+ "tv-javawriter-jar",
+ "tv-javax-annotations-jar",
+ ],
+ processor_class: "com.google.auto.factory.processor.AutoFactoryProcessor",
+ generates_api: true,
+}
+
+
+java_import {
+ name: "tv-auto-value-jar",
+ jars: ["auto-value-1.5.2.jar"],
+ host_supported: true,
+ sdk_version: "current",
+}
+
+java_plugin {
+ name: "tv-auto-value",
+ static_libs: [
+ "tv-auto-value-jar",
+ "tv-guava-jre-jar",
+ ],
+ processor_class: "com.google.auto.value.processor.AutoValueProcessor",
+}
+
+java_import {
+ name: "tv-error-prone-annotations-jar",
+ jars: ["error_prone_annotations-2.3.1.jar"],
+ sdk_version: "current",
+}
+
+java_import {
+ name: "tv-guava-jre-jar",
+ jars: ["guava-23.3-jre.jar"],
+ host_supported: true,
+ sdk_version: "current",
+}
+
+java_import {
+ name: "tv-guava-android-jar",
+ jars: ["guava-23.6-android.jar"],
+ sdk_version: "current",
+}
+
+java_import_host{
+ name: "tv-javawriter-jar",
+ jars: ["javawriter-2.5.1.jar"],
+}
+
+java_import {
+ name: "tv-javax-annotations-jar",
+ jars: ["javax.annotation-api-1.2.jar"],
+ host_supported: true,
+ sdk_version: "current",
+}
+
+
+android_library_import {
+ name: "tv-lib-exoplayer",
+ aars: ["exoplayer-r1.5.16.aar"],
+ sdk_version: "current",
+}
+
+android_library_import {
+ name: "tv-lib-exoplayer-v2-core",
+ aars: ["exoplayer-core-2.9.0.aar"],
+ sdk_version: "current",
+}
+
+java_import_host {
+ name: "tv-lib-dagger-compiler-deps",
+ jars: [
+ "google-java-format-1.4-all-deps.jar",
+ "guava-23.3-jre.jar",
+ "javapoet-1.8.0.jar",
+ ],
+}
+
+java_import_host {
+ name: "tv-lib-dagger-compiler-import",
+ jars: [
+ "dagger-compiler-2.15.jar",
+ "dagger-producers-2.15.jar",
+ "dagger-spi-2.15.jar",
+ ],
+}
+
+java_import {
+ name: "tv-lib-dagger",
+ jars: ["dagger-2.15.jar"],
+ host_supported: true,
+ sdk_version: "current",
+}
+
+java_plugin {
+ name: "tv-lib-dagger-compiler",
+ static_libs: [
+ "tv-lib-dagger-compiler-import",
+ "tv-lib-dagger-compiler-deps",
+ "jsr330",
+ "tv-lib-dagger",
+ ],
+ processor_class: "dagger.internal.codegen.ComponentProcessor",
+ generates_api: true,
+}
+
+android_library_import {
+ name: "tv-lib-dagger-android",
+ aars: ["dagger-android-2.15.aar"],
+ sdk_version: "current",
+}
+
+java_import_host {
+ name: "tv-lib-dagger-android-processor-import",
+ jars: [
+ "dagger-android-jarimpl-2.15.jar",
+ "dagger-android-processor-2.15.jar",
+ "dagger-android-support-jarimpl-2.15.jar",
+ ],
+}
+
+java_plugin {
+ name: "tv-lib-dagger-android-processor",
+ static_libs: [
+ "tv-lib-dagger-android-processor-import",
+ "tv-lib-dagger-compiler-deps",
+ "jsr330",
+ "tv-lib-dagger",
+ ],
+ processor_class: "dagger.android.processor.AndroidProcessor",
+ generates_api: true,
+}
+
+java_import {
+ name: "tv-lib-truth",
+ jars: ["truth-0.36.jar"],
+ host_supported: true,
+ sdk_version: "current",
+}
diff --git a/libs/auto-factory-1.0-beta2.jar b/libs/auto-factory-1.0-beta2.jar
new file mode 100644
index 00000000..ceaddac7
--- /dev/null
+++ b/libs/auto-factory-1.0-beta2.jar
Binary files differ
diff --git a/libs/auto-value-1.5.2.jar b/libs/auto-value-1.5.2.jar
new file mode 100644
index 00000000..8ac05679
--- /dev/null
+++ b/libs/auto-value-1.5.2.jar
Binary files differ
diff --git a/libs/dagger-2.15.jar b/libs/dagger-2.15.jar
new file mode 100644
index 00000000..6d766885
--- /dev/null
+++ b/libs/dagger-2.15.jar
Binary files differ
diff --git a/libs/dagger-android-2.15.aar b/libs/dagger-android-2.15.aar
new file mode 100644
index 00000000..430294a2
--- /dev/null
+++ b/libs/dagger-android-2.15.aar
Binary files differ
diff --git a/libs/dagger-android-jarimpl-2.15.jar b/libs/dagger-android-jarimpl-2.15.jar
new file mode 100644
index 00000000..7f7cd459
--- /dev/null
+++ b/libs/dagger-android-jarimpl-2.15.jar
Binary files differ
diff --git a/libs/dagger-android-processor-2.15.jar b/libs/dagger-android-processor-2.15.jar
new file mode 100644
index 00000000..3c7ac054
--- /dev/null
+++ b/libs/dagger-android-processor-2.15.jar
Binary files differ
diff --git a/libs/dagger-android-support-2.15.aar b/libs/dagger-android-support-2.15.aar
new file mode 100644
index 00000000..89a71a91
--- /dev/null
+++ b/libs/dagger-android-support-2.15.aar
Binary files differ
diff --git a/libs/dagger-android-support-jarimpl-2.15.jar b/libs/dagger-android-support-jarimpl-2.15.jar
new file mode 100644
index 00000000..d0ea01a7
--- /dev/null
+++ b/libs/dagger-android-support-jarimpl-2.15.jar
Binary files differ
diff --git a/libs/dagger-compiler-2.15.jar b/libs/dagger-compiler-2.15.jar
new file mode 100644
index 00000000..e73110f6
--- /dev/null
+++ b/libs/dagger-compiler-2.15.jar
Binary files differ
diff --git a/libs/dagger-producers-2.15.jar b/libs/dagger-producers-2.15.jar
new file mode 100644
index 00000000..f1dbb07a
--- /dev/null
+++ b/libs/dagger-producers-2.15.jar
Binary files differ
diff --git a/libs/dagger-spi-2.15.jar b/libs/dagger-spi-2.15.jar
new file mode 100644
index 00000000..6e3156a9
--- /dev/null
+++ b/libs/dagger-spi-2.15.jar
Binary files differ
diff --git a/libs/error_prone_annotations-2.3.1.jar b/libs/error_prone_annotations-2.3.1.jar
new file mode 100644
index 00000000..8a0efa37
--- /dev/null
+++ b/libs/error_prone_annotations-2.3.1.jar
Binary files differ
diff --git a/libs/exoplayer-core-2-SNAPHOT-20180114.aar b/libs/exoplayer-core-2-SNAPHOT-20180114.aar
deleted file mode 100644
index 90af2e63..00000000
--- a/libs/exoplayer-core-2-SNAPHOT-20180114.aar
+++ /dev/null
Binary files differ
diff --git a/libs/exoplayer-core-2.9.0.aar b/libs/exoplayer-core-2.9.0.aar
new file mode 100644
index 00000000..64c4f37f
--- /dev/null
+++ b/libs/exoplayer-core-2.9.0.aar
Binary files differ
diff --git a/libs/google-java-format-1.4-all-deps.jar b/libs/google-java-format-1.4-all-deps.jar
new file mode 100644
index 00000000..b10bfbd6
--- /dev/null
+++ b/libs/google-java-format-1.4-all-deps.jar
Binary files differ
diff --git a/libs/guava-23.3-jre.jar b/libs/guava-23.3-jre.jar
new file mode 100644
index 00000000..b13e275f
--- /dev/null
+++ b/libs/guava-23.3-jre.jar
Binary files differ
diff --git a/libs/guava-23.5-jre.jar b/libs/guava-23.5-jre.jar
new file mode 100644
index 00000000..7e5f13a8
--- /dev/null
+++ b/libs/guava-23.5-jre.jar
Binary files differ
diff --git a/libs/guava-23.6-android.jar b/libs/guava-23.6-android.jar
new file mode 100644
index 00000000..01180d23
--- /dev/null
+++ b/libs/guava-23.6-android.jar
Binary files differ
diff --git a/libs/javapoet-1.8.0.jar b/libs/javapoet-1.8.0.jar
new file mode 100644
index 00000000..6758b6d7
--- /dev/null
+++ b/libs/javapoet-1.8.0.jar
Binary files differ
diff --git a/libs/javawriter-2.5.1.jar b/libs/javawriter-2.5.1.jar
new file mode 100644
index 00000000..4ec579e7
--- /dev/null
+++ b/libs/javawriter-2.5.1.jar
Binary files differ
diff --git a/libs/javax.annotation-api-1.2.jar b/libs/javax.annotation-api-1.2.jar
new file mode 100644
index 00000000..9ab39ffa
--- /dev/null
+++ b/libs/javax.annotation-api-1.2.jar
Binary files differ
diff --git a/libs/truth-0.36.jar b/libs/truth-0.36.jar
new file mode 100644
index 00000000..8174e4a9
--- /dev/null
+++ b/libs/truth-0.36.jar
Binary files differ
diff --git a/material_res/drawable-hdpi/quantum_ic_arrow_downward_white_36.png b/material_res/drawable-hdpi/quantum_ic_arrow_downward_white_36.png
new file mode 100644
index 00000000..76a57d15
--- /dev/null
+++ b/material_res/drawable-hdpi/quantum_ic_arrow_downward_white_36.png
Binary files differ
diff --git a/material_res/drawable-hdpi/quantum_ic_arrow_upward_white_36.png b/material_res/drawable-hdpi/quantum_ic_arrow_upward_white_36.png
new file mode 100644
index 00000000..ce42f364
--- /dev/null
+++ b/material_res/drawable-hdpi/quantum_ic_arrow_upward_white_36.png
Binary files differ
diff --git a/material_res/drawable-hdpi/quantum_ic_check_circle_white_48.png b/material_res/drawable-hdpi/quantum_ic_check_circle_white_48.png
new file mode 100644
index 00000000..4f967453
--- /dev/null
+++ b/material_res/drawable-hdpi/quantum_ic_check_circle_white_48.png
Binary files differ
diff --git a/material_res/drawable-hdpi/quantum_ic_developer_mode_tv_white_48.png b/material_res/drawable-hdpi/quantum_ic_developer_mode_tv_white_48.png
new file mode 100644
index 00000000..0d9bf8b4
--- /dev/null
+++ b/material_res/drawable-hdpi/quantum_ic_developer_mode_tv_white_48.png
Binary files differ
diff --git a/material_res/drawable-hdpi/quantum_ic_error_white_48.png b/material_res/drawable-hdpi/quantum_ic_error_white_48.png
new file mode 100644
index 00000000..abe2573b
--- /dev/null
+++ b/material_res/drawable-hdpi/quantum_ic_error_white_48.png
Binary files differ
diff --git a/material_res/drawable-hdpi/quantum_ic_signal_cellular_0_bar_white_24.png b/material_res/drawable-hdpi/quantum_ic_signal_cellular_0_bar_white_24.png
new file mode 100644
index 00000000..c7e88489
--- /dev/null
+++ b/material_res/drawable-hdpi/quantum_ic_signal_cellular_0_bar_white_24.png
Binary files differ
diff --git a/material_res/drawable-hdpi/quantum_ic_signal_cellular_1_bar_white_24.png b/material_res/drawable-hdpi/quantum_ic_signal_cellular_1_bar_white_24.png
new file mode 100644
index 00000000..3b79370a
--- /dev/null
+++ b/material_res/drawable-hdpi/quantum_ic_signal_cellular_1_bar_white_24.png
Binary files differ
diff --git a/material_res/drawable-hdpi/quantum_ic_signal_cellular_2_bar_white_24.png b/material_res/drawable-hdpi/quantum_ic_signal_cellular_2_bar_white_24.png
new file mode 100644
index 00000000..bf736dc8
--- /dev/null
+++ b/material_res/drawable-hdpi/quantum_ic_signal_cellular_2_bar_white_24.png
Binary files differ
diff --git a/material_res/drawable-hdpi/quantum_ic_signal_cellular_3_bar_white_24.png b/material_res/drawable-hdpi/quantum_ic_signal_cellular_3_bar_white_24.png
new file mode 100644
index 00000000..8bee424e
--- /dev/null
+++ b/material_res/drawable-hdpi/quantum_ic_signal_cellular_3_bar_white_24.png
Binary files differ
diff --git a/material_res/drawable-hdpi/quantum_ic_signal_cellular_4_bar_white_24.png b/material_res/drawable-hdpi/quantum_ic_signal_cellular_4_bar_white_24.png
new file mode 100644
index 00000000..1765e947
--- /dev/null
+++ b/material_res/drawable-hdpi/quantum_ic_signal_cellular_4_bar_white_24.png
Binary files differ
diff --git a/material_res/drawable-hdpi/quantum_ic_warning_white_18.png b/material_res/drawable-hdpi/quantum_ic_warning_white_18.png
new file mode 100644
index 00000000..7520b79f
--- /dev/null
+++ b/material_res/drawable-hdpi/quantum_ic_warning_white_18.png
Binary files differ
diff --git a/material_res/drawable-hdpi/quantum_ic_warning_white_96.png b/material_res/drawable-hdpi/quantum_ic_warning_white_96.png
new file mode 100644
index 00000000..88c22324
--- /dev/null
+++ b/material_res/drawable-hdpi/quantum_ic_warning_white_96.png
Binary files differ
diff --git a/material_res/drawable-mdpi/quantum_ic_arrow_downward_white_36.png b/material_res/drawable-mdpi/quantum_ic_arrow_downward_white_36.png
new file mode 100644
index 00000000..dbfb7ab4
--- /dev/null
+++ b/material_res/drawable-mdpi/quantum_ic_arrow_downward_white_36.png
Binary files differ
diff --git a/material_res/drawable-mdpi/quantum_ic_arrow_upward_white_36.png b/material_res/drawable-mdpi/quantum_ic_arrow_upward_white_36.png
new file mode 100644
index 00000000..c39725cb
--- /dev/null
+++ b/material_res/drawable-mdpi/quantum_ic_arrow_upward_white_36.png
Binary files differ
diff --git a/material_res/drawable-mdpi/quantum_ic_check_circle_white_48.png b/material_res/drawable-mdpi/quantum_ic_check_circle_white_48.png
new file mode 100644
index 00000000..d36d696e
--- /dev/null
+++ b/material_res/drawable-mdpi/quantum_ic_check_circle_white_48.png
Binary files differ
diff --git a/material_res/drawable-mdpi/quantum_ic_developer_mode_tv_white_48.png b/material_res/drawable-mdpi/quantum_ic_developer_mode_tv_white_48.png
new file mode 100644
index 00000000..976a44bc
--- /dev/null
+++ b/material_res/drawable-mdpi/quantum_ic_developer_mode_tv_white_48.png
Binary files differ
diff --git a/material_res/drawable-mdpi/quantum_ic_error_white_48.png b/material_res/drawable-mdpi/quantum_ic_error_white_48.png
new file mode 100644
index 00000000..9829698d
--- /dev/null
+++ b/material_res/drawable-mdpi/quantum_ic_error_white_48.png
Binary files differ
diff --git a/material_res/drawable-mdpi/quantum_ic_signal_cellular_0_bar_white_24.png b/material_res/drawable-mdpi/quantum_ic_signal_cellular_0_bar_white_24.png
new file mode 100644
index 00000000..a0c5d415
--- /dev/null
+++ b/material_res/drawable-mdpi/quantum_ic_signal_cellular_0_bar_white_24.png
Binary files differ
diff --git a/material_res/drawable-mdpi/quantum_ic_signal_cellular_1_bar_white_24.png b/material_res/drawable-mdpi/quantum_ic_signal_cellular_1_bar_white_24.png
new file mode 100644
index 00000000..0e70e98d
--- /dev/null
+++ b/material_res/drawable-mdpi/quantum_ic_signal_cellular_1_bar_white_24.png
Binary files differ
diff --git a/material_res/drawable-mdpi/quantum_ic_signal_cellular_2_bar_white_24.png b/material_res/drawable-mdpi/quantum_ic_signal_cellular_2_bar_white_24.png
new file mode 100644
index 00000000..eb2124d0
--- /dev/null
+++ b/material_res/drawable-mdpi/quantum_ic_signal_cellular_2_bar_white_24.png
Binary files differ
diff --git a/material_res/drawable-mdpi/quantum_ic_signal_cellular_3_bar_white_24.png b/material_res/drawable-mdpi/quantum_ic_signal_cellular_3_bar_white_24.png
new file mode 100644
index 00000000..a2045c33
--- /dev/null
+++ b/material_res/drawable-mdpi/quantum_ic_signal_cellular_3_bar_white_24.png
Binary files differ
diff --git a/material_res/drawable-mdpi/quantum_ic_signal_cellular_4_bar_white_24.png b/material_res/drawable-mdpi/quantum_ic_signal_cellular_4_bar_white_24.png
new file mode 100644
index 00000000..8d76fd42
--- /dev/null
+++ b/material_res/drawable-mdpi/quantum_ic_signal_cellular_4_bar_white_24.png
Binary files differ
diff --git a/material_res/drawable-mdpi/quantum_ic_warning_white_18.png b/material_res/drawable-mdpi/quantum_ic_warning_white_18.png
new file mode 100644
index 00000000..1f055176
--- /dev/null
+++ b/material_res/drawable-mdpi/quantum_ic_warning_white_18.png
Binary files differ
diff --git a/material_res/drawable-mdpi/quantum_ic_warning_white_96.png b/material_res/drawable-mdpi/quantum_ic_warning_white_96.png
new file mode 100644
index 00000000..8683a2ea
--- /dev/null
+++ b/material_res/drawable-mdpi/quantum_ic_warning_white_96.png
Binary files differ
diff --git a/material_res/drawable-xhdpi/quantum_ic_arrow_downward_white_36.png b/material_res/drawable-xhdpi/quantum_ic_arrow_downward_white_36.png
new file mode 100644
index 00000000..225e4e54
--- /dev/null
+++ b/material_res/drawable-xhdpi/quantum_ic_arrow_downward_white_36.png
Binary files differ
diff --git a/material_res/drawable-xhdpi/quantum_ic_arrow_upward_white_36.png b/material_res/drawable-xhdpi/quantum_ic_arrow_upward_white_36.png
new file mode 100644
index 00000000..d7b27da8
--- /dev/null
+++ b/material_res/drawable-xhdpi/quantum_ic_arrow_upward_white_36.png
Binary files differ
diff --git a/material_res/drawable-xhdpi/quantum_ic_check_circle_white_48.png b/material_res/drawable-xhdpi/quantum_ic_check_circle_white_48.png
new file mode 100644
index 00000000..2c6e4741
--- /dev/null
+++ b/material_res/drawable-xhdpi/quantum_ic_check_circle_white_48.png
Binary files differ
diff --git a/material_res/drawable-xhdpi/quantum_ic_developer_mode_tv_white_48.png b/material_res/drawable-xhdpi/quantum_ic_developer_mode_tv_white_48.png
new file mode 100644
index 00000000..1336147d
--- /dev/null
+++ b/material_res/drawable-xhdpi/quantum_ic_developer_mode_tv_white_48.png
Binary files differ
diff --git a/material_res/drawable-xhdpi/quantum_ic_error_white_48.png b/material_res/drawable-xhdpi/quantum_ic_error_white_48.png
new file mode 100644
index 00000000..830fb7e1
--- /dev/null
+++ b/material_res/drawable-xhdpi/quantum_ic_error_white_48.png
Binary files differ
diff --git a/material_res/drawable-xhdpi/quantum_ic_signal_cellular_0_bar_white_24.png b/material_res/drawable-xhdpi/quantum_ic_signal_cellular_0_bar_white_24.png
new file mode 100644
index 00000000..26d6bff8
--- /dev/null
+++ b/material_res/drawable-xhdpi/quantum_ic_signal_cellular_0_bar_white_24.png
Binary files differ
diff --git a/material_res/drawable-xhdpi/quantum_ic_signal_cellular_1_bar_white_24.png b/material_res/drawable-xhdpi/quantum_ic_signal_cellular_1_bar_white_24.png
new file mode 100644
index 00000000..3b7f7ade
--- /dev/null
+++ b/material_res/drawable-xhdpi/quantum_ic_signal_cellular_1_bar_white_24.png
Binary files differ
diff --git a/material_res/drawable-xhdpi/quantum_ic_signal_cellular_2_bar_white_24.png b/material_res/drawable-xhdpi/quantum_ic_signal_cellular_2_bar_white_24.png
new file mode 100644
index 00000000..b80307c3
--- /dev/null
+++ b/material_res/drawable-xhdpi/quantum_ic_signal_cellular_2_bar_white_24.png
Binary files differ
diff --git a/material_res/drawable-xhdpi/quantum_ic_signal_cellular_3_bar_white_24.png b/material_res/drawable-xhdpi/quantum_ic_signal_cellular_3_bar_white_24.png
new file mode 100644
index 00000000..1ccc9770
--- /dev/null
+++ b/material_res/drawable-xhdpi/quantum_ic_signal_cellular_3_bar_white_24.png
Binary files differ
diff --git a/material_res/drawable-xhdpi/quantum_ic_signal_cellular_4_bar_white_24.png b/material_res/drawable-xhdpi/quantum_ic_signal_cellular_4_bar_white_24.png
new file mode 100644
index 00000000..62501b0b
--- /dev/null
+++ b/material_res/drawable-xhdpi/quantum_ic_signal_cellular_4_bar_white_24.png
Binary files differ
diff --git a/material_res/drawable-xhdpi/quantum_ic_warning_white_18.png b/material_res/drawable-xhdpi/quantum_ic_warning_white_18.png
new file mode 100644
index 00000000..55c68431
--- /dev/null
+++ b/material_res/drawable-xhdpi/quantum_ic_warning_white_18.png
Binary files differ
diff --git a/material_res/drawable-xhdpi/quantum_ic_warning_white_96.png b/material_res/drawable-xhdpi/quantum_ic_warning_white_96.png
new file mode 100644
index 00000000..23e6d932
--- /dev/null
+++ b/material_res/drawable-xhdpi/quantum_ic_warning_white_96.png
Binary files differ
diff --git a/material_res/drawable-xxhdpi/quantum_ic_arrow_downward_white_36.png b/material_res/drawable-xxhdpi/quantum_ic_arrow_downward_white_36.png
new file mode 100644
index 00000000..0502223d
--- /dev/null
+++ b/material_res/drawable-xxhdpi/quantum_ic_arrow_downward_white_36.png
Binary files differ
diff --git a/material_res/drawable-xxhdpi/quantum_ic_arrow_upward_white_36.png b/material_res/drawable-xxhdpi/quantum_ic_arrow_upward_white_36.png
new file mode 100644
index 00000000..eceb34c7
--- /dev/null
+++ b/material_res/drawable-xxhdpi/quantum_ic_arrow_upward_white_36.png
Binary files differ
diff --git a/material_res/drawable-xxhdpi/quantum_ic_check_circle_white_48.png b/material_res/drawable-xxhdpi/quantum_ic_check_circle_white_48.png
new file mode 100644
index 00000000..980d10b4
--- /dev/null
+++ b/material_res/drawable-xxhdpi/quantum_ic_check_circle_white_48.png
Binary files differ
diff --git a/material_res/drawable-xxhdpi/quantum_ic_developer_mode_tv_white_48.png b/material_res/drawable-xxhdpi/quantum_ic_developer_mode_tv_white_48.png
new file mode 100644
index 00000000..defa1bfe
--- /dev/null
+++ b/material_res/drawable-xxhdpi/quantum_ic_developer_mode_tv_white_48.png
Binary files differ
diff --git a/material_res/drawable-xxhdpi/quantum_ic_error_white_48.png b/material_res/drawable-xxhdpi/quantum_ic_error_white_48.png
new file mode 100644
index 00000000..c8349523
--- /dev/null
+++ b/material_res/drawable-xxhdpi/quantum_ic_error_white_48.png
Binary files differ
diff --git a/material_res/drawable-xxhdpi/quantum_ic_signal_cellular_0_bar_white_24.png b/material_res/drawable-xxhdpi/quantum_ic_signal_cellular_0_bar_white_24.png
new file mode 100644
index 00000000..2e200fbb
--- /dev/null
+++ b/material_res/drawable-xxhdpi/quantum_ic_signal_cellular_0_bar_white_24.png
Binary files differ
diff --git a/material_res/drawable-xxhdpi/quantum_ic_signal_cellular_1_bar_white_24.png b/material_res/drawable-xxhdpi/quantum_ic_signal_cellular_1_bar_white_24.png
new file mode 100644
index 00000000..215f9b73
--- /dev/null
+++ b/material_res/drawable-xxhdpi/quantum_ic_signal_cellular_1_bar_white_24.png
Binary files differ
diff --git a/material_res/drawable-xxhdpi/quantum_ic_signal_cellular_2_bar_white_24.png b/material_res/drawable-xxhdpi/quantum_ic_signal_cellular_2_bar_white_24.png
new file mode 100644
index 00000000..c8c0ebfc
--- /dev/null
+++ b/material_res/drawable-xxhdpi/quantum_ic_signal_cellular_2_bar_white_24.png
Binary files differ
diff --git a/material_res/drawable-xxhdpi/quantum_ic_signal_cellular_3_bar_white_24.png b/material_res/drawable-xxhdpi/quantum_ic_signal_cellular_3_bar_white_24.png
new file mode 100644
index 00000000..8377811b
--- /dev/null
+++ b/material_res/drawable-xxhdpi/quantum_ic_signal_cellular_3_bar_white_24.png
Binary files differ
diff --git a/material_res/drawable-xxhdpi/quantum_ic_signal_cellular_4_bar_white_24.png b/material_res/drawable-xxhdpi/quantum_ic_signal_cellular_4_bar_white_24.png
new file mode 100644
index 00000000..b191b9d8
--- /dev/null
+++ b/material_res/drawable-xxhdpi/quantum_ic_signal_cellular_4_bar_white_24.png
Binary files differ
diff --git a/material_res/drawable-xxhdpi/quantum_ic_warning_white_18.png b/material_res/drawable-xxhdpi/quantum_ic_warning_white_18.png
new file mode 100644
index 00000000..fb079e95
--- /dev/null
+++ b/material_res/drawable-xxhdpi/quantum_ic_warning_white_18.png
Binary files differ
diff --git a/material_res/drawable-xxhdpi/quantum_ic_warning_white_96.png b/material_res/drawable-xxhdpi/quantum_ic_warning_white_96.png
new file mode 100644
index 00000000..064cd51a
--- /dev/null
+++ b/material_res/drawable-xxhdpi/quantum_ic_warning_white_96.png
Binary files differ
diff --git a/material_res/drawable-xxxhdpi/quantum_ic_arrow_downward_white_36.png b/material_res/drawable-xxxhdpi/quantum_ic_arrow_downward_white_36.png
new file mode 100644
index 00000000..fe0ecc64
--- /dev/null
+++ b/material_res/drawable-xxxhdpi/quantum_ic_arrow_downward_white_36.png
Binary files differ
diff --git a/material_res/drawable-xxxhdpi/quantum_ic_arrow_upward_white_36.png b/material_res/drawable-xxxhdpi/quantum_ic_arrow_upward_white_36.png
new file mode 100644
index 00000000..5e61c3d1
--- /dev/null
+++ b/material_res/drawable-xxxhdpi/quantum_ic_arrow_upward_white_36.png
Binary files differ
diff --git a/material_res/drawable-xxxhdpi/quantum_ic_check_circle_white_48.png b/material_res/drawable-xxxhdpi/quantum_ic_check_circle_white_48.png
new file mode 100644
index 00000000..60463c56
--- /dev/null
+++ b/material_res/drawable-xxxhdpi/quantum_ic_check_circle_white_48.png
Binary files differ
diff --git a/material_res/drawable-xxxhdpi/quantum_ic_developer_mode_tv_white_48.png b/material_res/drawable-xxxhdpi/quantum_ic_developer_mode_tv_white_48.png
new file mode 100644
index 00000000..b316f1d5
--- /dev/null
+++ b/material_res/drawable-xxxhdpi/quantum_ic_developer_mode_tv_white_48.png
Binary files differ
diff --git a/material_res/drawable-xxxhdpi/quantum_ic_error_white_48.png b/material_res/drawable-xxxhdpi/quantum_ic_error_white_48.png
new file mode 100644
index 00000000..ad4a474a
--- /dev/null
+++ b/material_res/drawable-xxxhdpi/quantum_ic_error_white_48.png
Binary files differ
diff --git a/material_res/drawable-xxxhdpi/quantum_ic_signal_cellular_0_bar_white_24.png b/material_res/drawable-xxxhdpi/quantum_ic_signal_cellular_0_bar_white_24.png
new file mode 100644
index 00000000..f8051327
--- /dev/null
+++ b/material_res/drawable-xxxhdpi/quantum_ic_signal_cellular_0_bar_white_24.png
Binary files differ
diff --git a/material_res/drawable-xxxhdpi/quantum_ic_signal_cellular_1_bar_white_24.png b/material_res/drawable-xxxhdpi/quantum_ic_signal_cellular_1_bar_white_24.png
new file mode 100644
index 00000000..42debccf
--- /dev/null
+++ b/material_res/drawable-xxxhdpi/quantum_ic_signal_cellular_1_bar_white_24.png
Binary files differ
diff --git a/material_res/drawable-xxxhdpi/quantum_ic_signal_cellular_2_bar_white_24.png b/material_res/drawable-xxxhdpi/quantum_ic_signal_cellular_2_bar_white_24.png
new file mode 100644
index 00000000..6a7f3209
--- /dev/null
+++ b/material_res/drawable-xxxhdpi/quantum_ic_signal_cellular_2_bar_white_24.png
Binary files differ
diff --git a/material_res/drawable-xxxhdpi/quantum_ic_signal_cellular_3_bar_white_24.png b/material_res/drawable-xxxhdpi/quantum_ic_signal_cellular_3_bar_white_24.png
new file mode 100644
index 00000000..706da320
--- /dev/null
+++ b/material_res/drawable-xxxhdpi/quantum_ic_signal_cellular_3_bar_white_24.png
Binary files differ
diff --git a/material_res/drawable-xxxhdpi/quantum_ic_signal_cellular_4_bar_white_24.png b/material_res/drawable-xxxhdpi/quantum_ic_signal_cellular_4_bar_white_24.png
new file mode 100644
index 00000000..4a3ac28d
--- /dev/null
+++ b/material_res/drawable-xxxhdpi/quantum_ic_signal_cellular_4_bar_white_24.png
Binary files differ
diff --git a/material_res/drawable-xxxhdpi/quantum_ic_warning_white_18.png b/material_res/drawable-xxxhdpi/quantum_ic_warning_white_18.png
new file mode 100644
index 00000000..807b9fa1
--- /dev/null
+++ b/material_res/drawable-xxxhdpi/quantum_ic_warning_white_18.png
Binary files differ
diff --git a/material_res/drawable-xxxhdpi/quantum_ic_warning_white_96.png b/material_res/drawable-xxxhdpi/quantum_ic_warning_white_96.png
new file mode 100644
index 00000000..2439be1d
--- /dev/null
+++ b/material_res/drawable-xxxhdpi/quantum_ic_warning_white_96.png
Binary files differ
diff --git a/open_source_project.README b/open_source_project.README
index 31532f03..897b8eeb 100644
--- a/open_source_project.README
+++ b/open_source_project.README
@@ -7,7 +7,7 @@ How to build:
https://source.android.com/source/building.html
(Developers using PDK can skip the step 1.)
2. Enable the feature PackageManager.FEATURE_LIVE_TV.
-3. Put this project under Android platform repository.
+3. Put this project under Android platform repository if required.
4. Include this package inside platform build.
5. Build the platform.
https://source.android.com/source/building.html
diff --git a/partner_support/Android.bp b/partner_support/Android.bp
new file mode 100644
index 00000000..4775fc11
--- /dev/null
+++ b/partner_support/Android.bp
@@ -0,0 +1,32 @@
+//
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+android_library {
+ name: "live-channels-partner-support",
+ srcs: ["src/**/*.java"],
+
+ sdk_version: "system_current",
+ min_sdk_version: "23",
+
+ resource_dirs: ["res"],
+
+ static_libs: ["android-support-annotations"],
+
+ libs: ["tv-auto-value-jar"],
+
+ plugins: ["tv-auto-value"],
+
+}
diff --git a/partner_support/Android.mk b/partner_support/Android.mk
deleted file mode 100644
index 8306921b..00000000
--- a/partner_support/Android.mk
+++ /dev/null
@@ -1,23 +0,0 @@
-LOCAL_PATH:= $(call my-dir)
-include $(CLEAR_VARS)
-
-# Include all java files.
-LOCAL_SRC_FILES := $(call all-java-files-under, src)
-
-LOCAL_MODULE := live-channels-partner-support
-LOCAL_MODULE_CLASS := STATIC_JAVA_LIBRARIES
-LOCAL_MODULE_TAGS := optional
-LOCAL_SDK_VERSION := system_current
-LOCAL_MIN_SDK_VERSION := 23
-
-LOCAL_USE_AAPT2 := true
-
-LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
-
-LOCAL_STATIC_JAVA_LIBRARIES := android-support-annotations
-
-include $(LOCAL_PATH)/buildconfig.mk
-
-include $(BUILD_STATIC_JAVA_LIBRARY)
-
-include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/partner_support/AndroidManifest.xml b/partner_support/AndroidManifest.xml
index 5a45f0db..45a693fc 100644
--- a/partner_support/AndroidManifest.xml
+++ b/partner_support/AndroidManifest.xml
@@ -17,6 +17,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.android.tv.partner.support"
android:versionCode="1">
- <uses-sdk android:targetSdkVersion="26" android:minSdkVersion="21"/>
+ <uses-sdk android:targetSdkVersion="27" android:minSdkVersion="23"/>
<application />
</manifest>
diff --git a/partner_support/g3doc/SeriesIdColumnForPartners.md b/partner_support/g3doc/SeriesIdColumnForPartners.md
new file mode 100644
index 00000000..cd44db03
--- /dev/null
+++ b/partner_support/g3doc/SeriesIdColumnForPartners.md
@@ -0,0 +1,30 @@
+# 3rd party instructions for using series recording feature of Live Channels
+
+## Prerequisites
+
+* Updated agreement with Google
+* Oreo or patched Nougat
+
+## Nougat
+
+To enable series recording with Nougat you will need the following changes.
+
+### Patch TVProvider
+
+To run in Nougat you must backport the following changes
+
+* [Filter out non-existing customized columns in
+ DB](https://partner-android.googlesource.com/platform/packages/providers/TvProvider/+/142162af889b2c124bb012eea608c6a65eed54bb)
+* [Add TvProvider methods to get and add
+ columns](https://partner-android.googlesource.com/platform/packages/providers/TvProvider/+/cda6788ae903513a555fd3e07a5a1c14218c40a2)
+
+### Customisation
+
+Indicate TvProvider is patched by including the following in their TV
+customization resource
+
+```
+<bool name="tvprovider_allows_column_creation">true</bool>
+```
+
+See https://source.android.com/devices/tv/customize-tv-app
diff --git a/partner_support/g3doc/TurnOffEmbeddedTuner.md b/partner_support/g3doc/TurnOffEmbeddedTuner.md
new file mode 100644
index 00000000..0ba7cff2
--- /dev/null
+++ b/partner_support/g3doc/TurnOffEmbeddedTuner.md
@@ -0,0 +1,15 @@
+# 3rd party instructions turning off the embedded tuner in Live Channels
+
+Partners that have a built in tuner should provide a TV Input like
+SampleDvbTuner. When partners provide their own tuner they MUST turn of the
+embedded tuner in Live Channels.
+
+### Customisation
+
+Indicate Live Channels should not use it's embedded tuner implementation.
+
+```
+<bool name="turn_off_embedded_tuner">true</bool>
+```
+
+See https://source.android.com/devices/tv/customize-tv-app
diff --git a/partner_support/sample_customization/AndroidManifest.xml b/partner_support/sample_customization/AndroidManifest.xml
index f1edad3f..804691a6 100644
--- a/partner_support/sample_customization/AndroidManifest.xml
+++ b/partner_support/sample_customization/AndroidManifest.xml
@@ -18,13 +18,13 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.tvcustomization">
<!-- Customization package must have this permission to customize TV apps. -->
- <uses-permission android:name="com.android.tv.permission.CUSTOMIZE_TV_APP"/>
+ <uses-permission android:name="com.google.android.tv.permission.CUSTOMIZE_TV_APP"/>
<!-- Enable leanback library support. -->
<uses-feature android:name="android.software.leanback" android:required="true" />
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
- <uses-sdk android:targetSdkVersion="26" android:minSdkVersion="23"/>
+ <uses-sdk android:targetSdkVersion="27" android:minSdkVersion="23"/>
<application android:label="Partner Customization"
android:theme="@android:style/Theme.Holo.Light.NoActionBar"
diff --git a/partner_support/sample_customization/res/mipmap-hdpi/ic_launcher.png b/partner_support/sample_customization/res/mipmap-hdpi/ic_launcher.png
index 26add7f6..454c515a 100755..100644
--- a/partner_support/sample_customization/res/mipmap-hdpi/ic_launcher.png
+++ b/partner_support/sample_customization/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/partner_support/sample_customization/res/mipmap-mdpi/ic_launcher.png b/partner_support/sample_customization/res/mipmap-mdpi/ic_launcher.png
index 1ac20dbf..5e53eb55 100755..100644
--- a/partner_support/sample_customization/res/mipmap-mdpi/ic_launcher.png
+++ b/partner_support/sample_customization/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/partner_support/sample_customization/res/mipmap-xhdpi/ic_launcher.png b/partner_support/sample_customization/res/mipmap-xhdpi/ic_launcher.png
index f6cf6450..898bac41 100755..100644
--- a/partner_support/sample_customization/res/mipmap-xhdpi/ic_launcher.png
+++ b/partner_support/sample_customization/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/partner_support/sample_customization/res/mipmap-xxhdpi/ic_launcher.png b/partner_support/sample_customization/res/mipmap-xxhdpi/ic_launcher.png
index 72a250db..9da29906 100755..100644
--- a/partner_support/sample_customization/res/mipmap-xxhdpi/ic_launcher.png
+++ b/partner_support/sample_customization/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/partner_support/sample_customization/res/mipmap-xxxhdpi/ic_launcher.png b/partner_support/sample_customization/res/mipmap-xxxhdpi/ic_launcher.png
index 648001f7..ff5c4b1a 100755..100644
--- a/partner_support/sample_customization/res/mipmap-xxxhdpi/ic_launcher.png
+++ b/partner_support/sample_customization/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/partner_support/sample_customization/res/values/bools.xml b/partner_support/sample_customization/res/values/bools.xml
index 54fbe071..259548b4 100644
--- a/partner_support/sample_customization/res/values/bools.xml
+++ b/partner_support/sample_customization/res/values/bools.xml
@@ -17,4 +17,6 @@
<resources>
<bool name="tvprovider_allows_system_inserts_to_program_table">true</bool>
+ <bool name="tvprovider_allows_column_creation">true</bool>
+ <bool name="turn_off_embedded_tuner">true</bool>
</resources> \ No newline at end of file
diff --git a/partner_support/samples/Android.mk b/partner_support/samples/Android.mk
index 922a5b5d..2e771a5b 100644
--- a/partner_support/samples/Android.mk
+++ b/partner_support/samples/Android.mk
@@ -16,7 +16,7 @@ LOCAL_STATIC_ANDROID_LIBRARIES := \
android-support-core-ui \
android-support-v7-recyclerview \
android-support-v17-leanback \
- android-support-tv-provider
+ androidx.tvprovider_tvprovider
LOCAL_USE_AAPT2 := true
diff --git a/partner_support/samples/AndroidManifest.xml b/partner_support/samples/AndroidManifest.xml
index b9e086cb..d91c603a 100644
--- a/partner_support/samples/AndroidManifest.xml
+++ b/partner_support/samples/AndroidManifest.xml
@@ -29,12 +29,13 @@
<uses-permission android:name="com.android.providers.tv.permission.READ_EPG_DATA" />
<uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA" />
<uses-permission android:name="com.android.tv.permission.RECEIVE_INPUT_EVENT" />
- <uses-sdk android:targetSdkVersion="26" android:minSdkVersion="23"/>
+ <uses-sdk android:targetSdkVersion="27" android:minSdkVersion="23"/>
<!--TODO(b/68949299): remove tool hint when we have smaller dependency targets-->
<application android:label="@string/partner_support_sample_tv_input"
- tools:replace="android:label,icon,theme"
+ tools:replace="android:label,icon,theme,appComponentFactory"
android:icon="@mipmap/ic_launcher"
- android:theme="@android:style/Theme.Holo.Light.NoActionBar" >
+ android:theme="@android:style/Theme.Holo.Light.NoActionBar"
+ android:appComponentFactory="android.support.v4.app.CoreComponentFactory" >
<activity android:name=".SampleTvInputSetupActivity"
android:theme="@style/Theme.Leanback.GuidedStep">
<intent-filter>
diff --git a/partner_support/samples/res/mipmap-hdpi/ic_launcher.png b/partner_support/samples/res/mipmap-hdpi/ic_launcher.png
index a827add4..a044d2cc 100755..100644
--- a/partner_support/samples/res/mipmap-hdpi/ic_launcher.png
+++ b/partner_support/samples/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/partner_support/samples/res/mipmap-mdpi/ic_launcher.png b/partner_support/samples/res/mipmap-mdpi/ic_launcher.png
index d7d36f26..26307c20 100755..100644
--- a/partner_support/samples/res/mipmap-mdpi/ic_launcher.png
+++ b/partner_support/samples/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/partner_support/samples/res/mipmap-xhdpi/ic_launcher.png b/partner_support/samples/res/mipmap-xhdpi/ic_launcher.png
index 210bfcf1..49646832 100755..100644
--- a/partner_support/samples/res/mipmap-xhdpi/ic_launcher.png
+++ b/partner_support/samples/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/partner_support/samples/res/mipmap-xxhdpi/ic_launcher.png b/partner_support/samples/res/mipmap-xxhdpi/ic_launcher.png
index 59a090c7..93db5549 100755..100644
--- a/partner_support/samples/res/mipmap-xxhdpi/ic_launcher.png
+++ b/partner_support/samples/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/partner_support/samples/res/mipmap-xxxhdpi/ic_launcher.png b/partner_support/samples/res/mipmap-xxxhdpi/ic_launcher.png
index 388b6ebe..cfc2fb11 100755..100644
--- a/partner_support/samples/res/mipmap-xxxhdpi/ic_launcher.png
+++ b/partner_support/samples/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/partner_support/samples/src/com/example/partnersupportsampletvinput/ChannelScanFragment.java b/partner_support/samples/src/com/example/partnersupportsampletvinput/ChannelScanFragment.java
index 35f4b694..ec7589cb 100644
--- a/partner_support/samples/src/com/example/partnersupportsampletvinput/ChannelScanFragment.java
+++ b/partner_support/samples/src/com/example/partnersupportsampletvinput/ChannelScanFragment.java
@@ -23,8 +23,6 @@ import android.os.AsyncTask;
import android.os.Bundle;
import android.os.SystemClock;
import android.support.annotation.NonNull;
-import android.support.media.tv.Channel;
-import android.support.media.tv.TvContractCompat;
import android.support.v17.leanback.app.GuidedStepFragment;
import android.support.v17.leanback.widget.GuidanceStylist;
import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
@@ -32,6 +30,8 @@ import android.support.v17.leanback.widget.GuidedAction;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import androidx.tvprovider.media.tv.Channel;
+import androidx.tvprovider.media.tv.TvContractCompat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
diff --git a/partner_support/src/com/google/android/tv/partner/support/AutoValue_EpgInput.java b/partner_support/src/com/google/android/tv/partner/support/AutoValue_EpgInput.java
deleted file mode 100644
index aad51c76..00000000
--- a/partner_support/src/com/google/android/tv/partner/support/AutoValue_EpgInput.java
+++ /dev/null
@@ -1,97 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.google.android.tv.partner.support;
-
-
-
-/**
- * Hand copy of generated Autovalue class.
- *
- * TODO get autovalue working
- */
-final class AutoValue_EpgInput extends EpgInput {
-
- private final long id;
- private final String inputId;
- private final String lineupId;
-
- AutoValue_EpgInput(
- long id,
- String inputId,
- String lineupId) {
- this.id = id;
- if (inputId == null) {
- throw new NullPointerException("Null inputId");
- }
- this.inputId = inputId;
- if (lineupId == null) {
- throw new NullPointerException("Null lineupId");
- }
- this.lineupId = lineupId;
- }
-
- @Override
- public long getId() {
- return id;
- }
-
- @Override
- public String getInputId() {
- return inputId;
- }
-
- @Override
- public String getLineupId() {
- return lineupId;
- }
-
- @Override
- public String toString() {
- return "EpgInput{"
- + "id=" + id + ", "
- + "inputId=" + inputId + ", "
- + "lineupId=" + lineupId
- + "}";
- }
-
- @Override
- public boolean equals(Object o) {
- if (o == this) {
- return true;
- }
- if (o instanceof EpgInput) {
- EpgInput that = (EpgInput) o;
- return (this.id == that.getId())
- && (this.inputId.equals(that.getInputId()))
- && (this.lineupId.equals(that.getLineupId()));
- }
- return false;
- }
-
- @Override
- public int hashCode() {
- int h$ = 1;
- h$ *= 1000003;
- h$ ^= (int) ((id >>> 32) ^ id);
- h$ *= 1000003;
- h$ ^= inputId.hashCode();
- h$ *= 1000003;
- h$ ^= lineupId.hashCode();
- return h$;
- }
-
-}
diff --git a/partner_support/src/com/google/android/tv/partner/support/AutoValue_Lineup.java b/partner_support/src/com/google/android/tv/partner/support/AutoValue_Lineup.java
deleted file mode 100644
index 076f8a2a..00000000
--- a/partner_support/src/com/google/android/tv/partner/support/AutoValue_Lineup.java
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-
-
-package com.google.android.tv.partner.support;
-
-import android.support.annotation.Nullable;
-import java.util.List;
-
-/**
- * Hand copy of generated Autovalue class.
- *
- * TODO get autovalue working
- */
-
-final class AutoValue_Lineup extends Lineup {
-
- private final String id;
- private final int type;
- private final String name;
- private final List<String> channels;
-
- AutoValue_Lineup(
- String id,
- int type,
- @Nullable String name,
- List<String> channels) {
- if (id == null) {
- throw new NullPointerException("Null id");
- }
- this.id = id;
- this.type = type;
- this.name = name;
- if (channels == null) {
- throw new NullPointerException("Null channels");
- }
- this.channels = channels;
- }
-
- @Override
- public String getId() {
- return id;
- }
-
- @Override
- public int getType() {
- return type;
- }
-
- @Nullable
- @Override
- public String getName() {
- return name;
- }
-
- @Override
- public List<String> getChannels() {
- return channels;
- }
-
- @Override
- public String toString() {
- return "Lineup{"
- + "id=" + id + ", "
- + "type=" + type + ", "
- + "name=" + name + ", "
- + "channels=" + channels
- + "}";
- }
-
- @Override
- public boolean equals(Object o) {
- if (o == this) {
- return true;
- }
- if (o instanceof Lineup) {
- Lineup that = (Lineup) o;
- return (this.id.equals(that.getId()))
- && (this.type == that.getType())
- && ((this.name == null) ? (that.getName() == null) : this.name.equals(that.getName()))
- && (this.channels.equals(that.getChannels()));
- }
- return false;
- }
-
- @Override
- public int hashCode() {
- int h$ = 1;
- h$ *= 1000003;
- h$ ^= id.hashCode();
- h$ *= 1000003;
- h$ ^= type;
- h$ *= 1000003;
- h$ ^= (name == null) ? 0 : name.hashCode();
- h$ *= 1000003;
- h$ ^= channels.hashCode();
- return h$;
- }
-
-}
diff --git a/partner_support/src/com/google/android/tv/partner/support/BaseCustomization.java b/partner_support/src/com/google/android/tv/partner/support/BaseCustomization.java
index e40d90d7..1f7198eb 100644
--- a/partner_support/src/com/google/android/tv/partner/support/BaseCustomization.java
+++ b/partner_support/src/com/google/android/tv/partner/support/BaseCustomization.java
@@ -83,7 +83,14 @@ public class BaseCustomization {
? 0
: res.getIdentifier(resourceName, RES_TYPE_BOOLEAN, packageName);
if (DEBUG) {
- Log.d(TAG, "Boolean resource " + resourceName + " has " + resId);
+ Log.d(
+ TAG,
+ "Boolean resource "
+ + resourceName
+ + " has "
+ + resId
+ + " with value "
+ + (resId == 0 ? "missing" : res.getBoolean(resId)));
}
return resId == 0 ? Optional.empty() : Optional.of(res.getBoolean(resId));
} catch (PackageManager.NameNotFoundException e) {
diff --git a/partner_support/src/com/google/android/tv/partner/support/EpgInput.java b/partner_support/src/com/google/android/tv/partner/support/EpgInput.java
index 82cc463a..20b3542a 100644
--- a/partner_support/src/com/google/android/tv/partner/support/EpgInput.java
+++ b/partner_support/src/com/google/android/tv/partner/support/EpgInput.java
@@ -17,13 +17,14 @@
package com.google.android.tv.partner.support;
import android.content.ContentValues;
+import com.google.auto.value.AutoValue;
/**
* Value class representing a TV Input that uses Live TV EPG.
*
* @see {@link EpgContract.EpgInputs}
*/
-// TODO(b/72052568): Get autovalue to work in aosp master
+@AutoValue
public abstract class EpgInput {
public static EpgInput createEpgChannel(long id, String inputId, String lineupId) {
diff --git a/partner_support/src/com/google/android/tv/partner/support/EpgInputs.java b/partner_support/src/com/google/android/tv/partner/support/EpgInputs.java
index 53485ec8..dddcd08c 100644
--- a/partner_support/src/com/google/android/tv/partner/support/EpgInputs.java
+++ b/partner_support/src/com/google/android/tv/partner/support/EpgInputs.java
@@ -59,6 +59,8 @@ public final class EpgInputs {
result.add(EpgInput.createEpgChannel(contentValues));
}
return result;
+ } catch (Exception e) {
+ return Collections.emptySet();
}
}
diff --git a/partner_support/src/com/google/android/tv/partner/support/Lineup.java b/partner_support/src/com/google/android/tv/partner/support/Lineup.java
index 6123eebd..c5d30464 100644
--- a/partner_support/src/com/google/android/tv/partner/support/Lineup.java
+++ b/partner_support/src/com/google/android/tv/partner/support/Lineup.java
@@ -18,12 +18,13 @@ package com.google.android.tv.partner.support;
import android.content.ContentValues;
import android.support.annotation.Nullable;
+import com.google.auto.value.AutoValue;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/** Value class for {@link com.google.android.tv.partner.support.EpgContract.Lineups} */
-// TODO(b/72052568): Get autovalue to work in aosp master
+@AutoValue
public abstract class Lineup {
/** Lineup type for cable. */
public static final int LINEUP_CABLE = 0;
diff --git a/partner_support/src/com/google/android/tv/partner/support/PartnerCustomizations.java b/partner_support/src/com/google/android/tv/partner/support/PartnerCustomizations.java
index 7ff168e1..11331076 100644
--- a/partner_support/src/com/google/android/tv/partner/support/PartnerCustomizations.java
+++ b/partner_support/src/com/google/android/tv/partner/support/PartnerCustomizations.java
@@ -32,6 +32,11 @@ public final class PartnerCustomizations extends BaseCustomization {
public static final String TVPROVIDER_ALLOWS_SYSTEM_INSERTS_TO_PROGRAM_TABLE =
"tvprovider_allows_system_inserts_to_program_table";
+ public static final String TVPROVIDER_ALLOWS_COLUMN_CREATION =
+ "tvprovider_allows_column_creation";
+
+ public static final String TURN_OFF_EMBEDDED_TUNER = "turn_off_embedded_tuner";
+
public PartnerCustomizations(Context context) {
super(context, CUSTOMIZE_PERMISSIONS);
}
@@ -40,4 +45,12 @@ public final class PartnerCustomizations extends BaseCustomization {
return getBooleanResource(context, TVPROVIDER_ALLOWS_SYSTEM_INSERTS_TO_PROGRAM_TABLE)
.orElse(false);
}
+
+ public boolean doesTvProviderAllowColumnCreation(Context context) {
+ return getBooleanResource(context, TVPROVIDER_ALLOWS_COLUMN_CREATION).orElse(false);
+ }
+
+ public boolean turnOffEmbeddedTuner(Context context) {
+ return getBooleanResource(context, TURN_OFF_EMBEDDED_TUNER).orElse(false);
+ }
}
diff --git a/partner_support/tests/robotests/javatests/com/google/android/tv/partner/support/BaseCustomizationTest.java b/partner_support/tests/robotests/javatests/com/google/android/tv/partner/support/BaseCustomizationTest.java
index a6292e3e..e2170589 100644
--- a/partner_support/tests/robotests/javatests/com/google/android/tv/partner/support/BaseCustomizationTest.java
+++ b/partner_support/tests/robotests/javatests/com/google/android/tv/partner/support/BaseCustomizationTest.java
@@ -25,12 +25,12 @@ import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import com.android.tv.testing.TestSingletonApp;
import com.android.tv.testing.constants.ConfigConstants;
-import com.google.thirdparty.robolectric.GoogleRobolectricTestRunner;
import java.util.ArrayList;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
@@ -38,15 +38,8 @@ import org.robolectric.annotation.Config;
// TODO: move to partner-support
-@RunWith(GoogleRobolectricTestRunner.class)
-@Config(
- manifest =
- "//third_party/java_src/android_app/live_channels/common/src"
- + "/com/android/tv/common"
- + ":common/AndroidManifest.xml",
- sdk = ConfigConstants.SDK,
- application = TestSingletonApp.class
-)
+@RunWith(RobolectricTestRunner.class)
+@Config(sdk = ConfigConstants.SDK, application = TestSingletonApp.class)
public class BaseCustomizationTest {
private static final String[] PERMISSIONS = {"com.example.permission"};
diff --git a/proguard.flags b/proguard.flags
index 69b17861..b3795d65 100644
--- a/proguard.flags
+++ b/proguard.flags
@@ -68,3 +68,6 @@
# Grpc used by epg via reflection
-keep class io.grpc.internal.DnsNameResolverProvider
+
+# Don't warn about checkerframework in Android proguard
+-dontwarn org.checkerframework.**
diff --git a/res/drawable-xhdpi/bg_protection.png b/res/drawable-xhdpi/bg_protection.png
deleted file mode 100644
index 02df25ae..00000000
--- a/res/drawable-xhdpi/bg_protection.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/ic_store.png b/res/drawable-xhdpi/ic_app_store.png
index 767b8b64..767b8b64 100644
--- a/res/drawable-xhdpi/ic_store.png
+++ b/res/drawable-xhdpi/ic_app_store.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_check_circle_white_48dp.png b/res/drawable-xhdpi/ic_check_circle_white_48dp.png
deleted file mode 100644
index a1cf83e6..00000000
--- a/res/drawable-xhdpi/ic_check_circle_white_48dp.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/ic_developer_mode_tv_white_48dp.png b/res/drawable-xhdpi/ic_developer_mode_tv_white_48dp.png
deleted file mode 100644
index 594af851..00000000
--- a/res/drawable-xhdpi/ic_developer_mode_tv_white_48dp.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/ic_error_outline_pink_24dp.png b/res/drawable-xhdpi/ic_error_outline_pink_24dp.png
new file mode 100644
index 00000000..d48becee
--- /dev/null
+++ b/res/drawable-xhdpi/ic_error_outline_pink_24dp.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_error_white_48dp.png b/res/drawable-xhdpi/ic_error_white_48dp.png
deleted file mode 100644
index 8c2cf1e5..00000000
--- a/res/drawable-xhdpi/ic_error_white_48dp.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/ic_live_channels.png b/res/drawable-xhdpi/ic_live_channels.png
deleted file mode 100644
index bb1c2d9d..00000000
--- a/res/drawable-xhdpi/ic_live_channels.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/ic_tv_app.png b/res/drawable-xhdpi/ic_tv_app.png
new file mode 100644
index 00000000..c061bf04
--- /dev/null
+++ b/res/drawable-xhdpi/ic_tv_app.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_live_channels_96x96.png b/res/drawable-xhdpi/ic_tv_app_96x96.png
index 7f4eb10d..7f4eb10d 100644
--- a/res/drawable-xhdpi/ic_live_channels_96x96.png
+++ b/res/drawable-xhdpi/ic_tv_app_96x96.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_warning_white_18dp.png b/res/drawable-xhdpi/ic_warning_white_18dp.png
deleted file mode 100644
index 13d573e1..00000000
--- a/res/drawable-xhdpi/ic_warning_white_18dp.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/ic_warning_white_96dp.png b/res/drawable-xhdpi/ic_warning_white_96dp.png
deleted file mode 100644
index 50d1f295..00000000
--- a/res/drawable-xhdpi/ic_warning_white_96dp.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/ic_warning_yellow_24dp.png b/res/drawable-xhdpi/ic_warning_yellow_24dp.png
new file mode 100644
index 00000000..accb0613
--- /dev/null
+++ b/res/drawable-xhdpi/ic_warning_yellow_24dp.png
Binary files differ
diff --git a/res/drawable-xhdpi/banner.png b/res/drawable-xhdpi/live_tv_banner.png
index 57dbb05e..57dbb05e 100644
--- a/res/drawable-xhdpi/banner.png
+++ b/res/drawable-xhdpi/live_tv_banner.png
Binary files differ
diff --git a/res/drawable-xhdpi/usb_antenna.png b/res/drawable-xhdpi/usb_antenna.png
deleted file mode 100644
index ca5b2d72..00000000
--- a/res/drawable-xhdpi/usb_antenna.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable/menu_background.xml b/res/drawable/menu_background.xml
index bdd55c8c..defee88f 100644
--- a/res/drawable/menu_background.xml
+++ b/res/drawable/menu_background.xml
@@ -14,8 +14,13 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-
-<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
- android:src="@drawable/bg_protection"
- android:tileModeX="repeat"
- android:tileModeY="mirror" />
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <!-- 0% to 20%black half way down then 100% black-->
+ <gradient
+ android:startColor="#00000000"
+ android:endColor="#000000"
+ android:centerColor="#a0000000"
+ android:centerY=".5"
+ android:angle="270" />
+</shape>
diff --git a/res/layout/channel_banner.xml b/res/layout/channel_banner.xml
index 3f105fe3..4d3cc244 100644
--- a/res/layout/channel_banner.xml
+++ b/res/layout/channel_banner.xml
@@ -83,13 +83,22 @@
android:layout_toEndOf="@id/anchor"
android:visibility="gone" />
+ <ImageView android:id="@+id/channel_signal_strength"
+ android:layout_width="@dimen/channel_banner_input_logo_size"
+ android:layout_height="@dimen/channel_banner_input_logo_size"
+ android:layout_marginEnd="8dp"
+ android:layout_marginBottom="-2dp"
+ android:layout_alignBottom="@id/anchor"
+ android:layout_toEndOf="@id/tvinput_logo"
+ android:visibility="gone" />
+
<TextView android:id="@+id/channel_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="12dp"
android:layout_marginBottom="-4sp"
android:layout_alignBottom="@id/anchor"
- android:layout_toEndOf="@id/tvinput_logo"
+ android:layout_toEndOf="@id/channel_signal_strength"
android:singleLine="true"
android:ellipsize="end"
android:maxWidth="@dimen/channel_name_max_width"
diff --git a/res/layout/dvr_details_description.xml b/res/layout/dvr_details_description.xml
index d55688ba..ee749526 100644
--- a/res/layout/dvr_details_description.xml
+++ b/res/layout/dvr_details_description.xml
@@ -42,6 +42,27 @@
android:orientation="vertical"
android:background="?android:attr/selectableItemBackground">
+ <LinearLayout android:id="@+id/dvr_details_description_error_message"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:visibility="gone">
+
+ <ImageView android:layout_width="30dp"
+ android:layout_height="27dp"
+ android:paddingTop="9dp"
+ android:paddingLeft="12dp"
+ android:src="@drawable/ic_error_outline_pink_24dp"/>
+
+ <TextView android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingTop="9dp"
+ android:paddingRight="12dp"
+ android:textColor="@color/dvr_recording_failed_text_color"
+ android:text="@string/dvr_recording_failed"
+ style="?attr/detailsDescriptionBodyStyle" />
+ </LinearLayout>
+
<TextView android:id="@+id/dvr_details_description_body"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
diff --git a/res/layout/dvr_recording_card_view.xml b/res/layout/dvr_recording_card_view.xml
index 3e953510..3bf9bf6f 100644
--- a/res/layout/dvr_recording_card_view.xml
+++ b/res/layout/dvr_recording_card_view.xml
@@ -30,7 +30,7 @@
android:layout_gravity="center_horizontal"
android:scaleType="centerCrop"
android:contentDescription="@null"
- tv:layout_viewType="main" />
+ tv:layout_viewType="main"/>
<ProgressBar android:id="@+id/recording_progress"
style="@android:style/Widget.ProgressBar.Horizontal"
@@ -42,21 +42,7 @@
android:indeterminate="false"
android:visibility="gone"
android:max="100"
- android:layout_gravity="bottom" />
-
- <FrameLayout android:id="@+id/affiliated_icon_container"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:background="@drawable/card_image_gradient"
- android:visibility="invisible">
-
- <ImageView android:id="@+id/affiliated_icon"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="bottom|right"
- android:layout_margin="12dp" />
-
- </FrameLayout>
+ android:layout_gravity="bottom"/>
</FrameLayout>
<LinearLayout android:id="@+id/info_area"
@@ -99,10 +85,17 @@
android:layout_width="match_parent"
android:layout_height="wrap_content" >
+ <ImageView android:id="@+id/content_icon"
+ android:paddingTop="2dp"
+ android:layout_width="13dp"
+ android:layout_height="15dp"
+ android:gravity="start"
+ android:visibility="gone"/>
+
<TextView android:id="@+id/content_major"
+ android:layout_toEndOf="@+id/content_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:gravity="start"
style="@style/dvr_card_view_content_text" />
<TextView android:id="@+id/content_minor"
diff --git a/res/layout/dvr_schedules_item.xml b/res/layout/dvr_schedules_item.xml
index 90e1123d..9e9ee6af 100644
--- a/res/layout/dvr_schedules_item.xml
+++ b/res/layout/dvr_schedules_item.xml
@@ -100,14 +100,25 @@
android:lines="1"
android:textColor="@color/dvr_schedules_item_info"/>
</LinearLayout>
- <TextView android:id="@+id/conflict_info"
- android:layout_width="match_parent"
+
+ <LinearLayout android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:gravity="start"
- android:textSize="10sp"
- android:layout_marginBottom="@dimen/dvr_schedules_item_conflict_info_bottom_margin"
- android:textColor="@color/dvr_schedules_item_info"
- android:visibility="gone"/>
+ android:orientation="horizontal">
+ <ImageView android:id="@+id/extra_info_icon"
+ android:layout_width="13dp"
+ android:layout_height="13dp"
+ android:paddingTop="2dp"
+ android:src="@drawable/ic_error_outline_pink_24dp"
+ android:visibility="gone"/>
+ <TextView android:id="@+id/extra_info"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="start"
+ android:textSize="10sp"
+ android:layout_marginBottom="@dimen/dvr_schedules_item_conflict_info_bottom_margin"
+ android:textColor="@color/dvr_schedules_item_info"
+ android:visibility="gone"/>
+ </LinearLayout>
</LinearLayout>
</LinearLayout>
diff --git a/res/layout/menu_card_down.xml b/res/layout/menu_card_down.xml
new file mode 100644
index 00000000..0ccfc897
--- /dev/null
+++ b/res/layout/menu_card_down.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+
+<com.android.tv.menu.SimpleCardView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="@dimen/card_layout_width"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:elevation="@dimen/card_elevation_normal"
+ android:focusable="true"
+ android:clickable="true">
+
+ <ImageView
+ android:layout_width="@dimen/card_image_layout_width"
+ android:layout_height="@dimen/card_image_layout_height"
+ android:background="@color/channel_card_guide"
+ android:paddingBottom="16dp"
+ android:paddingEnd="30dp"
+ android:paddingStart="30dp"
+ android:paddingTop="16dp"
+ android:src="@drawable/quantum_ic_arrow_downward_white_36" />
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/card_meta_layout_height"
+ android:paddingStart="@dimen/card_meta_padding_start"
+ android:paddingEnd="@dimen/card_meta_padding_end"
+ android:paddingTop="@dimen/card_meta_padding_top"
+ android:singleLine="true"
+ android:ellipsize="end"
+ android:fontFamily="@string/condensed_font"
+ android:textColor="@color/card_meta_text_color"
+ android:background="@color/guide_card_meta_background"
+ android:text="@string/channels_item_down"
+ android:textSize="12sp" />
+
+</com.android.tv.menu.SimpleCardView>
diff --git a/res/layout/menu_card_up.xml b/res/layout/menu_card_up.xml
new file mode 100644
index 00000000..2ba365ef
--- /dev/null
+++ b/res/layout/menu_card_up.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+
+<com.android.tv.menu.SimpleCardView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="@dimen/card_layout_width"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:elevation="@dimen/card_elevation_normal"
+ android:focusable="true"
+ android:clickable="true">
+
+ <ImageView
+ android:layout_width="@dimen/card_image_layout_width"
+ android:layout_height="@dimen/card_image_layout_height"
+ android:background="@color/channel_card_guide"
+ android:paddingBottom="16dp"
+ android:paddingEnd="30dp"
+ android:paddingStart="30dp"
+ android:paddingTop="16dp"
+ android:src="@drawable/quantum_ic_arrow_upward_white_36" />
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/card_meta_layout_height"
+ android:paddingStart="@dimen/card_meta_padding_start"
+ android:paddingEnd="@dimen/card_meta_padding_end"
+ android:paddingTop="@dimen/card_meta_padding_top"
+ android:singleLine="true"
+ android:ellipsize="end"
+ android:fontFamily="@string/condensed_font"
+ android:textColor="@color/card_meta_text_color"
+ android:background="@color/guide_card_meta_background"
+ android:text="@string/channels_item_up"
+ android:textSize="12sp" />
+
+</com.android.tv.menu.SimpleCardView>
diff --git a/res/layout/pin_dialog.xml b/res/layout/pin_dialog.xml
index 5071717d..d40d70ec 100644
--- a/res/layout/pin_dialog.xml
+++ b/res/layout/pin_dialog.xml
@@ -35,7 +35,8 @@
android:textColor="@color/pin_dialog_text_color"
android:fontFamily="@string/font"
android:visibility="invisible"
- android:singleLine="false"/>
+ android:singleLine="false"
+ android:focusableInTouchMode="true"/>
<LinearLayout
android:id="@+id/enter_pin"
@@ -54,36 +55,14 @@
android:fontFamily="@string/font"
android:singleLine="false" />
- <LinearLayout
+ <com.android.tv.dialog.picker.PinPicker
+ android:id="@+id/pin_picker"
+ android:importantForAccessibility="yes"
android:layout_width="match_parent"
- android:layout_height="144dp"
+ android:layout_height="154dp"
android:paddingStart="24dp"
android:paddingEnd="24dp"
android:gravity="center"
- android:orientation="horizontal">
-
- <view class="com.android.tv.dialog.PinDialogFragment$PinNumberPicker"
- android:id="@+id/first"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content" />
-
- <view class="com.android.tv.dialog.PinDialogFragment$PinNumberPicker"
- android:id="@+id/second"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginStart="8dp" />
-
- <view class="com.android.tv.dialog.PinDialogFragment$PinNumberPicker"
- android:id="@+id/third"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginStart="8dp" />
-
- <view class="com.android.tv.dialog.PinDialogFragment$PinNumberPicker"
- android:id="@+id/fourth"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginStart="8dp" />
- </LinearLayout>
+ />
</LinearLayout>
</FrameLayout>
diff --git a/res/layout/pin_number_picker.xml b/res/layout/pin_number_picker.xml
deleted file mode 100644
index 8e8de9f5..00000000
--- a/res/layout/pin_number_picker.xml
+++ /dev/null
@@ -1,51 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ 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.
- -->
-<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="48dp"
- android:layout_height="144dp">
-
- <TextView android:id="@+id/focused_background"
- android:layout_width="@dimen/pin_number_picker_text_view_width"
- android:layout_height="@dimen/pin_number_picker_text_view_height"
- android:layout_gravity="center"
- android:gravity="center"
- android:textSize="@dimen/pin_number_picker_text_size"
- android:textColor="@color/pin_number_picker_text_color"
- android:fontFamily="@string/light_font"
- android:background="@drawable/pin_number_picker_focused_background" />
-
- <LinearLayout
- android:id="@+id/number_view_holder"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="center"
- android:focusable="true"
- android:orientation="vertical">
-
- <TextView android:id="@+id/previous2_number"
- style="@style/pin_number_view"/>
- <TextView android:id="@+id/previous_number"
- style="@style/pin_number_view"/>
- <TextView android:id="@+id/current_number"
- style="@style/pin_number_view"/>
- <TextView android:id="@+id/next_number"
- style="@style/pin_number_view"/>
- <TextView android:id="@+id/next2_number"
- style="@style/pin_number_view"/>
- </LinearLayout>
-
-</FrameLayout>
diff --git a/res/layout/tunable_tv_view.xml b/res/layout/tunable_tv_view.xml
index 549d0535..00c9908c 100644
--- a/res/layout/tunable_tv_view.xml
+++ b/res/layout/tunable_tv_view.xml
@@ -17,27 +17,6 @@
<merge xmlns:android="http://schemas.android.com/apk/res/android" >
- <View android:id="@+id/channel_up"
- android:layout_width="wrap_content"
- android:focusable="false"
- android:focusableInTouchMode="true"
- android:layout_height="1dp"
- android:layout_gravity="top" />
- <View android:id="@+id/placeholder"
- android:layout_width="1dp"
- android:layout_height="1dp"
- android:focusable="false"
- android:focusableInTouchMode="true"
- android:focusedByDefault="true"
- android:layout_gravity="center" />
-
- <View android:id="@+id/channel_down"
- android:layout_width="wrap_content"
- android:focusable="false"
- android:focusableInTouchMode="true"
- android:layout_height="1dp"
- android:layout_gravity="bottom" />
-
<com.android.tv.ui.AppLayerTvView android:id="@+id/tv_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
diff --git a/res/values/arrays.xml b/res/values/arrays.xml
index e57b0546..0604dd2b 100644
--- a/res/values/arrays.xml
+++ b/res/values/arrays.xml
@@ -29,31 +29,130 @@
<!-- The category strings to be displayed in the channel guide.
This list should be synced the data in src/com/android/tv/data/GenreItems.java -->
<eat-comment />
- <!-- Genre list [CHAR LIMIT=20] -->
+ <!-- Genre list [CHAR LIMIT=25] -->
<string-array name="genre_labels" translatable="true">
+ <!-- This is an item in a list to filter channels shown in a TV program guide, based on
+ genres.
+ "All channels", implies all channels will be shown in the program guide.
+ [CHAR LIMIT=25] -->
<item>All channels</item>
+ <!-- This is an item in a list to filter channels shown in a TV program guide, based on
+ genres.
+ "Family/Kids", implies only channels with "Family/Kids" programs will be shown in the
+ guide.
+ "Family/Kids" programs, are programs designed for families and safe for viewing by
+ young children.
+ [CHAR LIMIT=25] -->
<item>Family/Kids</item>
+ <!-- This is an item in a list to filter channels shown in a TV program guide, based on
+ genres.
+ "Sports", implies only channels with "Sports" programs will be shown in the guide.
+ "Sports" programs, include sporting events, news and other shows about sports.
+ [CHAR LIMIT=25] -->
<item>Sports</item>
+ <!-- This is an item in a list to filter channels shown in a TV program guide, based on
+ genres.
+ "Shopping", implies only channels with "Shopping" programs will be shown in the guide.
+ "Shopping" programs are TV shows where people can buy or bid on items.
+ [CHAR LIMIT=25] -->
<item>Shopping</item>
+ <!-- This is an item in a list to filter channels shown in a TV program guide, based on
+ genres.
+ "Movies", implies only channels with "Movies" programs will be shown in the guide.
+ [CHAR LIMIT=25] -->
<item>Movies</item>
+ <!-- This is an item in a list to filter channels shown in a TV program guide, based on
+ genres.
+ "Comedy", implies only channels with "Comedy" programs will be shown in the guide.
+ "Comedy" programs are generally intended to be humorous or amusing.
+ [CHAR LIMIT=25] -->
<item>Comedy</item>
+ <!-- This is an item in a list to filter channels shown in a TV program guide, based on
+ genres.
+ "Travel", implies only channels with "Travel" programs will be shown in the guide.
+ "Travel" programs feature popular destination or travel reviews.
+ [CHAR LIMIT=25] -->
<item>Travel</item>
+ <!-- This is an item in a list to filter channels shown in a TV program guide, based on
+ genres.
+ "Drama", implies only channels with "Drama" programs will be shown in the guide.
+ "Drama" programs are fictional shows, featuring actors.
+ [CHAR LIMIT=25] -->
<item>Drama</item>
+ <!-- This is an item in a list to filter channels shown in a TV program guide, based on
+ genres.
+ "Education", implies only channels with "Education" programs will be shown in the guide.
+ "Education" programs are designed to teach, either formally or informally.
+ [CHAR LIMIT=25] -->
<item>Education</item>
+ <!-- This is an item in a list to filter channels shown in a TV program guide, based on
+ genres.
+ "Animal/Wildlife", implies only channels with "Animal/Wildlife" programs will be shown in
+ the guide.
+ "Animal/Wildlife" programs are about wild animals or pets.
+ [CHAR LIMIT=25] -->
<item>Animal/Wildlife</item>
+ <!-- This is an item in a list to filter channels shown in a TV program guide, based on
+ genres.
+ "News", implies only channels with "News" programs will be shown in the guide.
+ "News" programs report world or local events as they unfold.
+ [CHAR LIMIT=25] -->
<item>News</item>
+ <!-- This is an item in a list to filter channels shown in a TV program guide, based on
+ genres.
+ "Gaming", implies only channels with "Gaming" programs will be shown in the guide.
+ "Gaming" programs rare about games, including video games and board games,
+ but excluding sports.
+ [CHAR LIMIT=25] -->
<item>Gaming</item>
+ <!-- This is an item in a list to filter channels shown in a TV program guide, based on
+ genres.
+ "Arts", implies only channels with "Arts" programs will be shown in the guide.
+ "Arts" programs about artistic endeavours or events like dance, music theater, drawing.
+ [CHAR LIMIT=25] -->
<item>Arts</item>
+ <!-- This is an item in a list to filter channels shown in a TV program guide, based on
+ genres.
+ "Entertainment", implies only channels with "Entertainment" programs will be shown in
+ the guide.
+ "Entertainment" programs discuss news and the people involved in the entertainment
+ industry.
+ [CHAR LIMIT=25] -->
<item>Entertainment</item>
+ <!-- This is an item in a list to filter channels shown in a TV program guide, based on
+ genres.
+ "Lifestyle", implies only channels with "Lifestyle" programs will be shown in
+ the guide.
+ "Lifestyle" programs feature topics such as fashion, diet, exercise, health and leisure
+ pursuits.
+ [CHAR LIMIT=25] -->
<item>Lifestyle</item>
+ <!-- This is an item in a list to filter channels shown in a TV program guide, based on
+ genres.
+ "Music", implies only channels with "Music" programs will be shown in
+ the guide.
+ "Music" programs feature live or recorded music.
+ [CHAR LIMIT=25] -->
<item>Music</item>
+ <!-- This is an item in a list to filter channels shown in a TV program guide, based on
+ genres.
+ "Premier", implies only channels with "Premier" programs will be shown in
+ the guide.
+ "Premier" programs are available at an extra cost.
+ [CHAR LIMIT=25] -->
<item>Premier</item>
+ <!-- This is an item in a list to filter channels shown in a TV program guide, based on
+ genres.
+ "Tech/Science", implies only channels with "Tech/Science" programs will be shown in
+ the guide.
+ "Tech/Science" programs are about science or technology .
+ [CHAR LIMIT=25] -->
<item>Tech/Science</item>
</string-array>
<!-- Titles in the onboarding page. -->
<string-array name="welcome_page_titles">
- <item>Live TV</item>
+ <item><xliff:g id="app_name">Live TV</xliff:g> </item>
<item>A simple way to discover content</item>
<item>Download apps, get more channels</item>
<item>Customize your channel line-up</item>
diff --git a/res/values/colors.xml b/res/values/colors.xml
index e0a0b99f..b68feb13 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -158,4 +158,6 @@
<color name="dvr_guided_step_action_text_color_selected">#111111</color>
<color name="dvr_detail_default_background">#FF01579B</color>
<color name="dvr_detail_default_background_scrim">#CC000000</color>
+ <color name="dvr_recording_failed_text_color">#FFCDD2</color>
+ <color name="dvr_recording_conflict_text_color">#FFE082</color>
</resources>
diff --git a/tuner/tests/TestManifest.xml b/res/values/strings-custom.xml
index f84aa90f..22f73318 100644
--- a/tuner/tests/TestManifest.xml
+++ b/res/values/strings-custom.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
- ~ Copyright (C) 2018 The Android Open Source Project
+ ~ Copyright (C) 2019 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
@@ -14,10 +14,9 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
+<resources>
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="com.android.tv.tuner.tests">
- <!-- android_local_test needs minSdkVersion set -->
- <uses-sdk android:minSdkVersion="23"/>
+ <!-- Name of application [CHAR LIMIT=NONE] -->
+ <string name="app_name" translatable="false">Live TV</string>
-</manifest> \ No newline at end of file
+</resources> \ No newline at end of file
diff --git a/res/values/strings.xml b/res/values/strings.xml
index cea4ee6c..36824759 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -26,21 +26,18 @@
<string name="option_item_divider_font" translatable="false">@string/condensed_font</string>
- <!-- Name of application [CHAR LIMIT=NONE] -->
- <string name="app_name">Live TV</string>
-
<!-- Title of an application permission, listed so the user can choose
whether they want to allow the application to do this. -->
- <string name="permlab_receiveInputEvent" translatable="false">receive input events from Live TV app</string>
+ <string name="permlab_receiveInputEvent" translatable="false">receive input events from <xliff:g id="app_name">Live TV</xliff:g> app</string>
<!-- Description of an application permission, listed so the user can choose
whether they want to allow the application to do this. -->
- <string name="permdesc_receiveInputEvent" translatable="false">Allows the app to receive input events from Live TV app</string>
+ <string name="permdesc_receiveInputEvent" translatable="false">Allows the app to receive input events from <xliff:g id="app_name">Live TV</xliff:g> app</string>
<!-- Title of an application permission, listed so the user can choose
whether they want to allow the application to do this. -->
- <string name="permlab_customizeTvApp" translatable="false">customize Live TV app</string>
+ <string name="permlab_customizeTvApp" translatable="false">customize <xliff:g id="app_name">Live TV</xliff:g> app</string>
<!-- Description of an application permission, listed so the user can choose
whether they want to allow the application to do this. -->
- <string name="permdesc_customizeTvApp" translatable="false">Allows the app to customize Live TV app</string>
+ <string name="permdesc_customizeTvApp" translatable="false">Allows the app to customize <xliff:g id="app_name">Live TV</xliff:g> app</string>
<!-- Program information, mainly used for channel banner and program guide. -->
<eat-comment />
@@ -90,6 +87,12 @@
<!-- Label of Program guide item in the channel list row. [CHAR LIMIT=23] -->
<string name="channels_item_program_guide">Program guide</string>
+ <!-- Label of the item in the "channel menu" that changes the current TV channel in the "up"
+ direction, to the next larger channel number. [CHAR LIMIT=23] -->
+ <string name="channels_item_up">Channel up</string>
+ <!-- Label of the item in the "channel menu" that changes the current TV channel in the "down"
+ direction, to the next smaller channel number. [CHAR LIMIT=23] -->
+ <string name="channels_item_down">Channel down</string>
<!-- Label of setup item in the channel list row. The item is shown only
when new inputs are installed. [CHAR LIMIT=23] -->
<string name="channels_item_setup">New channels available</string>
@@ -366,10 +369,22 @@
<eat-comment />
<!-- Description on the locked screen when current channel is locked by parental control. [CHAR LIMIT=NONE] -->
<string name="tvview_channel_locked">To watch this channel, press Right and enter your PIN</string>
+ <!-- Description on the locked screen when current channel is locked by parental control and talk back is turned on.
+ "press select" refers to a button on the remote. [CHAR LIMIT=NONE] -->
+ <string name="tvview_channel_locked_talkback">To watch this channel, press select and enter your PIN</string>
<!-- Description on the locked screen when the rating of the current content is restricted by parental control. [CHAR LIMIT=NONE] -->
<string name="tvview_content_locked">To watch this program, press Right and enter your PIN</string>
+ <!-- Description on the locked screen when the rating of the current content is restricted by parental control and talk back is turned on.
+ "press select" refers to a button on the remote.[CHAR LIMIT=NONE] -->
+ <string name="tvview_content_locked_talkback">To watch this program, press select and enter your PIN</string>
<!-- Description on the locked screen when the current content is unrated and it's restricted by parental control. [CHAR LIMIT=NONE] -->
<string name="tvview_content_locked_unrated">This program is unrated.\nTo watch this program, press Right and enter your PIN</string>
+ <!-- Description on the locked screen when the current content is unrated and it's restricted by parental control and talk back is turned on.
+ "press select" refers to a button on the remote.[CHAR LIMIT=NONE] -->
+ <string name="tvview_content_locked_unrated_talkback">This program is unrated.\nTo watch this program, press select and enter your PIN</string>
+ <!-- Description on the locked screen with the rating when the rating of the current content is restricted by parental control and talk back is turned on. [CHAR LIMIT=NONE]
+ "press select" refers to a button on the remote.-->
+ <string name="tvview_content_locked_format_talkback">This program is rated <xliff:g id="rating" example="TV_MA">%1$s</xliff:g>.\nTo watch this program, press select and enter your PIN.</string>
<!-- Description on the locked screen with the rating when the rating of the current content is restricted by parental control. [CHAR LIMIT=NONE] -->
<string name="tvview_content_locked_format">This program is rated <xliff:g id="rating" example="TV_MA">%1$s</xliff:g>.\nTo watch this program, press Right and enter your PIN.</string>
<!-- Description on the locked screen when current channel is locked by parental control. [CHAR LIMIT=NONE] -->
@@ -464,7 +479,7 @@
the source video through to the display and don't provide ability to tune to a specific
channel unless the user directly controls the external source device (e.g. game console,
DVD player, settop box, etc) that is connected to the TV. [CHAR LIMIT=NONE] -->
- <string name="msg_not_passthrough_input">Tuner type not suitable. Please launch Live TV app for tuner type TV input.</string>
+ <string name="msg_not_passthrough_input">Tuner type not suitable. Please launch <xliff:g id="app_name">Live TV</xliff:g> app for tuner type TV input.</string>
<!-- Error message when tune is failed. [CHAR LIMIT=NONE] -->
<string name="msg_tune_failed">Tune failed</string>
<!-- Error message when the user attempts an action (select TIS setup-activity, app-link,
@@ -475,11 +490,13 @@
<string name="msg_all_channels_hidden">All source channels are hidden.\nSelect at least one channel to watch.</string>
<!-- Message displayed when availability is changed by unknown reason. [CHAR LIMIT=NONE] -->
<string name="msg_channel_unavailable_unknown">The video is unexpectedly unavailable</string>
+ <!-- Message displayed when a TV input (eg HDMI Cable) is not physically connected. [CHAR LIMIT=NONE] -->
+ <string name="msg_channel_unavailable_not_connected">No Signal. Please check your source connection.</string>
<!-- Message to notify the different use of Back Button: Home Button(To exit) Back button
(commands for external device) [CHAR LIMIT=NONE] -->
<string name="msg_back_key_guide">BACK key is for connected device. Press HOME button to exit.</string>
<!-- Error message when a user denied to grant READ_TV_LISTING permission. [CHAR LIMIT=NONE] -->
- <string name="msg_read_tv_listing_permission_denied">Live TV needs permission to read the TV listings.</string>
+ <string name="msg_read_tv_listing_permission_denied"><xliff:g id="app_name">Live TV</xliff:g> needs permission to read the TV listings.</string>
<!-- Strings for debug or not to be shown to users -->
<eat-comment />
@@ -505,13 +522,13 @@
<string name="dvr_history_dialog_title" translatable="false">DVR history</string>
<!-- Display name of DVR recording service's notification channel. -->
- <string name="dvr_notification_channel_name" translatable="false">Live TV DVR</string>
+ <string name="dvr_notification_channel_name" translatable="false"><xliff:g id="app_name">Live TV</xliff:g> DVR</string>
<!-- Content title of DVR recording service's notification. -->
- <string name="dvr_notification_content_title" translatable="false">Live TV DVR</string>
+ <string name="dvr_notification_content_title" translatable="false"><xliff:g id="app_name">Live TV</xliff:g> DVR</string>
<!-- Content text of DVR recording service's notification during recording. -->
- <string name="dvr_notification_content_text_recording" translatable="false">Live TV are recording.</string>
+ <string name="dvr_notification_content_text_recording" translatable="false"><xliff:g id="app_name">Live TV</xliff:g> are recording.</string>
<!-- Content text of DVR recording service's notification during updating schedules. -->
- <string name="dvr_notification_content_text_loading" translatable="false">Live TV are updating recording schedules.</string>
+ <string name="dvr_notification_content_text_loading" translatable="false"><xliff:g id="app_name">Live TV</xliff:g> are updating recording schedules.</string>
<!-- Default content title of tuner installing notifications. -->
<string name="tuner_install_notification_content_title" translatable="false">Install <xliff:g id="tuner_package" example="Tuner package">%s</xliff:g></string>
@@ -577,7 +594,11 @@
<!-- Description of a card view to show full list of scheduled recordings. [CHAR LIMIT=25] -->
<string name="dvr_full_schedule_card_view_title">Full schedule</string>
<!-- Description of failed recordings. [CHAR LIMIT=25] -->
- <string name="dvr_recording_failed">Recording Failed</string>
+ <string name="dvr_recording_failed">Recording Failed.</string>
+ <!-- Description of failed recordings. [CHAR LIMIT=25] -->
+ <string name="dvr_recording_failed_no_period">Recording Failed</string>
+ <!-- Description of recording conflicts. [CHAR LIMIT=25] -->
+ <string name="dvr_recording_conflict">Recording Conflict</string>
<!-- Description of how many following days the schedule list will show. [CHAR LIMIT=25] -->
<plurals name="dvr_full_schedule_card_view_content">
<item quantity="one">Next %1$d day</item>
@@ -607,6 +628,8 @@
<!-- DVR detailed page -->
<eat-comment />
+ <!-- Button label to schedule a recording. -->
+ <string name="dvr_detail_schedule_recording">Schedule recording</string>
<!-- Button label to cancel the recording schedule. -->
<string name="dvr_detail_cancel_recording">Cancel recording</string>
<!-- Button label to stop the current recording. -->
@@ -631,6 +654,18 @@
<string name="dvr_detail_view_schedule">View schedule</string>
<!-- Text label to indicate there's more text in the details description [CHAR LIMIT=20] -->
<string name="dvr_detail_read_more">Read more</string>
+ <!-- Description of failed recordings caused by system failures. -->
+ <string name="dvr_recording_failed_system_failure">System failure. (Error code: <xliff:g id="errorCode" example="0">%1$d</xliff:g>)</string>
+ <!-- Description of failed recordings when they are not started correctly. -->
+ <string name="dvr_recording_failed_not_started">Recording was not started. Please check the antenna and hard drive connections (if any).</string>
+ <!-- Description of failed recordings when required resource is busy. -->
+ <string name="dvr_recording_failed_resource_busy">Failed to tune to the channel. Possible causes: weak signal, poor antenna connection, or recording conflict.</string>
+ <!-- Description of failed recordings when the input is unavailable. -->
+ <string name="dvr_recording_failed_input_unavailable"><xliff:g id="inputId" example="com.example.partnersupportsampletvinput/.SampleTvInputService">%1$s</xliff:g> unavailable.</string>
+ <!-- Description of failed recordings when the input doesn't support recording. -->
+ <string name="dvr_recording_failed_input_dvr_unsupported">Recording of this channel is not supported.</string>
+ <!-- Description of failed recordings when the space is insufficient. -->
+ <string name="dvr_recording_failed_insufficient_space">Insufficient space. Please connect an external storage device or delete some existing recordings.</string>
<!-- DVR series settings -->
@@ -754,7 +789,7 @@
sufficient space.-->
<string name="dvr_error_insufficient_space_description_three_or_more_recordings">The recordings of <xliff:g id="programName_1" example="Friends">%1$s</xliff:g>, <xliff:g id="programName_2" example="Friends">%2$s</xliff:g> and <xliff:g id="programName_3" example="Friends">%3$s</xliff:g> didn\'t complete due to insufficient storage.</string>
<!-- Dialog title which will be shown when the current storage is too small for DVR. -->
- <string name="dvr_error_small_sized_storage_title">More stroage needed</string>
+ <string name="dvr_error_small_sized_storage_title">More storage needed</string>
<!-- Dialog description which will be shown when the current storage is too small for DVR. -->
<string name="dvr_error_small_sized_storage_description">You will be able to record programs. However there is not enough storage on your device to start recording. Please connect an external drive that is <xliff:g id="storage_size" example="10GB">%1$d</xliff:g>GB or larger and follow the steps to format it as device storage.</string>
<!-- Dialog title which will be shown when there is no free space on the current storage for DVR. -->
@@ -930,6 +965,20 @@
<item quantity="other">(%1$d minutes)</item>
</plurals>
+ <!-- DVR history list strings -->
+ <!-- Short description of failed recordings. -->
+ <string name="dvr_recording_failed_short">Failed.</string>
+ <!-- Short description of failed recordings when they are not started correctly. -->
+ <string name="dvr_recording_failed_not_started_short">Failed to start recording.</string>
+ <!-- Short description of failed recordings when required resource is busy. -->
+ <string name="dvr_recording_failed_resource_busy_short">Failed to tune to the channel.</string>
+ <!-- Short description of failed recordings when the input is unavailable. -->
+ <string name="dvr_recording_failed_input_unavailable_short"><xliff:g id="inputId" example="com.example.partnersupportsampletvinput/.SampleTvInputService">%1$s</xliff:g> unavailable.</string>
+ <!-- Short description of failed recordings when the input doesn't support recording. -->
+ <string name="dvr_recording_failed_input_dvr_unsupported_short">Recording not supported.</string>
+ <!-- Short description of failed recordings when the space is insufficient. -->
+ <string name="dvr_recording_failed_insufficient_space_short">Insufficient space.</string>
+
<!-- DVR date related strings -->
<eat-comment/>
<!-- Date text to represent today. -->
@@ -952,4 +1001,21 @@
<eat-comment/>
<!-- Name for recorded programs preview channel -->
<string name="recorded_programs_preview_channel">Recorded Programs</string>
+
+ <!-- Request Permission -->
+ <eat-comment />
+ <!-- Title of the dialog to show storage permission rationale -->
+ <string name="write_storage_permission_rationale_title">Request permission</string>
+ <!-- The users has asked to delete TV programs they have recorded, however the application
+ first needs a system permission to delete files.
+ This is the text of a dialog that explains to the users they will be asked to grant
+ Live TV permission to delete files.
+ "Allow Live TV to access photos, media, and files on your device?\" is from the text
+ from the system message at
+ https://tc.corp.google.com/btviewer/messagedetail?project=AndroidPlatform&msgId=7885942926944299560
+ -->
+ <string name="write_storage_permission_rationale_description">You will be asked to, \"Allow <xliff:g id="app_name">Live TV</xliff:g> to access photos, media, and files on your device?\"\n
+ Selecting \"Allow\", enables <xliff:g id="app_name">Live TV</xliff:g> to immediately
+ free storage space when deleting recorded TV programs.
+ This makes more space available for new recordings.</string>
</resources>
diff --git a/res/values/themes.xml b/res/values/themes.xml
index 2165a756..96537077 100644
--- a/res/values/themes.xml
+++ b/res/values/themes.xml
@@ -77,7 +77,6 @@
<item name="guidanceIconStyle">@style/TV.Dvr.GuidanceIconStyle</item>
<item name="guidedActionsListStyle">@style/TV.Dvr.GuidedActionsListStyle</item>
<item name="guidedActionItemContainerStyle">@style/TV.Dvr.GuidedActionItemContainerStyle</item>
- <item name="guidedActionContentWidthWeight">@string/lb_guidedactions_width_weight</item>
</style>
<style name="Theme.TV.Dvr.GuidedStep.Twoline.Action" parent = "Theme.TV.Dvr.GuidedStep">
@@ -120,4 +119,4 @@
clicking DVR cards overlapping with fragment transition. -->
<item name="android:windowAllowEnterTransitionOverlap">false</item>
</style>
-</resources> \ No newline at end of file
+</resources>
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 00000000..6d5cb547
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+/*
+ * Experimental gradle configuration. This file may not be up to date.
+ */
+
+include ':common'
+include ':tuner'
+include ':SampleDvbTuner'
+project(":SampleDvbTuner").projectDir = file("tuner/SampleDvbTuner")
diff --git a/src/com/android/tv/ChannelChanger.java b/src/com/android/tv/ChannelChanger.java
new file mode 100644
index 00000000..55035696
--- /dev/null
+++ b/src/com/android/tv/ChannelChanger.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv;
+
+/** Changes the channel. */
+public interface ChannelChanger {
+
+ void channelUp();
+
+ void channelDown();
+}
diff --git a/src/com/android/tv/ChannelTuner.java b/src/com/android/tv/ChannelTuner.java
index 8ab145a4..fe138980 100644
--- a/src/com/android/tv/ChannelTuner.java
+++ b/src/com/android/tv/ChannelTuner.java
@@ -97,13 +97,7 @@ public class ChannelTuner {
mStarted = true;
mChannelDataManager.addListener(mChannelDataManagerListener);
if (mChannelDataManager.isDbLoadFinished()) {
- mHandler.post(
- new Runnable() {
- @Override
- public void run() {
- mChannelDataManagerListener.onLoadFinished();
- }
- });
+ mHandler.post(mChannelDataManagerListener::onLoadFinished);
}
}
diff --git a/src/com/android/tv/InputSessionManager.java b/src/com/android/tv/InputSessionManager.java
index 4f298ed6..ea17751b 100644
--- a/src/com/android/tv/InputSessionManager.java
+++ b/src/com/android/tv/InputSessionManager.java
@@ -20,11 +20,8 @@ import android.annotation.TargetApi;
import android.content.Context;
import android.media.tv.TvContentRating;
import android.media.tv.TvInputInfo;
-import android.media.tv.TvRecordingClient;
-import android.media.tv.TvRecordingClient.RecordingCallback;
import android.media.tv.TvTrackInfo;
import android.media.tv.TvView;
-import android.media.tv.TvView.TvInputCallback;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
@@ -36,9 +33,15 @@ import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.ArraySet;
import android.util.Log;
+import com.android.tv.common.compat.TvRecordingClientCompat;
+import com.android.tv.common.compat.TvRecordingClientCompat.RecordingCallbackCompat;
+import com.android.tv.common.compat.TvViewCompat;
+import com.android.tv.common.compat.TvViewCompat.TvInputCallbackCompat;
import com.android.tv.data.api.Channel;
+import com.android.tv.dvr.DvrTvView;
import com.android.tv.ui.TunableTvView;
import com.android.tv.ui.TunableTvView.OnTuneListener;
+import com.android.tv.ui.api.TunableTvViewPlayingApi;
import com.android.tv.util.TvInputManagerHelper;
import java.util.Collections;
import java.util.List;
@@ -87,7 +90,9 @@ public class InputSessionManager {
@MainThread
@NonNull
public TvViewSession createTvViewSession(
- TvView tvView, TunableTvView tunableTvView, TvInputCallback callback) {
+ TvViewCompat tvView,
+ TunableTvViewPlayingApi tunableTvView,
+ TvInputCallbackCompat callback) {
TvViewSession session = new TvViewSession(tvView, tunableTvView, callback);
mTvViewSessions.add(session);
if (DEBUG) Log.d(TAG, "TvView session created: " + session);
@@ -107,7 +112,7 @@ public class InputSessionManager {
public RecordingSession createRecordingSession(
String inputId,
String tag,
- RecordingCallback callback,
+ RecordingCallbackCompat callback,
Handler handler,
long endTimeMs) {
RecordingSession session = new RecordingSession(inputId, tag, callback, handler, endTimeMs);
@@ -237,9 +242,10 @@ public class InputSessionManager {
*/
@MainThread
public class TvViewSession {
- private final TvView mTvView;
- private final TunableTvView mTunableTvView;
- private final TvInputCallback mCallback;
+ private final TvViewCompat mTvView;
+ private final TunableTvViewPlayingApi mTunableTvView;
+ private final TvInputCallbackCompat mCallback;
+ private final boolean mIsDvrSession;
private Channel mChannel;
private String mInputId;
private Uri mChannelUri;
@@ -248,10 +254,14 @@ public class InputSessionManager {
private boolean mTuned;
private boolean mNeedToBeRetuned;
- TvViewSession(TvView tvView, TunableTvView tunableTvView, TvInputCallback callback) {
+ TvViewSession(
+ TvViewCompat tvView,
+ TunableTvViewPlayingApi tunableTvView,
+ TvInputCallbackCompat callback) {
mTvView = tvView;
mTunableTvView = tunableTvView;
mCallback = callback;
+ mIsDvrSession = tunableTvView instanceof DvrTvView;
mTvView.setCallback(
new DelegateTvInputCallback(mCallback) {
@Override
@@ -338,9 +348,13 @@ public class InputSessionManager {
void retune() {
if (DEBUG) Log.d(TAG, "Retune requested.");
+ if (mIsDvrSession) {
+ Log.w(TAG, "DVR session should not call retune()!");
+ return;
+ }
if (mNeedToBeRetuned) {
if (DEBUG) Log.d(TAG, "Retuning: {channel=" + mChannel + "}");
- mTunableTvView.tuneTo(mChannel, mParams, mOnTuneListener);
+ ((TunableTvView) mTunableTvView).tuneTo(mChannel, mParams, mOnTuneListener);
mNeedToBeRetuned = false;
}
}
@@ -369,9 +383,13 @@ public class InputSessionManager {
void resetByRecording() {
mCallback.onVideoUnavailable(
mInputId, TunableTvView.VIDEO_UNAVAILABLE_REASON_NO_RESOURCE);
+ if (mIsDvrSession) {
+ Log.w(TAG, "DVR session should not call resetByRecording()!");
+ return;
+ }
if (mTuned) {
if (DEBUG) Log.d(TAG, "Reset TvView session by recording");
- mTunableTvView.resetByRecording();
+ ((TunableTvView) mTunableTvView).resetByRecording();
reset();
}
mNeedToBeRetuned = true;
@@ -386,22 +404,22 @@ public class InputSessionManager {
public class RecordingSession {
private final String mInputId;
private Uri mChannelUri;
- private final RecordingCallback mCallback;
+ private final RecordingCallbackCompat mCallback;
private final Handler mHandler;
private volatile long mEndTimeMs;
- private TvRecordingClient mClient;
+ private TvRecordingClientCompat mClient;
private boolean mTuned;
RecordingSession(
String inputId,
String tag,
- RecordingCallback callback,
+ RecordingCallbackCompat callback,
Handler handler,
long endTimeMs) {
mInputId = inputId;
mCallback = callback;
mHandler = handler;
- mClient = new TvRecordingClient(mContext, tag, callback, handler);
+ mClient = new TvRecordingClientCompat(mContext, tag, callback, handler);
mEndTimeMs = endTimeMs;
}
@@ -409,29 +427,26 @@ public class InputSessionManager {
if (DEBUG) Log.d(TAG, "Release of recording session requested.");
runOnHandler(
mMainThreadHandler,
- new Runnable() {
- @Override
- public void run() {
- if (DEBUG) Log.d(TAG, "Releasing of recording session.");
- mTuned = false;
- mClient.release();
- mClient = null;
- for (TvViewSession session : mTvViewSessions) {
- if (DEBUG) {
- Log.d(
- TAG,
- "Finding TvView sessions for retune: {tuned="
- + session.mTuned
- + ", inputId="
- + session.mInputId
- + ", session="
- + session
- + "}");
- }
- if (!session.mTuned && Objects.equals(session.mInputId, mInputId)) {
- session.retune();
- break;
- }
+ () -> {
+ if (DEBUG) Log.d(TAG, "Releasing of recording session.");
+ mTuned = false;
+ mClient.release();
+ mClient = null;
+ for (TvViewSession session : mTvViewSessions) {
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "Finding TvView sessions for retune: {tuned="
+ + session.mTuned
+ + ", inputId="
+ + session.mInputId
+ + ", session="
+ + session
+ + "}");
+ }
+ if (!session.mTuned && Objects.equals(session.mInputId, mInputId)) {
+ session.retune();
+ break;
}
}
});
@@ -441,42 +456,39 @@ public class InputSessionManager {
public void tune(String inputId, Uri channelUri) {
runOnHandler(
mMainThreadHandler,
- new Runnable() {
- @Override
- public void run() {
- int tunedRecordingSessionCount = getTunedRecordingSessionCount(inputId);
- TvInputInfo input = mInputManager.getTvInputInfo(inputId);
- if (input == null
- || !input.canRecord()
- || input.getTunerCount() <= tunedRecordingSessionCount) {
- runOnHandler(
- mHandler,
- new Runnable() {
- @Override
- public void run() {
- mCallback.onConnectionFailed(inputId);
- }
- });
- return;
- }
- mTuned = true;
- int tunedTuneSessionCount = getTunedTvViewSessionCount(inputId);
- if (!isTunedForTvView(channelUri)
- && tunedTuneSessionCount > 0
- && tunedRecordingSessionCount + tunedTuneSessionCount
- >= input.getTunerCount()) {
- for (TvViewSession session : mTvViewSessions) {
- if (session.mTuned
- && Objects.equals(session.mInputId, inputId)
- && !isTunedForRecording(session.mChannelUri)) {
- session.resetByRecording();
- break;
- }
+ () -> {
+ int tunedRecordingSessionCount = getTunedRecordingSessionCount(inputId);
+ TvInputInfo input = mInputManager.getTvInputInfo(inputId);
+ if (input == null
+ || !input.canRecord()
+ || input.getTunerCount() <= tunedRecordingSessionCount) {
+ runOnHandler(
+ mHandler,
+ new Runnable() {
+ @Override
+ public void run() {
+ mCallback.onConnectionFailed(inputId);
+ }
+ });
+ return;
+ }
+ mTuned = true;
+ int tunedTuneSessionCount = getTunedTvViewSessionCount(inputId);
+ if (!isTunedForTvView(channelUri)
+ && tunedTuneSessionCount > 0
+ && tunedRecordingSessionCount + tunedTuneSessionCount
+ >= input.getTunerCount()) {
+ for (TvViewSession session : mTvViewSessions) {
+ if (session.mTuned
+ && Objects.equals(session.mInputId, inputId)
+ && !isTunedForRecording(session.mChannelUri)) {
+ session.resetByRecording();
+ break;
}
}
- mChannelUri = channelUri;
- mClient.tune(inputId, channelUri);
}
+ mChannelUri = channelUri;
+ mClient.tune(inputId, channelUri);
});
}
@@ -504,10 +516,10 @@ public class InputSessionManager {
}
}
- private static class DelegateTvInputCallback extends TvInputCallback {
- private final TvInputCallback mDelegate;
+ private static class DelegateTvInputCallback extends TvInputCallbackCompat {
+ private final TvInputCallbackCompat mDelegate;
- DelegateTvInputCallback(TvInputCallback delegate) {
+ DelegateTvInputCallback(TvInputCallbackCompat delegate) {
mDelegate = delegate;
}
@@ -565,6 +577,11 @@ public class InputSessionManager {
public void onTimeShiftStatusChanged(String inputId, int status) {
mDelegate.onTimeShiftStatusChanged(inputId, status);
}
+
+ @Override
+ public void onSignalStrength(String inputId, int value) {
+ mDelegate.onSignalStrength(inputId, value);
+ }
}
/** Called when the {@link TvView} channel is changed. */
diff --git a/src/com/android/tv/MainActivity.java b/src/com/android/tv/MainActivity.java
index 94a86cce..b4cf71db 100644
--- a/src/com/android/tv/MainActivity.java
+++ b/src/com/android/tv/MainActivity.java
@@ -22,7 +22,6 @@ import android.app.SearchManager;
import android.content.ActivityNotFoundException;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
-import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.Context;
import android.content.Intent;
@@ -49,6 +48,7 @@ import android.provider.Settings;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
import android.text.TextUtils;
import android.util.ArraySet;
import android.util.Log;
@@ -65,16 +65,22 @@ import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.widget.FrameLayout;
import android.widget.Toast;
+import com.android.tv.MainActivity.MySingletons;
import com.android.tv.analytics.SendChannelStatusRunnable;
import com.android.tv.analytics.SendConfigInfoRunnable;
import com.android.tv.analytics.Tracker;
+import com.android.tv.audio.AudioManagerHelper;
+import com.android.tv.audiotvservice.AudioOnlyTvServiceUtil;
import com.android.tv.common.BuildConfig;
+import com.android.tv.common.CommonConstants;
import com.android.tv.common.CommonPreferences;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.common.TvContentRatingCache;
import com.android.tv.common.WeakHandler;
+import com.android.tv.common.compat.TvInputInfoCompat;
import com.android.tv.common.feature.CommonFeatures;
import com.android.tv.common.memory.MemoryManageable;
+import com.android.tv.common.singletons.HasSingletons;
import com.android.tv.common.ui.setup.OnActionClickListener;
import com.android.tv.common.util.CommonUtils;
import com.android.tv.common.util.ContentUriUtils;
@@ -99,17 +105,19 @@ import com.android.tv.dvr.data.ScheduledRecording;
import com.android.tv.dvr.recorder.ConflictChecker;
import com.android.tv.dvr.ui.DvrStopRecordingFragment;
import com.android.tv.dvr.ui.DvrUiHelper;
+import com.android.tv.features.TvFeatures;
import com.android.tv.menu.Menu;
import com.android.tv.onboarding.OnboardingActivity;
import com.android.tv.parental.ContentRatingsManager;
import com.android.tv.parental.ParentalControlSettings;
-import com.android.tv.perf.EventNames;
-import com.android.tv.perf.PerformanceMonitor;
-import com.android.tv.perf.TimerEvent;
+import com.android.tv.perf.PerformanceMonitorManagerFactory;
+import com.android.tv.receiver.AudioCapabilitiesReceiver;
import com.android.tv.recommendation.ChannelPreviewUpdater;
import com.android.tv.recommendation.NotificationService;
import com.android.tv.search.ProgramGuideSearchFragment;
+import com.android.tv.tunerinputcontroller.BuiltInTunerManager;
import com.android.tv.ui.ChannelBannerView;
+import com.android.tv.ui.DetailsActivity;
import com.android.tv.ui.InputBannerView;
import com.android.tv.ui.KeypadChannelSwitchView;
import com.android.tv.ui.SelectInputView;
@@ -128,6 +136,7 @@ import com.android.tv.ui.sidepanel.SettingsFragment;
import com.android.tv.ui.sidepanel.SideFragment;
import com.android.tv.ui.sidepanel.parentalcontrols.ParentalControlsFragment;
import com.android.tv.util.AsyncDbTask;
+import com.android.tv.util.AsyncDbTask.DbExecutor;
import com.android.tv.util.CaptionSettings;
import com.android.tv.util.OnboardingUtils;
import com.android.tv.util.RecurringRunner;
@@ -140,6 +149,10 @@ import com.android.tv.util.ViewCache;
import com.android.tv.util.account.AccountHelper;
import com.android.tv.util.images.ImageCache;
+import com.google.common.base.Optional;
+import dagger.android.AndroidInjection;
+import dagger.android.ContributesAndroidInjector;
+import com.android.tv.common.flags.BackendKnobsFlags;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayDeque;
@@ -150,11 +163,21 @@ import java.util.Objects;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
+import javax.inject.Inject;
+import javax.inject.Provider;
/** The main activity for the Live TV app. */
-public class MainActivity extends Activity implements OnActionClickListener, OnPinCheckedListener {
+public class MainActivity extends Activity
+ implements OnActionClickListener,
+ OnPinCheckedListener,
+ ChannelChanger,
+ HasSingletons<MySingletons> {
private static final String TAG = "MainActivity";
private static final boolean DEBUG = false;
+ private AudioCapabilitiesReceiver mAudioCapabilitiesReceiver;
+
+ /** Singletons needed for this class. */
+ public interface MySingletons extends ChannelBannerView.MySingletons {}
@Retention(RetentionPolicy.SOURCE)
@IntDef({
@@ -175,6 +198,8 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
private static final float FRAME_RATE_FOR_FILM = 23.976f;
private static final float FRAME_RATE_EPSILON = 0.1f;
+// AOSP_Comment_Out private static final String PLUTO_TV_PACKAGE_NAME = "tv.pluto.android";
+
private static final int PERMISSIONS_REQUEST_READ_TV_LISTINGS = 1;
private static final String PERMISSION_READ_TV_LISTINGS = "android.permission.READ_TV_LISTINGS";
@@ -232,10 +257,17 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
private static final int UNDEFINED_TRACK_INDEX = -1;
private static final long START_UP_TIMER_RESET_THRESHOLD_MS = TimeUnit.SECONDS.toMillis(3);
+ {
+ PerformanceMonitorManagerFactory.create().getStartupMeasure().onActivityInit();
+ }
+
+ private final MySingletonsImpl mMySingletons = new MySingletonsImpl();
+ @Inject @DbExecutor Executor mDbExecutor;
+
private AccessibilityManager mAccessibilityManager;
- private ChannelDataManager mChannelDataManager;
- private ProgramDataManager mProgramDataManager;
- private TvInputManagerHelper mTvInputManagerHelper;
+ @Inject ChannelDataManager mChannelDataManager;
+ @Inject ProgramDataManager mProgramDataManager;
+ @Inject TvInputManagerHelper mTvInputManagerHelper;
private ChannelTuner mChannelTuner;
private final TvOptionsManager mTvOptionsManager = new TvOptionsManager(this);
private TvViewUiManager mTvViewUiManager;
@@ -245,10 +277,12 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
private final DurationTimer mTuneDurationTimer = new DurationTimer();
private DvrManager mDvrManager;
private ConflictChecker mDvrConflictChecker;
- private SetupUtils mSetupUtils;
+ @Inject BackendKnobsFlags mBackendKnobs;
+ @Inject SetupUtils mSetupUtils;
+ @Inject Optional<BuiltInTunerManager> mOptionalBuiltInTunerManager;
+ @VisibleForTesting protected TunableTvView mTvView;
private View mContentView;
- private TunableTvView mTvView;
private Bundle mTuneParams;
@Nullable private Uri mInitChannelUri;
@Nullable private String mParentInputIdWhenScreenOff;
@@ -274,9 +308,7 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
private boolean mNeedShowBackKeyGuide;
private boolean mVisibleBehind;
private boolean mShowNewSourcesFragment = true;
- private String mTunerInputId;
private boolean mOtherActivityLaunched;
- private PerformanceMonitor mPerformanceMonitor;
private boolean mIsInPIPMode;
private boolean mIsFilmModeSet;
@@ -304,6 +336,8 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
private RecurringRunner mSendConfigInfoRecurringRunner;
private RecurringRunner mChannelStatusRecurringRunner;
+ private String mLastInputIdFromIntent;
+
private final Handler mHandler = new MainActivityHandler(this);
private final Set<OnActionClickListener> mOnActionClickListeners = new ArraySet<>();
@@ -399,28 +433,27 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
public void onChannelChanged(Channel previousChannel, Channel currentChannel) {}
};
- private final Runnable mRestoreMainViewRunnable =
- new Runnable() {
- @Override
- public void run() {
- restoreMainTvView();
- }
- };
+ private final Runnable mRestoreMainViewRunnable = this::restoreMainTvView;
private ProgramGuideSearchFragment mSearchFragment;
private final TvInputCallback mTvInputCallback =
new TvInputCallback() {
@Override
public void onInputAdded(String inputId) {
- if (TvFeatures.TUNER.isEnabled(MainActivity.this)
- && mTunerInputId.equals(inputId)
+ if (mOptionalBuiltInTunerManager.isPresent()
&& CommonPreferences.shouldShowSetupActivity(MainActivity.this)) {
- Intent intent =
- TvSingletons.getSingletons(MainActivity.this)
- .getTunerSetupIntent(MainActivity.this);
- startActivity(intent);
- CommonPreferences.setShouldShowSetupActivity(MainActivity.this, false);
- mSetupUtils.markAsKnownInput(mTunerInputId);
+ BuiltInTunerManager builtInTunerManager =
+ mOptionalBuiltInTunerManager.get();
+ String tunerInputId = builtInTunerManager.getEmbeddedTunerInputId();
+ if (tunerInputId.equals(inputId)) {
+ Intent intent =
+ builtInTunerManager
+ .getTunerInputController()
+ .createSetupIntent(MainActivity.this);
+ startActivity(intent);
+ CommonPreferences.setShouldShowSetupActivity(MainActivity.this, false);
+ mSetupUtils.markAsKnownInput(tunerInputId);
+ }
}
}
};
@@ -435,12 +468,16 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
}
@Override
+ public MySingletons singletons() {
+ return mMySingletons;
+ }
+
+ @Override
protected void onCreate(Bundle savedInstanceState) {
+ AndroidInjection.inject(this);
mAccessibilityManager =
(AccessibilityManager) getSystemService(Context.ACCESSIBILITY_SERVICE);
TvSingletons tvSingletons = TvSingletons.getSingletons(this);
- mPerformanceMonitor = tvSingletons.getPerformanceMonitor();
- TimerEvent timer = mPerformanceMonitor.startTimer();
DurationTimer startUpDebugTimer = Debug.getTimer(Debug.TAG_START_UP_TIMER);
if (!startUpDebugTimer.isStarted()
|| startUpDebugTimer.getDuration() > START_UP_TIMER_RESET_THRESHOLD_MS) {
@@ -454,16 +491,13 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
}
Starter.start(this);
super.onCreate(savedInstanceState);
- if (!tvSingletons.getTvInputManagerHelper().hasTvInputManager()) {
+ if (!mTvInputManagerHelper.hasTvInputManager()) {
Log.wtf(TAG, "Stopping because device does not have a TvInputManager");
finishAndRemoveTask();
return;
}
- mPerformanceMonitor = tvSingletons.getPerformanceMonitor();
- mSetupUtils = tvSingletons.getSetupUtils();
- TvApplication tvApplication = (TvApplication) getApplication();
- mChannelDataManager = tvApplication.getChannelDataManager();
+ TvSingletons tvApplication = (TvSingletons) getApplication();
// In API 23, TvContract.isChannelUriForPassthroughInput is hidden.
boolean isPassthroughInput =
TvContract.isChannelUriForPassthroughInput(getIntent().getData());
@@ -480,17 +514,12 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
return;
}
setContentView(R.layout.activity_tv);
- mProgramDataManager = tvApplication.getProgramDataManager();
- mTvInputManagerHelper = tvApplication.getTvInputManagerHelper();
mTvView = (TunableTvView) findViewById(R.id.main_tunable_tv_view);
mTvView.initialize(mProgramDataManager, mTvInputManagerHelper);
mTvView.setOnUnhandledInputEventListener(
new OnUnhandledInputEventListener() {
@Override
public boolean onUnhandledInputEvent(InputEvent event) {
- if (DEBUG) {
- Log.d(TAG, "onUnhandledInputEvent " + event);
- }
if (isKeyEventBlocked()) {
return true;
}
@@ -511,7 +540,7 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
return false;
}
});
- mTvView.setOnTalkBackDpadKeyListener(keycode -> handleUpDownKeys(keycode, null));
+ mTvView.setBlockedInfoOnClickListener(v -> showPinDialogFragment());
long channelId = Utils.getLastWatchedChannelId(this);
String inputId = Utils.getLastWatchedTunerInputId(this);
if (!isPassthroughInput
@@ -525,10 +554,9 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
Toast.makeText(this, "Using Strict Mode for eng builds", Toast.LENGTH_SHORT).show();
}
mTracker = tvApplication.getTracker();
- if (TvFeatures.TUNER.isEnabled(this)) {
+ if (mOptionalBuiltInTunerManager.isPresent()) {
mTvInputManagerHelper.addCallback(mTvInputCallback);
}
- mTunerInputId = tvSingletons.getEmbeddedTunerInputId();
mProgramDataManager.addOnCurrentProgramUpdatedListener(
Channel.INVALID_ID, mOnCurrentProgramUpdatedListener);
mProgramDataManager.setPrefetchEnabled(true);
@@ -657,6 +685,8 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
mAccessibilityManager.addAccessibilityStateChangeListener(mOverlayManager);
mAudioManagerHelper = new AudioManagerHelper(this, mTvView);
+ mAudioCapabilitiesReceiver = new AudioCapabilitiesReceiver(this, null);
+ mAudioCapabilitiesReceiver.register();
Intent nowPlayingIntent = new Intent(this, MainActivity.class);
PendingIntent pendingIntent =
PendingIntent.getActivity(this, REQUEST_CODE_NOW_PLAYING, nowPlayingIntent, 0);
@@ -687,7 +717,6 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
}
initForTest();
Debug.getTimer(Debug.TAG_START_UP_TIMER).log("MainActivity.onCreate end");
- mPerformanceMonitor.stopTimer(timer, EventNames.MAIN_ACTIVITY_ONCREATE);
}
private void startOnboardingActivity() {
@@ -778,7 +807,6 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
@Override
protected void onStart() {
- TimerEvent timer = mPerformanceMonitor.startTimer();
if (DEBUG) {
Log.d(TAG, "onStart()");
}
@@ -796,15 +824,17 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
notificationIntent.setAction(NotificationService.ACTION_SHOW_RECOMMENDATION);
startService(notificationIntent);
}
- TvSingletons singletons = TvSingletons.getSingletons(this);
- singletons.getTunerInputController().executeNetworkTunerDiscoveryAsyncTask(this);
- singletons.getEpgFetcher().fetchImmediatelyIfNeeded();
- mPerformanceMonitor.stopTimer(timer, EventNames.MAIN_ACTIVITY_ONSTART);
+ if (mOptionalBuiltInTunerManager.isPresent()) {
+ mOptionalBuiltInTunerManager
+ .get()
+ .getTunerInputController()
+ .executeNetworkTunerDiscoveryAsyncTask(this);
+ }
+ TvSingletons.getSingletons(this).getEpgFetcher().fetchImmediatelyIfNeeded();
}
@Override
protected void onResume() {
- TimerEvent timer = mPerformanceMonitor.startTimer();
Debug.getTimer(Debug.TAG_START_UP_TIMER).log("MainActivity.onResume start");
if (DEBUG) Log.d(TAG, "onResume()");
super.onResume();
@@ -836,13 +866,9 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
getApplicationContext(), TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE)
&& !failedScheduledRecordingInfoSet.isEmpty()) {
runAfterAttachedToWindow(
- new Runnable() {
- @Override
- public void run() {
+ () ->
DvrUiHelper.showDvrInsufficientSpaceErrorDialog(
- MainActivity.this, failedScheduledRecordingInfoSet);
- }
- });
+ MainActivity.this, failedScheduledRecordingInfoSet));
}
if (mChannelTuner.areAllChannelsLoaded()) {
@@ -861,32 +887,23 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
// This will delay the start of the animation until after the Live Channel app is
// shown. Without this the animation is completed before it is actually visible on
// the screen.
- mHandler.post(
- new Runnable() {
- @Override
- public void run() {
- mOverlayManager.showProgramGuide();
- }
- });
+ mHandler.post(() -> mOverlayManager.showProgramGuide());
} else if (mShowSelectInputView) {
mShowSelectInputView = false;
// mShowSelectInputView is true when the activity is started/resumed because the
// TV_INPUT button was pressed in a different app. This will delay the start of
// the animation until after the Live Channel app is shown. Without this the
// animation is completed before it is actually visible on the screen.
- mHandler.post(
- new Runnable() {
- @Override
- public void run() {
- mOverlayManager.showSelectInputView();
- }
- });
+ mHandler.post(() -> mOverlayManager.showSelectInputView());
}
if (mDvrConflictChecker != null) {
mDvrConflictChecker.start();
}
+ if (CommonFeatures.ENABLE_TV_SERVICE.isEnabled(this) && isAudioOnlyInput()) {
+ // TODO(b/110969180): figure out when to call AudioOnlyTvServiceUtil.stopAudioOnlyInput
+ AudioOnlyTvServiceUtil.startAudioOnlyInput(this, mLastInputIdFromIntent);
+ }
Debug.getTimer(Debug.TAG_START_UP_TIMER).log("MainActivity.onResume end");
- mPerformanceMonitor.stopTimer(timer, EventNames.MAIN_ACTIVITY_ONRESUME);
}
@Override
@@ -913,7 +930,6 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
} else {
mTracker.sendScreenView(SCREEN_BEHIND_NAME);
}
- TvSingletons.getSingletons(this).getExperimentLoader().asyncRefreshExperiments(this);
super.onPause();
}
@@ -1068,6 +1084,9 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
markCurrentChannelDuringScreenOff();
}
}
+ if (mChannelTuner.isCurrentChannelPassthrough()) {
+ mInitChannelUri = mChannelTuner.getCurrentChannelUri();
+ }
mActivityStarted = false;
stopAll(false);
unregisterReceiver(mBroadcastReceiver);
@@ -1299,19 +1318,15 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
if (!Objects.equals(mTvView.getCurrentChannel(), returnChannel)) {
final Channel channel = returnChannel;
Runnable tuneAction =
- new Runnable() {
- @Override
- public void run() {
- tuneToChannel(channel);
- if (mChannelBeforeShrunkenTvView == null
- || !mChannelBeforeShrunkenTvView.equals(channel)) {
- Utils.setLastWatchedChannel(MainActivity.this, channel);
- }
- mIsCompletingShrunkenTvView = false;
- mIsCurrentChannelUnblockedByUser =
- mWasChannelUnblockedBeforeShrunkenByUser;
- mTvView.setBlockScreenType(getDesiredBlockScreenType());
+ () -> {
+ tuneToChannel(channel);
+ if (mChannelBeforeShrunkenTvView == null
+ || !mChannelBeforeShrunkenTvView.equals(channel)) {
+ Utils.setLastWatchedChannel(MainActivity.this, channel);
}
+ mIsCompletingShrunkenTvView = false;
+ mIsCurrentChannelUnblockedByUser = mWasChannelUnblockedBeforeShrunkenByUser;
+ mTvView.setBlockScreenType(getDesiredBlockScreenType());
};
mTvViewUiManager.fadeOutTvView(tuneAction);
// Will automatically fade-in when video becomes available.
@@ -1423,17 +1438,12 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
/** Notifies the key input focus is changed to the TV view. */
public void updateKeyInputFocus() {
- mHandler.post(
- new Runnable() {
- @Override
- public void run() {
- mTvView.setBlockScreenType(getDesiredBlockScreenType());
- }
- });
+ mHandler.post(() -> mTvView.setBlockScreenType(getDesiredBlockScreenType()));
}
// It should be called before onResume.
private boolean handleIntent(Intent intent) {
+ mLastInputIdFromIntent = getInputId(intent);
// Reset the closed caption settings when the activity is 1)created or 2) restarted.
// And do not reset while TvView is playing.
if (!mTvView.isPlaying()) {
@@ -1455,13 +1465,7 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
}
if (TvInputManager.ACTION_SETUP_INPUTS.equals(intent.getAction())) {
- runAfterAttachedToWindow(
- new Runnable() {
- @Override
- public void run() {
- mOverlayManager.showSetupFragment();
- }
- });
+ runAfterAttachedToWindow(() -> mOverlayManager.showSetupFragment());
} else if (Intent.ACTION_VIEW.equals(intent.getAction())) {
Uri uri = intent.getData();
if (Utils.isProgramsUri(uri)) {
@@ -1497,8 +1501,7 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
long channelIdFromIntent = ContentUriUtils.safeParseId(mInitChannelUri);
if (programUriFromIntent != null && channelIdFromIntent != Channel.INVALID_ID) {
new AsyncQueryProgramTask(
- TvSingletons.getSingletons(this).getDbExecutor(),
- getContentResolver(),
+ mDbExecutor,
programUriFromIntent,
Program.PROJECTION,
null,
@@ -1565,14 +1568,13 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
public AsyncQueryProgramTask(
Executor executor,
- ContentResolver contentResolver,
Uri uri,
String[] projection,
String selection,
String[] selectionArgs,
String orderBy,
long channelId) {
- super(executor, contentResolver, uri, projection, selection, selectionArgs, orderBy);
+ super(executor, MainActivity.this, uri, projection, selection, selectionArgs, orderBy);
mChannelIdFromIntent = channelId;
}
@@ -1593,26 +1595,12 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
}
Channel channel = mChannelDataManager.getChannel(mChannelIdFromIntent);
if (channel != null) {
- ScheduledRecording scheduledRecording =
- TvSingletons.getSingletons(MainActivity.this)
- .getDvrDataManager()
- .getScheduledRecordingForProgramId(program.getId());
- DvrUiHelper.checkStorageStatusAndShowErrorMessage(
- MainActivity.this,
- channel.getInputId(),
- new Runnable() {
- @Override
- public void run() {
- if (CommonFeatures.DVR.isEnabled(MainActivity.this)
- && scheduledRecording == null
- && mDvrManager.isProgramRecordable(program)) {
- DvrUiHelper.requestRecordingFutureProgram(
- MainActivity.this, program, false);
- } else {
- DvrUiHelper.showProgramInfoDialog(MainActivity.this, program);
- }
- }
- });
+ Intent intent = new Intent(MainActivity.this, DetailsActivity.class);
+ intent.putExtra(DetailsActivity.CHANNEL_ID, mChannelIdFromIntent);
+ intent.putExtra(DetailsActivity.DETAILS_VIEW_TYPE, DetailsActivity.PROGRAM_VIEW);
+ intent.putExtra(DetailsActivity.PROGRAM, program);
+ intent.putExtra(DetailsActivity.INPUT_ID, channel.getInputId());
+ startActivity(intent);
}
}
}
@@ -1671,6 +1659,11 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
return;
}
mTunePending = false;
+ if (CommonFeatures.TUNER_SIGNAL_STRENGTH.isEnabled(this)) {
+ mTvView.resetChannelSignalStrength();
+ mOverlayManager.updateChannelBannerAndShowIfNeeded(
+ TvOverlayManager.UPDATE_CHANNEL_BANNER_REASON_UPDATE_SIGNAL_STRENGTH);
+ }
final Channel channel = mChannelTuner.getCurrentChannel();
SoftPreconditions.checkState(channel != null);
if (channel == null) {
@@ -1717,18 +1710,14 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
&& mSetupUtils.hasUnrecognizedInput(mTvInputManagerHelper)) {
// Show new channel sources fragment.
runAfterAttachedToWindow(
- new Runnable() {
- @Override
- public void run() {
+ () ->
mOverlayManager.runAfterOverlaysAreClosed(
new Runnable() {
@Override
public void run() {
mOverlayManager.showNewSourcesFragment();
}
- });
- }
- });
+ }));
}
mSetupUtils.onTuned();
if (mTuneParams != null) {
@@ -1799,12 +1788,9 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
// should be closed when the activity is paused.
private void runAfterAttachedToWindow(final Runnable runnable) {
final Runnable runOnlyIfActivityIsResumed =
- new Runnable() {
- @Override
- public void run() {
- if (mActivityResumed) {
- runnable.run();
- }
+ () -> {
+ if (mActivityResumed) {
+ runnable.run();
}
};
if (mContentView.isAttachedToWindow()) {
@@ -1918,25 +1904,36 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
window.setAttributes(layoutParams);
}
- private void applyMultiAudio() {
+ @VisibleForTesting
+ protected void applyMultiAudio(String trackId) {
List<TvTrackInfo> tracks = getTracks(TvTrackInfo.TYPE_AUDIO);
if (tracks == null) {
mTvOptionsManager.onMultiAudioChanged(null);
return;
}
- String id = TvSettings.getMultiAudioId(this);
- String language = TvSettings.getMultiAudioLanguage(this);
- int channelCount = TvSettings.getMultiAudioChannelCount(this);
- TvTrackInfo bestTrack =
- TvTrackInfoUtils.getBestTrackInfo(tracks, id, language, channelCount);
+ TvTrackInfo bestTrack = null;
+ if (trackId != null) {
+ for (TvTrackInfo track : tracks) {
+ if (trackId.equals(track.getId())) {
+ bestTrack = track;
+ break;
+ }
+ }
+ }
+ if (bestTrack == null) {
+ String id = TvSettings.getMultiAudioId(this);
+ String language = TvSettings.getMultiAudioLanguage(this);
+ int channelCount = TvSettings.getMultiAudioChannelCount(this);
+ bestTrack = TvTrackInfoUtils.getBestTrackInfo(tracks, id, language, channelCount);
+ }
if (bestTrack != null) {
String selectedTrack = getSelectedTrack(TvTrackInfo.TYPE_AUDIO);
if (!bestTrack.getId().equals(selectedTrack)) {
selectTrack(TvTrackInfo.TYPE_AUDIO, bestTrack, UNDEFINED_TRACK_INDEX);
} else {
mTvOptionsManager.onMultiAudioChanged(
- Utils.getMultiAudioString(this, bestTrack, false));
+ TvTrackInfoUtils.getMultiAudioString(this, bestTrack, false));
}
return;
}
@@ -2056,8 +2053,8 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
if (mMediaSessionWrapper != null) {
mMediaSessionWrapper.release();
}
- if (mAudioManagerHelper != null) {
- mAudioManagerHelper.release();
+ if (mAudioCapabilitiesReceiver != null) {
+ mAudioCapabilitiesReceiver.unregister();
}
mHandler.removeCallbacksAndMessages(null);
application.getMainActivityWrapper().onMainActivityDestroyed(this);
@@ -2071,7 +2068,7 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
}
if (mTvInputManagerHelper != null) {
mTvInputManagerHelper.clearTvInputLabels();
- if (TvFeatures.TUNER.isEnabled(this)) {
+ if (mOptionalBuiltInTunerManager.isPresent()) {
mTvInputManagerHelper.removeCallback(mTvInputCallback);
}
}
@@ -2100,51 +2097,59 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
if (!mChannelTuner.areAllChannelsLoaded()) {
return false;
}
- if (handleUpDownKeys(keyCode, event)) {
- return true;
- }
- return super.onKeyDown(keyCode, event);
- }
-
- private boolean handleUpDownKeys(int keyCode, @Nullable KeyEvent event) {
if (!mChannelTuner.isCurrentChannelPassthrough()) {
switch (keyCode) {
case KeyEvent.KEYCODE_CHANNEL_UP:
case KeyEvent.KEYCODE_DPAD_UP:
- if ((event == null || event.getRepeatCount() == 0)
+ if (event.getRepeatCount() == 0
&& mChannelTuner.getBrowsableChannelCount() > 0) {
- // message sending should be done before moving channel, because we use the
- // existence of message to decide if users are switching channel.
- if (event != null) {
- mHandler.sendMessageDelayed(
- mHandler.obtainMessage(
- MSG_CHANNEL_UP_PRESSED, System.currentTimeMillis()),
- CHANNEL_CHANGE_INITIAL_DELAY_MILLIS);
- }
- moveToAdjacentChannel(true, false);
- mTracker.sendChannelUp();
+
+ channelUpPressed();
}
return true;
case KeyEvent.KEYCODE_CHANNEL_DOWN:
case KeyEvent.KEYCODE_DPAD_DOWN:
- if ((event == null || event.getRepeatCount() == 0)
+ if (event.getRepeatCount() == 0
&& mChannelTuner.getBrowsableChannelCount() > 0) {
- // message sending should be done before moving channel, because we use the
- // existence of message to decide if users are switching channel.
- if (event != null) {
- mHandler.sendMessageDelayed(
- mHandler.obtainMessage(
- MSG_CHANNEL_DOWN_PRESSED, System.currentTimeMillis()),
- CHANNEL_CHANGE_INITIAL_DELAY_MILLIS);
- }
- moveToAdjacentChannel(false, false);
- mTracker.sendChannelDown();
+ channelDownPressed();
}
return true;
default: // fall out
}
}
- return false;
+ return super.onKeyDown(keyCode, event);
+ }
+
+ @Override
+ public void channelDown() {
+ channelDownPressed();
+ finishChannelChangeIfNeeded();
+ }
+
+ private void channelDownPressed() {
+ // message sending should be done before moving channel, because we use the
+ // existence of message to decide if users are switching channel.
+ mHandler.sendMessageDelayed(
+ mHandler.obtainMessage(MSG_CHANNEL_DOWN_PRESSED, System.currentTimeMillis()),
+ CHANNEL_CHANGE_INITIAL_DELAY_MILLIS);
+ moveToAdjacentChannel(false, false);
+ mTracker.sendChannelDown();
+ }
+
+ @Override
+ public void channelUp() {
+ channelUpPressed();
+ finishChannelChangeIfNeeded();
+ }
+
+ private void channelUpPressed() {
+ // message sending should be done before moving channel, because we use the
+ // existence of message to decide if users are switching channel.
+ mHandler.sendMessageDelayed(
+ mHandler.obtainMessage(MSG_CHANNEL_UP_PRESSED, System.currentTimeMillis()),
+ CHANNEL_CHANGE_INITIAL_DELAY_MILLIS);
+ moveToAdjacentChannel(true, false);
+ mTracker.sendChannelUp();
}
@Override
@@ -2228,24 +2233,7 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
this, mChannelTuner.getCurrentChannel());
return true;
}
- if (!PermissionUtils.hasModifyParentalControls(this)) {
- return true;
- }
- PinDialogFragment dialog = null;
- if (mTvView.isScreenBlocked()) {
- dialog =
- PinDialogFragment.create(
- PinDialogFragment.PIN_DIALOG_TYPE_UNLOCK_CHANNEL);
- } else if (mTvView.isContentBlocked()) {
- dialog =
- PinDialogFragment.create(
- PinDialogFragment.PIN_DIALOG_TYPE_UNLOCK_PROGRAM,
- mTvView.getBlockedContentRating().flattenToString());
- }
- if (dialog != null) {
- mOverlayManager.showDialogFragment(
- PinDialogFragment.DIALOG_TAG, dialog, false);
- }
+ showPinDialogFragment();
return true;
case KeyEvent.KEYCODE_WINDOW:
enterPictureInPictureMode();
@@ -2315,16 +2303,12 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
DvrUiHelper.checkStorageStatusAndShowErrorMessage(
this,
currentChannel.getInputId(),
- new Runnable() {
- @Override
- public void run() {
+ () ->
DvrUiHelper.requestRecordingCurrentProgram(
MainActivity.this,
currentChannel,
program,
- false);
- }
- });
+ false));
}
} else {
DvrUiHelper.showStopRecordingDialog(
@@ -2391,6 +2375,24 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
return super.onKeyUp(keyCode, event);
}
+ private void showPinDialogFragment() {
+ if (!PermissionUtils.hasModifyParentalControls(this)) {
+ return;
+ }
+ PinDialogFragment dialog = null;
+ if (mTvView.isScreenBlocked()) {
+ dialog = PinDialogFragment.create(PinDialogFragment.PIN_DIALOG_TYPE_UNLOCK_CHANNEL);
+ } else if (mTvView.isContentBlocked()) {
+ dialog =
+ PinDialogFragment.create(
+ PinDialogFragment.PIN_DIALOG_TYPE_UNLOCK_PROGRAM,
+ mTvView.getBlockedContentRating().flattenToString());
+ }
+ if (dialog != null) {
+ mOverlayManager.showDialogFragment(PinDialogFragment.DIALOG_TAG, dialog, false);
+ }
+ }
+
@Override
public boolean onKeyLongPress(int keyCode, KeyEvent event) {
if (SystemProperties.LOG_KEYEVENT.getValue()) Log.d(TAG, "onKeyLongPress(" + event);
@@ -2423,13 +2425,7 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
mIsInPIPMode = true;
if (mOverlayManager.isOverlayOpened()) {
mOverlayManager.hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_WITHOUT_ANIMATION);
- mHandler.post(
- new Runnable() {
- @Override
- public void run() {
- MainActivity.super.enterPictureInPictureMode();
- }
- });
+ mHandler.post(MainActivity.super::enterPictureInPictureMode);
} else {
MainActivity.super.enterPictureInPictureMode();
}
@@ -2586,7 +2582,9 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
mTvView.selectTrack(type, track == null ? null : track.getId());
if (type == TvTrackInfo.TYPE_AUDIO) {
mTvOptionsManager.onMultiAudioChanged(
- track == null ? null : Utils.getMultiAudioString(this, track, false));
+ track == null
+ ? null
+ : TvTrackInfoUtils.getMultiAudioString(this, track, false));
} else if (type == TvTrackInfo.TYPE_SUBTITLE) {
mTvOptionsManager.onClosedCaptionsChanged(track, trackIndex);
}
@@ -2594,7 +2592,7 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
public void selectAudioTrack(String trackId) {
saveMultiAudioSetting(trackId);
- applyMultiAudio();
+ applyMultiAudio(trackId);
}
private void saveMultiAudioSetting(String trackId) {
@@ -2657,6 +2655,13 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
case TvInputManager.VIDEO_UNAVAILABLE_REASON_AUDIO_ONLY:
case TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL:
return;
+ case CommonConstants.VIDEO_UNAVAILABLE_REASON_NOT_CONNECTED:
+ Toast.makeText(
+ this,
+ R.string.msg_channel_unavailable_not_connected,
+ Toast.LENGTH_SHORT)
+ .show();
+ break;
case TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN:
default:
Toast.makeText(this, R.string.msg_channel_unavailable_unknown, Toast.LENGTH_SHORT)
@@ -2725,14 +2730,11 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
mLazyInitialized = true;
// Running initialization.
mHandler.postDelayed(
- new Runnable() {
- @Override
- public void run() {
- if (mActivityStarted) {
- initAnimations();
- initSideFragments();
- initMenuItemViews();
- }
+ () -> {
+ if (mActivityStarted) {
+ initAnimations();
+ initSideFragments();
+ initMenuItemViews();
}
},
LAZY_INITIALIZATION_DELAY);
@@ -2751,6 +2753,23 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
mOverlayManager.getMenu().preloadItemViews();
}
+ private boolean isAudioOnlyInput() {
+ if (mLastInputIdFromIntent == null) {
+ return false;
+ }
+ TvInputInfoCompat inputInfo =
+ mTvInputManagerHelper.getTvInputInfoCompat(mLastInputIdFromIntent);
+ return inputInfo != null && inputInfo.isAudioOnly();
+ }
+
+ @Nullable
+ private String getInputId(Intent intent) {
+ Uri uri = intent.getData();
+ return TvContract.isChannelUriForPassthroughInput(uri)
+ ? uri.getPathSegments().get(1)
+ : null;
+ }
+
@Override
public void onTrimMemory(int level) {
super.onTrimMemory(level);
@@ -2793,15 +2812,22 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
}
}
- private class MyOnTuneListener implements OnTuneListener {
+ /** {@link OnTuneListener} implementation */
+ @VisibleForTesting
+ protected class MyOnTuneListener implements OnTuneListener {
boolean mUnlockAllowedRatingBeforeShrunken = true;
boolean mWasUnderShrunkenTvView;
Channel mChannel;
- private void onTune(Channel channel, boolean wasUnderShrukenTvView) {
+ private void onTune(Channel channel, boolean wasUnderShrunkenTvView) {
Debug.getTimer(Debug.TAG_START_UP_TIMER).log("MainActivity.MyOnTuneListener.onTune");
mChannel = channel;
- mWasUnderShrunkenTvView = wasUnderShrukenTvView;
+ mWasUnderShrunkenTvView = wasUnderShrunkenTvView;
+
+ if (mBackendKnobs.enablePartialProgramFetch()) {
+ // Fetch complete projection of tuned channel.
+ mProgramDataManager.prefetchChannel(channel.getId());
+ }
}
@Override
@@ -2824,7 +2850,7 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
}
@Override
- public void onStreamInfoChanged(StreamInfo info) {
+ public void onStreamInfoChanged(StreamInfo info, boolean allowAutoSelectionOfTrack) {
if (info.isVideoAvailable() && mTuneDurationTimer.isRunning()) {
mTracker.sendChannelTuneTime(info.getCurrentChannel(), mTuneDurationTimer.reset());
}
@@ -2834,7 +2860,8 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
}
applyDisplayRefreshRate(info.getVideoFrameRate());
mTvViewUiManager.updateTvAspectRatio();
- applyMultiAudio();
+ applyMultiAudio(
+ allowAutoSelectionOfTrack ? null : getSelectedTrack(TvTrackInfo.TYPE_AUDIO));
applyClosedCaption();
mOverlayManager.getMenu().onStreamInfoChanged();
if (mTvView.isVideoAvailable()) {
@@ -2861,6 +2888,12 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
+ channel);
return;
}
+ /* Begin_AOSP_Comment_Out
+ if (PLUTO_TV_PACKAGE_NAME.equals(currentChannel.getPackageName())) {
+ // Do nothing for the Pluto TV input because it misuses this API. b/22720711.
+ return;
+ }
+ End_AOSP_Comment_Out */
if (isChannelChangeKeyDownReceived()) {
// Ignore this message if the user is changing the channel.
return;
@@ -2883,7 +2916,7 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
// before.
if (mWasUnderShrunkenTvView
&& mUnlockAllowedRatingBeforeShrunken
- && mChannelBeforeShrunkenTvView.equals(mChannel)
+ && Objects.equals(mChannelBeforeShrunkenTvView, mChannel)
&& rating.equals(mAllowedRatingBeforeShrunken)) {
mUnlockAllowedRatingBeforeShrunken = isUnderShrunkenTvView();
mTvView.unblockContent(rating);
@@ -2901,5 +2934,53 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
mOverlayManager.setBlockingContentRating(null);
mMediaSessionWrapper.update(false, getCurrentChannel(), getCurrentProgram());
}
+
+ @Override
+ public void onChannelSignalStrength() {
+ if (CommonFeatures.TUNER_SIGNAL_STRENGTH.isEnabled(getApplicationContext())) {
+ mOverlayManager.updateChannelBannerAndShowIfNeeded(
+ TvOverlayManager.UPDATE_CHANNEL_BANNER_REASON_UPDATE_SIGNAL_STRENGTH);
+ }
+ }
+ }
+
+ private class MySingletonsImpl implements MySingletons {
+
+ @Override
+ public Provider<Channel> getCurrentChannelProvider() {
+ return MainActivity.this::getCurrentChannel;
+ }
+
+ @Override
+ public Provider<Program> getCurrentProgramProvider() {
+ return MainActivity.this::getCurrentProgram;
+ }
+
+ @Override
+ public Provider<TvOverlayManager> getOverlayManagerProvider() {
+ return MainActivity.this::getOverlayManager;
+ }
+
+ @Override
+ public TvInputManagerHelper getTvInputManagerHelperSingleton() {
+ return getTvInputManagerHelper();
+ }
+
+ @Override
+ public Provider<Long> getCurrentPlayingPositionProvider() {
+ return MainActivity.this::getCurrentPlayingPosition;
+ }
+
+ @Override
+ public DvrManager getDvrManagerSingleton() {
+ return TvSingletons.getSingletons(getApplicationContext()).getDvrManager();
+ }
+ }
+
+ /** Exports {@link MainActivity} for Dagger codegen to create the appropriate injector. */
+ @dagger.Module
+ public abstract static class Module {
+ @ContributesAndroidInjector
+ abstract MainActivity contributesMainActivityActivityInjector();
}
}
diff --git a/src/com/android/tv/MediaSessionWrapper.java b/src/com/android/tv/MediaSessionWrapper.java
index 43cd74dd..a647a06f 100644
--- a/src/com/android/tv/MediaSessionWrapper.java
+++ b/src/com/android/tv/MediaSessionWrapper.java
@@ -16,12 +16,14 @@
package com.android.tv;
+import android.app.Activity;
import android.app.PendingIntent;
import android.content.Context;
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;
@@ -31,6 +33,7 @@ import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.text.TextUtils;
+import android.util.Log;
import com.android.tv.data.Program;
import com.android.tv.data.api.Channel;
import com.android.tv.util.Utils;
@@ -41,9 +44,12 @@ import com.android.tv.util.images.ImageLoader;
* {@link MainActivity}.
*/
class MediaSessionWrapper {
+ private static final String TAG = "MediaSessionWrapper";
+ private static final boolean DEBUG = false;
private static final String MEDIA_SESSION_TAG = "com.android.tv.mediasession";
- private static final PlaybackState MEDIA_SESSION_STATE_PLAYING =
+ @VisibleForTesting
+ static final PlaybackState MEDIA_SESSION_STATE_PLAYING =
new PlaybackState.Builder()
.setState(
PlaybackState.STATE_PLAYING,
@@ -51,7 +57,8 @@ class MediaSessionWrapper {
1.0f)
.build();
- private static final PlaybackState MEDIA_SESSION_STATE_STOPPED =
+ @VisibleForTesting
+ static final PlaybackState MEDIA_SESSION_STATE_STOPPED =
new PlaybackState.Builder()
.setState(
PlaybackState.STATE_STOPPED,
@@ -61,6 +68,20 @@ class MediaSessionWrapper {
private final Context mContext;
private final MediaSession mMediaSession;
+ private final MediaController.Callback mMediaControllerCallback =
+ new MediaController.Callback() {
+ @Override
+ public void onPlaybackStateChanged(@Nullable PlaybackState state) {
+ super.onPlaybackStateChanged(state);
+ if (DEBUG) {
+ Log.d(TAG, "onPlaybackStateChanged: " + state);
+ }
+ if (isMediaSessionStateStop(state)) {
+ mMediaSession.setActive(false);
+ }
+ }
+ };
+ private MediaController mMediaController;
private int mNowPlayingCardWidth;
private int mNowPlayingCardHeight;
@@ -79,6 +100,8 @@ class MediaSessionWrapper {
MediaSession.FLAG_HANDLES_MEDIA_BUTTONS
| MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS);
mMediaSession.setSessionActivity(pendingIntent);
+
+ initMediaController();
mNowPlayingCardWidth =
mContext.getResources().getDimensionPixelSize(R.dimen.notif_card_img_max_width);
mNowPlayingCardHeight =
@@ -97,7 +120,6 @@ class MediaSessionWrapper {
mMediaSession.setPlaybackState(MEDIA_SESSION_STATE_PLAYING);
} else if (mMediaSession.isActive()) {
mMediaSession.setPlaybackState(MEDIA_SESSION_STATE_STOPPED);
- mMediaSession.setActive(false);
}
}
@@ -150,6 +172,7 @@ class MediaSessionWrapper {
* @see MediaSession#release()
*/
void release() {
+ unregisterMediaControllerCallback();
mMediaSession.release();
}
@@ -223,6 +246,30 @@ class MediaSessionWrapper {
return mMediaSession;
}
+ @VisibleForTesting
+ MediaController.Callback getMediaControllerCallback() {
+ return mMediaControllerCallback;
+ }
+
+ @VisibleForTesting
+ void initMediaController() {
+ mMediaController = new MediaController(mContext, mMediaSession.getSessionToken());
+ ((Activity) mContext).setMediaController(mMediaController);
+ mMediaController.registerCallback(mMediaControllerCallback);
+ }
+
+ @VisibleForTesting
+ void unregisterMediaControllerCallback() {
+ mMediaController.unregisterCallback(mMediaControllerCallback);
+ }
+
+ private static boolean isMediaSessionStateStop(PlaybackState state) {
+ return state != null
+ && state.getState() == MEDIA_SESSION_STATE_STOPPED.getState()
+ && state.getPosition() == MEDIA_SESSION_STATE_STOPPED.getPosition()
+ && state.getPlaybackSpeed() == MEDIA_SESSION_STATE_STOPPED.getPlaybackSpeed();
+ }
+
private static class ProgramPosterArtCallback
extends ImageLoader.ImageLoaderCallback<MediaSessionWrapper> {
private final Channel mChannel;
diff --git a/src/com/android/tv/SetupPassthroughActivity.java b/src/com/android/tv/SetupPassthroughActivity.java
index 199ea51d..5185b122 100644
--- a/src/com/android/tv/SetupPassthroughActivity.java
+++ b/src/com/android/tv/SetupPassthroughActivity.java
@@ -28,11 +28,11 @@ import android.support.annotation.MainThread;
import android.util.Log;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.common.actions.InputSetupActionUtils;
-import com.android.tv.common.experiments.Experiments;
import com.android.tv.data.ChannelDataManager;
import com.android.tv.data.ChannelDataManager.Listener;
import com.android.tv.data.epg.EpgFetcher;
import com.android.tv.data.epg.EpgInputWhiteList;
+import com.android.tv.features.TvFeatures;
import com.android.tv.util.SetupUtils;
import com.android.tv.util.TvInputManagerHelper;
import com.android.tv.util.Utils;
@@ -66,12 +66,10 @@ public class SetupPassthroughActivity extends Activity {
Intent intent = getIntent();
String inputId = intent.getStringExtra(InputSetupActionUtils.EXTRA_INPUT_ID);
mTvInputInfo = inputManager.getTvInputInfo(inputId);
- mEpgInputWhiteList = new EpgInputWhiteList(tvSingletons.getRemoteConfig());
+ mEpgInputWhiteList = new EpgInputWhiteList(tvSingletons.getCloudEpgFlags());
mActivityAfterCompletion = InputSetupActionUtils.getExtraActivityAfter(intent);
boolean needToFetchEpg =
- mTvInputInfo != null
- && Utils.isInternalTvInput(this, mTvInputInfo.getId())
- && Experiments.CLOUD_EPG.get();
+ mTvInputInfo != null && Utils.isInternalTvInput(this, mTvInputInfo.getId());
if (needToFetchEpg) {
// In case when the activity is restored, this flag should be restored as well.
mEpgFetcherDuringScan = true;
@@ -144,23 +142,30 @@ public class SetupPassthroughActivity extends Activity {
finish();
return;
}
+ if (mTvInputInfo == null) {
+ Log.w(
+ TAG,
+ "There is no input with ID "
+ + getIntent().getStringExtra(InputSetupActionUtils.EXTRA_INPUT_ID)
+ + ".");
+ setResult(resultCode, data);
+ finish();
+ return;
+ }
TvSingletons.getSingletons(this)
.getSetupUtils()
.onTvInputSetupFinished(
mTvInputInfo.getId(),
- new Runnable() {
- @Override
- public void run() {
- if (mActivityAfterCompletion != null) {
- try {
- startActivity(mActivityAfterCompletion);
- } catch (ActivityNotFoundException e) {
- Log.w(TAG, "Activity launch failed", e);
- }
+ () -> {
+ if (mActivityAfterCompletion != null) {
+ try {
+ startActivity(mActivityAfterCompletion);
+ } catch (ActivityNotFoundException e) {
+ Log.w(TAG, "Activity launch failed", e);
}
- setResult(resultCode, data);
- finish();
}
+ setResult(resultCode, data);
+ finish();
});
}
@@ -178,15 +183,12 @@ public class SetupPassthroughActivity extends Activity {
private final ChannelDataManager mChannelDataManager;
private final Handler mHandler = new Handler(Looper.getMainLooper());
private final Runnable mScanTimeoutRunnable =
- new Runnable() {
- @Override
- public void run() {
- Log.w(
- TAG,
- "No channels has been added for a while."
- + " The scan might have finished unexpectedly.");
- onScanTimedOut();
- }
+ () -> {
+ Log.w(
+ TAG,
+ "No channels has been added for a while."
+ + " The scan might have finished unexpectedly.");
+ onScanTimedOut();
};
private final Listener mChannelDataManagerListener =
new Listener() {
diff --git a/src/com/android/tv/TimeShiftManager.java b/src/com/android/tv/TimeShiftManager.java
index bb3574d7..779e8df6 100644
--- a/src/com/android/tv/TimeShiftManager.java
+++ b/src/com/android/tv/TimeShiftManager.java
@@ -17,7 +17,6 @@
package com.android.tv;
import android.annotation.SuppressLint;
-import android.content.ContentResolver;
import android.content.Context;
import android.os.Handler;
import android.os.Message;
@@ -35,7 +34,7 @@ import com.android.tv.data.Program;
import com.android.tv.data.ProgramDataManager;
import com.android.tv.data.api.Channel;
import com.android.tv.ui.TunableTvView;
-import com.android.tv.ui.TunableTvViewPlayingApi.TimeShiftListener;
+import com.android.tv.ui.api.TunableTvViewPlayingApi.TimeShiftListener;
import com.android.tv.util.AsyncDbTask;
import com.android.tv.util.TimeShiftUtils;
import com.android.tv.util.Utils;
@@ -87,16 +86,15 @@ public class TimeShiftManager {
@Retention(RetentionPolicy.SOURCE)
@IntDef(
- flag = true,
- value = {
- TIME_SHIFT_ACTION_ID_PLAY,
- TIME_SHIFT_ACTION_ID_PAUSE,
- TIME_SHIFT_ACTION_ID_REWIND,
- TIME_SHIFT_ACTION_ID_FAST_FORWARD,
- TIME_SHIFT_ACTION_ID_JUMP_TO_PREVIOUS,
- TIME_SHIFT_ACTION_ID_JUMP_TO_NEXT
- }
- )
+ flag = true,
+ value = {
+ TIME_SHIFT_ACTION_ID_PLAY,
+ TIME_SHIFT_ACTION_ID_PAUSE,
+ TIME_SHIFT_ACTION_ID_REWIND,
+ TIME_SHIFT_ACTION_ID_FAST_FORWARD,
+ TIME_SHIFT_ACTION_ID_JUMP_TO_PREVIOUS,
+ TIME_SHIFT_ACTION_ID_JUMP_TO_NEXT
+ })
public @interface TimeShiftActionId {}
public static final int TIME_SHIFT_ACTION_ID_PLAY = 1;
@@ -715,7 +713,7 @@ public class TimeShiftManager {
: mRecordEndTimeMs;
long currentPositionMs =
Math.max(
- Math.min(mTvView.timeshiftGetCurrentPositionMs(), currentTimeMs),
+ Math.min(mTvView.timeShiftGetCurrentPositionMs(), currentTimeMs),
mRecordStartTimeMs);
boolean isCurrentTime =
currentTimeMs - currentPositionMs < RECORDING_BOUNDARY_THRESHOLD;
@@ -723,7 +721,7 @@ public class TimeShiftManager {
if (isCurrentTime && isForwarding()) {
// It's playing forward and the current playing position reached
// the current system time. i.e. The live stream is played.
- // Therefore no need to call TvView.timeshiftGetCurrentPositionMs
+ // Therefore no need to call TvView.timeShiftGetCurrentPositionMs
// any more.
newCurrentPositionMs = currentTimeMs;
mIsPlayOffsetChanged = false;
@@ -753,14 +751,14 @@ public class TimeShiftManager {
mDisplayedPlaySpeed = PLAY_SPEED_1X;
mPlaybackSpeed = 1;
mPlayDirection = PLAY_DIRECTION_FORWARD;
- mTvView.timeshiftPlay();
+ mTvView.timeShiftPlay();
setPlayStatus(PLAY_STATUS_PLAYING);
}
void pause() {
mDisplayedPlaySpeed = PLAY_SPEED_1X;
mPlaybackSpeed = 1;
- mTvView.timeshiftPause();
+ mTvView.timeShiftPause();
setPlayStatus(PLAY_STATUS_PAUSED);
mIsPlayOffsetChanged = true;
}
@@ -783,7 +781,7 @@ public class TimeShiftManager {
}
mPlayDirection = PLAY_DIRECTION_BACKWARD;
mPlaybackSpeed = getPlaybackSpeed();
- mTvView.timeshiftRewind(mPlaybackSpeed);
+ mTvView.timeShiftRewind(mPlaybackSpeed);
setPlayStatus(PLAY_STATUS_PLAYING);
mIsPlayOffsetChanged = true;
}
@@ -796,14 +794,14 @@ public class TimeShiftManager {
}
mPlayDirection = PLAY_DIRECTION_FORWARD;
mPlaybackSpeed = getPlaybackSpeed();
- mTvView.timeshiftFastForward(mPlaybackSpeed);
+ mTvView.timeShiftFastForward(mPlaybackSpeed);
setPlayStatus(PLAY_STATUS_PLAYING);
mIsPlayOffsetChanged = true;
}
/** Moves to the specified time. */
void seekTo(long timeMs) {
- mTvView.timeshiftSeekTo(
+ mTvView.timeShiftSeekTo(
Math.min(
mRecordEndTimeMs == CURRENT_TIME
? System.currentTimeMillis()
@@ -821,9 +819,9 @@ public class TimeShiftManager {
if (playbackSpeed != mPlaybackSpeed) {
mPlaybackSpeed = playbackSpeed;
if (mPlayDirection == PLAY_DIRECTION_FORWARD) {
- mTvView.timeshiftFastForward(mPlaybackSpeed);
+ mTvView.timeShiftFastForward(mPlaybackSpeed);
} else {
- mTvView.timeshiftRewind(mPlaybackSpeed);
+ mTvView.timeShiftRewind(mPlaybackSpeed);
}
}
}
@@ -977,8 +975,7 @@ public class TimeShiftManager {
}
}
if (mChannel != null) {
- mProgramLoadTask =
- new LoadProgramsForCurrentChannelTask(mContext.getContentResolver(), next);
+ mProgramLoadTask = new LoadProgramsForCurrentChannelTask(next);
mProgramLoadTask.executeOnDbThread();
}
}
@@ -1225,10 +1222,10 @@ public class TimeShiftManager {
private class LoadProgramsForCurrentChannelTask
extends AsyncDbTask.LoadProgramsForChannelTask {
- LoadProgramsForCurrentChannelTask(ContentResolver contentResolver, Range<Long> period) {
+ LoadProgramsForCurrentChannelTask(Range<Long> period) {
super(
TvSingletons.getSingletons(mContext).getDbExecutor(),
- contentResolver,
+ mContext,
mChannel.getId(),
period);
}
@@ -1309,13 +1306,7 @@ public class TimeShiftManager {
mProgramLoadTask = null;
}
// Need to post to handler, because the task is still running.
- mHandler.post(
- new Runnable() {
- @Override
- public void run() {
- startTaskIfNeeded();
- }
- });
+ mHandler.post(ProgramManager.this::startTaskIfNeeded);
}
boolean overlaps(Queue<Range<Long>> programLoadQueue) {
diff --git a/src/com/android/tv/TvApplication.java b/src/com/android/tv/TvApplication.java
index 826317b9..5f25a24b 100644
--- a/src/com/android/tv/TvApplication.java
+++ b/src/com/android/tv/TvApplication.java
@@ -34,8 +34,8 @@ import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.Log;
import android.view.KeyEvent;
+import android.widget.Toast;
import com.android.tv.common.BaseApplication;
-import com.android.tv.common.concurrent.NamedThreadFactory;
import com.android.tv.common.feature.CommonFeatures;
import com.android.tv.common.recording.RecordingStorageStatusManager;
import com.android.tv.common.ui.setup.animation.SetupAnimationHelper;
@@ -55,17 +55,22 @@ import com.android.tv.dvr.DvrStorageStatusManager;
import com.android.tv.dvr.DvrWatchedPositionManager;
import com.android.tv.dvr.recorder.RecordingScheduler;
import com.android.tv.dvr.ui.browse.DvrBrowseActivity;
+import com.android.tv.features.TvFeatures;
+import com.android.tv.perf.PerformanceMonitorManager;
+import com.android.tv.perf.PerformanceMonitorManagerFactory;
import com.android.tv.recommendation.ChannelPreviewUpdater;
import com.android.tv.recommendation.RecordedProgramPreviewUpdater;
-import com.android.tv.tuner.TunerInputController;
-import com.android.tv.tuner.util.TunerInputInfoUtils;
+import com.android.tv.tunerinputcontroller.BuiltInTunerManager;
+import com.android.tv.tunerinputcontroller.TunerInputController;
+import com.android.tv.util.AsyncDbTask.DbExecutor;
import com.android.tv.util.SetupUtils;
import com.android.tv.util.TvInputManagerHelper;
import com.android.tv.util.Utils;
+import com.google.common.base.Optional;
+import dagger.Lazy;
import java.util.List;
import java.util.concurrent.Executor;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
+import javax.inject.Inject;
/**
* Live TV application.
@@ -73,6 +78,9 @@ import java.util.concurrent.Executors;
* <p>This includes all the Google specific hooks.
*/
public abstract class TvApplication extends BaseApplication implements TvSingletons, Starter {
+
+ protected static final PerformanceMonitorManager PERFORMANCE_MONITOR_MANAGER =
+ PerformanceMonitorManagerFactory.create();
private static final String TAG = "TvApplication";
private static final boolean DEBUG = false;
@@ -89,10 +97,6 @@ public abstract class TvApplication extends BaseApplication implements TvSinglet
private static final String PREFERENCE_IS_FIRST_LAUNCH = "is_first_launch";
- private static final NamedThreadFactory THREAD_FACTORY = new NamedThreadFactory("tv-app-db");
- private static final ExecutorService DB_EXECUTOR =
- Executors.newSingleThreadExecutor(THREAD_FACTORY);
-
private String mVersionName = "";
private final MainActivityWrapper mMainActivityWrapper = new MainActivityWrapper();
@@ -111,22 +115,28 @@ public abstract class TvApplication extends BaseApplication implements TvSinglet
// STOP-SHIP: Remove this variable when Tuner Process is split to another application.
// When this variable is null, we don't know in which process TvApplication runs.
private Boolean mRunningInMainProcess;
- private TvInputManagerHelper mTvInputManagerHelper;
+ @Inject Lazy<TvInputManagerHelper> mLazyTvInputManagerHelper;
private boolean mStarted;
private EpgFetcher mEpgFetcher;
- private TunerInputController mTunerInputController;
+
+ @Inject Optional<BuiltInTunerManager> mOptionalBuiltInTunerManager;
+ @Inject SetupUtils mSetupUtils;
+ @Inject @DbExecutor Executor mDbExecutor;
@Override
public void onCreate() {
+ if (getSystemService(TvInputManager.class) == null) {
+ String msg = "Not an Android TV device.";
+ Toast.makeText(this, msg, Toast.LENGTH_LONG);
+ Log.wtf(TAG, msg);
+ throw new IllegalStateException(msg);
+ }
super.onCreate();
SharedPreferencesUtils.initialize(
this,
- new Runnable() {
- @Override
- public void run() {
- if (mRunningInMainProcess != null && mRunningInMainProcess) {
- checkTunerServiceOnFirstLaunch();
- }
+ () -> {
+ if (mRunningInMainProcess != null && mRunningInMainProcess) {
+ checkTunerServiceOnFirstLaunch();
}
});
try {
@@ -164,13 +174,19 @@ public abstract class TvApplication extends BaseApplication implements TvSinglet
new TvInputCallback() {
@Override
public void onInputAdded(String inputId) {
- if (TvFeatures.TUNER.isEnabled(TvApplication.this)
- && TextUtils.equals(
- inputId, getEmbeddedTunerInputId())) {
- TunerInputInfoUtils.updateTunerInputInfo(
- TvApplication.this);
+ if (mOptionalBuiltInTunerManager.isPresent()) {
+ BuiltInTunerManager builtInTunerManager =
+ mOptionalBuiltInTunerManager.get();
+ if (TextUtils.equals(
+ inputId,
+ builtInTunerManager.getEmbeddedTunerInputId())) {
+
+ builtInTunerManager
+ .getTunerInputController()
+ .updateTunerInputInfo(TvApplication.this);
+ }
+ handleInputCountChanged();
}
- handleInputCountChanged();
}
@Override
@@ -178,10 +194,13 @@ public abstract class TvApplication extends BaseApplication implements TvSinglet
handleInputCountChanged();
}
});
- if (TvFeatures.TUNER.isEnabled(this)) {
+ if (mOptionalBuiltInTunerManager.isPresent()) {
// If the tuner input service is added before the app is started, we need to
// handle it here.
- TunerInputInfoUtils.updateTunerInputInfo(TvApplication.this);
+ mOptionalBuiltInTunerManager
+ .get()
+ .getTunerInputController()
+ .updateTunerInputInfo(TvApplication.this);
}
if (CommonFeatures.DVR.isEnabled(this)) {
mDvrScheduleManager = new DvrScheduleManager(this);
@@ -205,8 +224,12 @@ public abstract class TvApplication extends BaseApplication implements TvSinglet
boolean isFirstLaunch = sharedPreferences.getBoolean(PREFERENCE_IS_FIRST_LAUNCH, true);
if (isFirstLaunch) {
if (DEBUG) Log.d(TAG, "Congratulations, it's the first launch!");
- getTunerInputController()
- .onCheckingUsbTunerStatus(this, ACTION_APPLICATION_FIRST_LAUNCHED);
+ if (mOptionalBuiltInTunerManager.isPresent()) {
+ mOptionalBuiltInTunerManager
+ .get()
+ .getTunerInputController()
+ .onCheckingUsbTunerStatus(this, ACTION_APPLICATION_FIRST_LAUNCHED);
+ }
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putBoolean(PREFERENCE_IS_FIRST_LAUNCH, false);
editor.apply();
@@ -220,7 +243,7 @@ public abstract class TvApplication extends BaseApplication implements TvSinglet
@Override
public synchronized SetupUtils getSetupUtils() {
- return SetupUtils.createForTvSingletons(this);
+ return mSetupUtils;
}
/** Returns the {@link DvrManager}. */
@@ -282,13 +305,10 @@ public abstract class TvApplication extends BaseApplication implements TvSinglet
return mProgramDataManager;
}
Utils.runInMainThreadAndWait(
- new Runnable() {
- @Override
- public void run() {
- if (mProgramDataManager == null) {
- mProgramDataManager = new ProgramDataManager(TvApplication.this);
- mProgramDataManager.start();
- }
+ () -> {
+ if (mProgramDataManager == null) {
+ mProgramDataManager = new ProgramDataManager(TvApplication.this);
+ mProgramDataManager.start();
}
});
return mProgramDataManager;
@@ -340,21 +360,7 @@ public abstract class TvApplication extends BaseApplication implements TvSinglet
/** Returns {@link TvInputManagerHelper}. */
@Override
public TvInputManagerHelper getTvInputManagerHelper() {
- if (mTvInputManagerHelper == null) {
- mTvInputManagerHelper = new TvInputManagerHelper(this);
- mTvInputManagerHelper.start();
- }
- return mTvInputManagerHelper;
- }
-
- @Override
- public synchronized TunerInputController getTunerInputController() {
- if (mTunerInputController == null) {
- mTunerInputController =
- new TunerInputController(
- ComponentName.unflattenFromString(getEmbeddedTunerInputId()));
- }
- return mTunerInputController;
+ return mLazyTvInputManagerHelper.get();
}
@Override
@@ -480,12 +486,16 @@ public abstract class TvApplication extends BaseApplication implements TvSinglet
if (!enable) {
List<TvInputInfo> inputs = inputManager.getTvInputList();
boolean skipTunerInputCheck = false;
+ Optional<String> optionalEmbeddedTunerInputId =
+ mOptionalBuiltInTunerManager.transform(
+ BuiltInTunerManager::getEmbeddedTunerInputId);
// Enable the TvActivity only if there is at least one tuner type input.
if (!skipTunerInputCheck) {
for (TvInputInfo input : inputs) {
if (calledByTunerServiceChanged
&& !tunerServiceEnabled
- && getEmbeddedTunerInputId().equals(input.getId())) {
+ && optionalEmbeddedTunerInputId.isPresent()
+ && optionalEmbeddedTunerInputId.get().equals(input.getId())) {
continue;
}
if (input.getType() == TvInputInfo.TYPE_TUNER) {
@@ -507,11 +517,11 @@ public abstract class TvApplication extends BaseApplication implements TvSinglet
name, newState, dontKillApp ? PackageManager.DONT_KILL_APP : 0);
Log.i(TAG, (enable ? "Un-hide" : "Hide") + " Live TV.");
}
- getSetupUtils().onInputListUpdated(inputManager);
+ mSetupUtils.onInputListUpdated(inputManager);
}
@Override
public Executor getDbExecutor() {
- return DB_EXECUTOR;
+ return mDbExecutor;
}
}
diff --git a/src/com/android/tv/TvSingletons.java b/src/com/android/tv/TvSingletons.java
index 0c7f78a3..20edf3d4 100644
--- a/src/com/android/tv/TvSingletons.java
+++ b/src/com/android/tv/TvSingletons.java
@@ -22,6 +22,7 @@ import com.android.tv.analytics.Tracker;
import com.android.tv.common.BaseApplication;
import com.android.tv.common.BaseSingletons;
import com.android.tv.common.experiments.ExperimentLoader;
+import com.android.tv.common.flags.has.HasUiFlags;
import com.android.tv.data.ChannelDataManager;
import com.android.tv.data.PreviewDataManager;
import com.android.tv.data.ProgramDataManager;
@@ -33,17 +34,23 @@ import com.android.tv.dvr.DvrScheduleManager;
import com.android.tv.dvr.DvrWatchedPositionManager;
import com.android.tv.dvr.recorder.RecordingScheduler;
import com.android.tv.perf.PerformanceMonitor;
-import com.android.tv.tuner.TunerInputController;
+import com.android.tv.tunerinputcontroller.HasBuiltInTunerManager;
import com.android.tv.util.SetupUtils;
import com.android.tv.util.TvInputManagerHelper;
import com.android.tv.util.account.AccountHelper;
+import com.android.tv.common.flags.BackendKnobsFlags;
import java.util.concurrent.Executor;
import javax.inject.Provider;
/** Interface with getters for application scoped singletons. */
-public interface TvSingletons extends BaseSingletons {
+public interface TvSingletons extends BaseSingletons, HasBuiltInTunerManager, HasUiFlags {
- /** Returns the @{@link TvSingletons} using the application context. */
+ /**
+ * Returns the @{@link TvSingletons} using the application context.
+ *
+ * @deprecated use injection instead.
+ */
+ @Deprecated
static TvSingletons getSingletons(Context context) {
return (TvSingletons) BaseApplication.getSingletons(context);
}
@@ -52,6 +59,7 @@ public interface TvSingletons extends BaseSingletons {
void handleInputCountChanged();
+ @Deprecated
ChannelDataManager getChannelDataManager();
/**
@@ -60,6 +68,8 @@ public interface TvSingletons extends BaseSingletons {
*/
boolean isChannelDataManagerLoadFinished();
+ /** @deprecated use injection instead. */
+ @Deprecated
ProgramDataManager getProgramDataManager();
/**
@@ -92,17 +102,23 @@ public interface TvSingletons extends BaseSingletons {
PerformanceMonitor getPerformanceMonitor();
+ /** @deprecated use injection instead. */
+ @Deprecated
TvInputManagerHelper getTvInputManagerHelper();
Provider<EpgReader> providesEpgReader();
EpgFetcher getEpgFetcher();
+ /** @deprecated use injection instead. */
+ @Deprecated
SetupUtils getSetupUtils();
- TunerInputController getTunerInputController();
-
ExperimentLoader getExperimentLoader();
+ /** @deprecated use injection instead. */
+ @Deprecated
Executor getDbExecutor();
+
+ BackendKnobsFlags getBackendKnobs();
}
diff --git a/src/com/android/tv/analytics/SendChannelStatusRunnable.java b/src/com/android/tv/analytics/SendChannelStatusRunnable.java
index 4a84434c..306bd855 100644
--- a/src/com/android/tv/analytics/SendChannelStatusRunnable.java
+++ b/src/com/android/tv/analytics/SendChannelStatusRunnable.java
@@ -43,13 +43,7 @@ public class SendChannelStatusRunnable implements Runnable {
final SendChannelStatusRunnable sendChannelStatusRunnable =
new SendChannelStatusRunnable(channelDataManager, tracker);
- Runnable onStopRunnable =
- new Runnable() {
- @Override
- public void run() {
- sendChannelStatusRunnable.setDbLoadListener(null);
- }
- };
+ Runnable onStopRunnable = () -> sendChannelStatusRunnable.setDbLoadListener(null);
final RecurringRunner recurringRunner =
new RecurringRunner(
context,
@@ -70,14 +64,7 @@ public class SendChannelStatusRunnable implements Runnable {
// done
// via a post on the main thread
new Handler(Looper.getMainLooper())
- .post(
- new Runnable() {
- @Override
- public void run() {
- sendChannelStatusRunnable.setDbLoadListener(
- null);
- }
- });
+ .post(() -> sendChannelStatusRunnable.setDbLoadListener(null));
recurringRunner.start();
}
diff --git a/src/com/android/tv/app/LiveTvApplication.java b/src/com/android/tv/app/LiveTvApplication.java
index 461331d5..38e85e48 100644
--- a/src/com/android/tv/app/LiveTvApplication.java
+++ b/src/com/android/tv/app/LiveTvApplication.java
@@ -16,36 +16,37 @@
package com.android.tv.app;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.media.tv.TvContract;
import com.android.tv.TvApplication;
+import com.android.tv.TvSingletons;
import com.android.tv.analytics.Analytics;
import com.android.tv.analytics.StubAnalytics;
import com.android.tv.analytics.Tracker;
-import com.android.tv.common.CommonConstants;
-import com.android.tv.common.actions.InputSetupActionUtils;
-import com.android.tv.common.config.DefaultConfigManager;
-import com.android.tv.common.config.api.RemoteConfig;
+import com.android.tv.common.dagger.ApplicationModule;
import com.android.tv.common.experiments.ExperimentLoader;
-import com.android.tv.common.util.CommonUtils;
+import com.android.tv.common.flags.impl.DefaultBackendKnobsFlags;
+import com.android.tv.common.flags.impl.DefaultCloudEpgFlags;
+import com.android.tv.common.flags.impl.DefaultConcurrentDvrPlaybackFlags;
+import com.android.tv.common.flags.impl.DefaultUiFlags;
+import com.android.tv.common.singletons.HasSingletons;
import com.android.tv.data.epg.EpgReader;
import com.android.tv.data.epg.StubEpgReader;
+import com.android.tv.modules.TvSingletonsModule;
import com.android.tv.perf.PerformanceMonitor;
-import com.android.tv.perf.StubPerformanceMonitor;
-import com.android.tv.tuner.livetuner.LiveTvTunerTvInputService;
-import com.android.tv.tuner.setup.LiveTvTunerSetupActivity;
+import com.android.tv.perf.PerformanceMonitorManagerFactory;
+import com.android.tv.tunerinputcontroller.BuiltInTunerManager;
import com.android.tv.util.account.AccountHelper;
import com.android.tv.util.account.AccountHelperImpl;
+import com.google.common.base.Optional;
+import dagger.android.AndroidInjector;
import javax.inject.Provider;
/** The top level application for Live TV. */
-public class LiveTvApplication extends TvApplication {
- protected static final String TV_ACTIVITY_CLASS_NAME =
- CommonConstants.BASE_PACKAGE + ".TvActivity";
+public class LiveTvApplication extends TvApplication implements HasSingletons<TvSingletons> {
+
+ static {
+ PERFORMANCE_MONITOR_MANAGER.getStartupMeasure().onAppClassLoaded();
+ }
- private final StubPerformanceMonitor performanceMonitor = new StubPerformanceMonitor();
private final Provider<EpgReader> mEpgReaderProvider =
new Provider<EpgReader>() {
@@ -55,12 +56,30 @@ public class LiveTvApplication extends TvApplication {
}
};
+ private final DefaultBackendKnobsFlags mBackendKnobsFlags = new DefaultBackendKnobsFlags();
+ private final DefaultCloudEpgFlags mCloudEpgFlags = new DefaultCloudEpgFlags();
+ private final DefaultUiFlags mUiFlags = new DefaultUiFlags();
+ private final DefaultConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags =
+ new DefaultConcurrentDvrPlaybackFlags();
private AccountHelper mAccountHelper;
private Analytics mAnalytics;
private Tracker mTracker;
- private String mEmbeddedInputId;
- private RemoteConfig mRemoteConfig;
private ExperimentLoader mExperimentLoader;
+ private PerformanceMonitor mPerformanceMonitor;
+
+ @Override
+ protected AndroidInjector<LiveTvApplication> applicationInjector() {
+ return DaggerLiveTvApplicationComponent.builder()
+ .applicationModule(new ApplicationModule(this))
+ .tvSingletonsModule(new TvSingletonsModule(this))
+ .build();
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ PERFORMANCE_MONITOR_MANAGER.getStartupMeasure().onAppCreate(this);
+ }
/** Returns the {@link AccountHelperImpl}. */
@Override
@@ -73,7 +92,10 @@ public class LiveTvApplication extends TvApplication {
@Override
public synchronized PerformanceMonitor getPerformanceMonitor() {
- return performanceMonitor;
+ if (mPerformanceMonitor == null) {
+ mPerformanceMonitor = PerformanceMonitorManagerFactory.create().initialize(this);
+ }
+ return mPerformanceMonitor;
}
@Override
@@ -87,6 +109,11 @@ public class LiveTvApplication extends TvApplication {
return mExperimentLoader;
}
+ @Override
+ public DefaultBackendKnobsFlags getBackendKnobs() {
+ return mBackendKnobsFlags;
+ }
+
/** Returns the {@link Analytics}. */
@Override
public synchronized Analytics getAnalytics() {
@@ -106,34 +133,32 @@ public class LiveTvApplication extends TvApplication {
}
@Override
- public Intent getTunerSetupIntent(Context context) {
- // Make an intent to launch the setup activity of TV tuner input.
- Intent intent =
- CommonUtils.createSetupIntent(
- new Intent(context, LiveTvTunerSetupActivity.class), mEmbeddedInputId);
- intent.putExtra(InputSetupActionUtils.EXTRA_INPUT_ID, mEmbeddedInputId);
- Intent tvActivityIntent = new Intent();
- tvActivityIntent.setComponent(new ComponentName(context, TV_ACTIVITY_CLASS_NAME));
- intent.putExtra(InputSetupActionUtils.EXTRA_ACTIVITY_AFTER_COMPLETION, tvActivityIntent);
- return intent;
+ public DefaultCloudEpgFlags getCloudEpgFlags() {
+ return mCloudEpgFlags;
}
@Override
- public synchronized String getEmbeddedTunerInputId() {
- if (mEmbeddedInputId == null) {
- mEmbeddedInputId =
- TvContract.buildInputId(
- new ComponentName(this, LiveTvTunerTvInputService.class));
- }
- return mEmbeddedInputId;
+ public DefaultUiFlags getUiFlags() {
+ return mUiFlags;
}
@Override
- public RemoteConfig getRemoteConfig() {
- if (mRemoteConfig == null) {
- // No need to synchronize this, it does not hurt to create two and throw one away.
- mRemoteConfig = DefaultConfigManager.createInstance(this).getRemoteConfig();
- }
- return mRemoteConfig;
+ public Optional<BuiltInTunerManager> getBuiltInTunerManager() {
+ return Optional.absent();
+ }
+
+ @Override
+ public BuildType getBuildType() {
+ return BuildType.AOSP;
+ }
+
+ @Override
+ public DefaultConcurrentDvrPlaybackFlags getConcurrentDvrPlaybackFlags() {
+ return mConcurrentDvrPlaybackFlags;
+ }
+
+ @Override
+ public TvSingletons singletons() {
+ return this;
}
}
diff --git a/src/com/android/tv/app/LiveTvApplicationComponent.java b/src/com/android/tv/app/LiveTvApplicationComponent.java
new file mode 100644
index 00000000..3d3f0492
--- /dev/null
+++ b/src/com/android/tv/app/LiveTvApplicationComponent.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.app;
+
+import dagger.Component;
+import dagger.android.AndroidInjectionModule;
+import dagger.android.AndroidInjector;
+import javax.inject.Singleton;
+
+/** Dagger component for {@link LiveTvApplication}. */
+@Singleton
+@Component(modules = {AndroidInjectionModule.class, LiveTvModule.class})
+public interface LiveTvApplicationComponent extends AndroidInjector<LiveTvApplication> {}
diff --git a/src/com/android/tv/app/LiveTvModule.java b/src/com/android/tv/app/LiveTvModule.java
new file mode 100644
index 00000000..a28749bd
--- /dev/null
+++ b/src/com/android/tv/app/LiveTvModule.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.app;
+
+import com.android.tv.common.flags.impl.DefaultFlagsModule;
+import com.android.tv.modules.TvApplicationModule;
+import com.android.tv.tunerinputcontroller.BuiltInTunerManager;
+import com.google.common.base.Optional;
+import dagger.Module;
+import dagger.Provides;
+
+/** Dagger module for {@link LiveTvApplication}. */
+@Module(includes = {DefaultFlagsModule.class, TvApplicationModule.class})
+class LiveTvModule {
+
+ @Provides
+ Optional<BuiltInTunerManager> providesBuiltInTunerManager() {
+ return Optional.absent();
+ }
+}
diff --git a/src/com/android/tv/AudioManagerHelper.java b/src/com/android/tv/audio/AudioManagerHelper.java
index 942d431d..4acff2d3 100644
--- a/src/com/android/tv/AudioManagerHelper.java
+++ b/src/com/android/tv/audio/AudioManagerHelper.java
@@ -13,18 +13,20 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.tv;
+package com.android.tv.audio;
import android.app.Activity;
import android.content.Context;
+import android.media.AudioAttributes;
+import android.media.AudioFocusRequest;
import android.media.AudioManager;
import android.os.Build;
-import com.android.tv.receiver.AudioCapabilitiesReceiver;
-import com.android.tv.ui.TunableTvView;
-import com.android.tv.ui.TunableTvViewPlayingApi;
+import android.support.annotation.Nullable;
+import com.android.tv.features.TvFeatures;
+import com.android.tv.ui.api.TunableTvViewPlayingApi;
-/** A helper class to help {@link MainActivity} to handle audio-related stuffs. */
-class AudioManagerHelper implements AudioManager.OnAudioFocusChangeListener {
+/** A helper class to help {@code Activities} to handle audio-related stuffs. */
+public class AudioManagerHelper implements AudioManager.OnAudioFocusChangeListener {
private static final float AUDIO_MAX_VOLUME = 1.0f;
private static final float AUDIO_MIN_VOLUME = 0.0f;
private static final float AUDIO_DUCKING_VOLUME = 0.3f;
@@ -32,42 +34,53 @@ class AudioManagerHelper implements AudioManager.OnAudioFocusChangeListener {
private final Activity mActivity;
private final TunableTvViewPlayingApi mTvView;
private final AudioManager mAudioManager;
- private final AudioCapabilitiesReceiver mAudioCapabilitiesReceiver;
+ @Nullable private final AudioFocusRequest mFocusRequest;
- private boolean mAc3PassthroughSupported;
- private int mAudioFocusStatus = AudioManager.AUDIOFOCUS_LOSS;
+ private int mAudioFocusStatus = AudioManager.AUDIOFOCUS_NONE;
- AudioManagerHelper(Activity activity, TunableTvViewPlayingApi tvView) {
+ public AudioManagerHelper(Activity activity, TunableTvViewPlayingApi tvView) {
mActivity = activity;
mTvView = tvView;
mAudioManager = (AudioManager) activity.getSystemService(Context.AUDIO_SERVICE);
- mAudioCapabilitiesReceiver =
- new AudioCapabilitiesReceiver(
- activity,
- new AudioCapabilitiesReceiver.OnAc3PassthroughCapabilityChangeListener() {
- @Override
- public void onAc3PassthroughCapabilityChange(boolean capability) {
- mAc3PassthroughSupported = capability;
- }
- });
- mAudioCapabilitiesReceiver.register();
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ mFocusRequest =
+ new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
+ .setAudioAttributes(
+ new AudioAttributes.Builder()
+ .setContentType(AudioAttributes.CONTENT_TYPE_MOVIE)
+ .setUsage(AudioAttributes.USAGE_MEDIA)
+ .build())
+ .setOnAudioFocusChangeListener(this)
+ // Auto ducking from the system does not mute the TV Input Service.
+ // Using will pause when ducked allows us to set the stream volume
+ // even when we are not pausing.
+ .setWillPauseWhenDucked(true)
+ .build();
+ } else {
+ mFocusRequest = null;
+ }
}
/**
- * Sets suitable volume to {@link TunableTvView} according to the current audio focus. If the
- * focus status is {@link AudioManager#AUDIOFOCUS_LOSS} and the activity is under PIP mode, this
- * method will finish the activity.
+ * Sets suitable volume to {@link TunableTvViewPlayingApi} according to the current audio focus.
+ *
+ * <p>If the focus status is {@link AudioManager#AUDIOFOCUS_LOSS} or {@link
+ * AudioManager#AUDIOFOCUS_NONE} and the activity is under PIP mode, this method will finish the
+ * activity. Sets suitable volume to {@link TunableTvViewPlayingApi} according to the current
+ * audio focus. If the focus status is {@link AudioManager#AUDIOFOCUS_LOSS} and the activity is
+ * under PIP mode, this method will finish the activity.
*/
- void setVolumeByAudioFocusStatus() {
+ public void setVolumeByAudioFocusStatus() {
if (mTvView.isPlaying()) {
switch (mAudioFocusStatus) {
case AudioManager.AUDIOFOCUS_GAIN:
if (mTvView.isTimeShiftAvailable()) {
- mTvView.timeshiftPlay();
+ mTvView.timeShiftPlay();
} else {
mTvView.setStreamVolume(AUDIO_MAX_VOLUME);
}
break;
+ case AudioManager.AUDIOFOCUS_NONE:
case AudioManager.AUDIOFOCUS_LOSS:
if (TvFeatures.PICTURE_IN_PICTURE.isEnabled(mActivity)
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
@@ -78,14 +91,14 @@ class AudioManagerHelper implements AudioManager.OnAudioFocusChangeListener {
// fall through
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
if (mTvView.isTimeShiftAvailable()) {
- mTvView.timeshiftPause();
+ mTvView.timeShiftPause();
} else {
mTvView.setStreamVolume(AUDIO_MIN_VOLUME);
}
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
if (mTvView.isTimeShiftAvailable()) {
- mTvView.timeshiftPause();
+ mTvView.timeShiftPause();
} else {
mTvView.setStreamVolume(AUDIO_DUCKING_VOLUME);
}
@@ -98,10 +111,15 @@ class AudioManagerHelper implements AudioManager.OnAudioFocusChangeListener {
* Tries to request audio focus from {@link AudioManager} and set volume according to the
* returned result.
*/
- void requestAudioFocus() {
- int result =
- mAudioManager.requestAudioFocus(
- this, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
+ public void requestAudioFocus() {
+ int result;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ result = mAudioManager.requestAudioFocus(mFocusRequest);
+ } else {
+ result =
+ mAudioManager.requestAudioFocus(
+ this, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
+ }
mAudioFocusStatus =
(result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED)
? AudioManager.AUDIOFOCUS_GAIN
@@ -110,19 +128,13 @@ class AudioManagerHelper implements AudioManager.OnAudioFocusChangeListener {
}
/** Abandons audio focus. */
- void abandonAudioFocus() {
+ public void abandonAudioFocus() {
mAudioFocusStatus = AudioManager.AUDIOFOCUS_LOSS;
- mAudioManager.abandonAudioFocus(this);
- }
-
- /** Returns {@code true} if the device supports AC3 pass-through. */
- boolean isAc3PassthroughSupported() {
- return mAc3PassthroughSupported;
- }
-
- /** Release the resources the helper class may occupied. */
- void release() {
- mAudioCapabilitiesReceiver.unregister();
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ mAudioManager.abandonAudioFocusRequest(mFocusRequest);
+ } else {
+ mAudioManager.abandonAudioFocus(this);
+ }
}
@Override
diff --git a/src/com/android/tv/audiotvservice/AudioOnlyTvService.java b/src/com/android/tv/audiotvservice/AudioOnlyTvService.java
new file mode 100644
index 00000000..5d0e9c82
--- /dev/null
+++ b/src/com/android/tv/audiotvservice/AudioOnlyTvService.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.audiotvservice;
+
+import android.app.Notification;
+import android.app.Service;
+import android.content.Intent;
+import android.media.session.MediaSession;
+import android.net.Uri;
+import android.os.IBinder;
+import android.support.annotation.Nullable;
+import android.util.Log;
+import com.android.tv.data.ChannelImpl;
+import com.android.tv.data.StreamInfo;
+import com.android.tv.data.api.Channel;
+import com.android.tv.ui.TunableTvView;
+import com.android.tv.ui.TunableTvView.OnTuneListener;
+
+/** Foreground service for audio-only TV inputs. */
+public class AudioOnlyTvService extends Service implements OnTuneListener {
+ // TODO(b/110969180): implement this service.
+ private static final String TAG = "AudioOnlyTvService";
+ private static final int NOTIFICATION_ID = 1;
+
+ @Nullable private String mTvInputId;
+ private TunableTvView mTvView;
+ // TODO(b/110969180): perhaps use MediaSessionWrapper
+ private MediaSession mMediaSession;
+
+ @Nullable
+ @Override
+ public IBinder onBind(Intent intent) {
+ Log.i(TAG, "onBind");
+ return null;
+ }
+
+ @Override
+ public void onCreate() {
+ Log.i(TAG, "onCreate");
+ // TODO(b/110969180): create TvView
+
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ Log.i(TAG, "onStartCommand. flags = " + flags + ", startId = " + startId);
+ // TODO(b/110969180): real notification and or media session
+ startForeground(NOTIFICATION_ID, new Notification());
+ mTvInputId = AudioOnlyTvServiceUtil.getInputIdFromIntent(intent);
+ tune(mTvInputId);
+ return START_STICKY;
+ }
+
+ private void tune(String tvInputId) {
+ Channel channel = ChannelImpl.createPassthroughChannel(tvInputId);
+ mTvView.tuneTo(channel, null, this);
+ }
+
+ @Override
+ public void onDestroy() {
+ Log.i(TAG, "onDestroy");
+ mTvInputId = null;
+ // TODO(b/110969180): clear TvView
+ }
+
+ // TODO(b/110969180): figure out when to stop ourselves, mediaSession event?
+
+ // TODO(b/110969180): handle OnTuner Listener
+ @Override
+ public void onTuneFailed(Channel channel) {}
+
+ @Override
+ public void onUnexpectedStop(Channel channel) {}
+
+ @Override
+ public void onStreamInfoChanged(StreamInfo info, boolean allowAutoSelectionOfTrack) {}
+
+ @Override
+ public void onChannelRetuned(Uri channel) {}
+
+ @Override
+ public void onContentBlocked() {}
+
+ @Override
+ public void onContentAllowed() {}
+
+ @Override
+ public void onChannelSignalStrength() {}
+}
diff --git a/src/com/android/tv/audiotvservice/AudioOnlyTvServiceUtil.java b/src/com/android/tv/audiotvservice/AudioOnlyTvServiceUtil.java
new file mode 100644
index 00000000..7ffe8833
--- /dev/null
+++ b/src/com/android/tv/audiotvservice/AudioOnlyTvServiceUtil.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.audiotvservice;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build;
+import android.support.annotation.MainThread;
+import android.support.annotation.Nullable;
+import android.util.Log;
+
+/** Utility methods to start and stop audio only TV Player. */
+public final class AudioOnlyTvServiceUtil {
+ private static final String TAG = "AudioOnlyTvServiceUtil";
+ private static final String EXTRA_INPUT_ID = "intputId";
+
+ @MainThread
+ public static void startAudioOnlyInput(Context context, String tvInputId) {
+ Log.i(TAG, "startAudioOnlyInput");
+ Intent intent = getIntent(context);
+ if (intent == null) {
+ return;
+ }
+ intent.putExtra(EXTRA_INPUT_ID, tvInputId);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ context.startForegroundService(intent);
+ } else {
+ context.startService(intent);
+ }
+ }
+
+ @Nullable
+ private static Intent getIntent(Context context) {
+ try {
+ return new Intent(
+ context, Class.forName("com.android.tv.audiotvservice.AudioOnlyTvService"));
+ } catch (ClassNotFoundException e) {
+ Log.wtf(TAG, e);
+ return null;
+ }
+ }
+
+ @MainThread
+ public static void stopAudioOnlyInput(Context context) {
+ Log.i(TAG, "stopForegroundService");
+ context.stopService(getIntent(context));
+ }
+
+ @Nullable
+ public static String getInputIdFromIntent(Intent intent) {
+ return intent.getStringExtra(EXTRA_INPUT_ID);
+ }
+
+ private AudioOnlyTvServiceUtil() {}
+}
diff --git a/src/com/android/tv/audiotvservice/README.md b/src/com/android/tv/audiotvservice/README.md
new file mode 100644
index 00000000..0f40ff6c
--- /dev/null
+++ b/src/com/android/tv/audiotvservice/README.md
@@ -0,0 +1,18 @@
+# AudioOnlyTvServiceUtil
+
+This service plays audio only TV inputs in the "background".
+
+
+
+## Usage
+
+To start playing call
+
+```java
+AudioOnlyTvServiceUtil.startAudioOnlyInput(context, tivInputServiceUri);
+```
+To stop the playback call.
+
+```java
+AudioOnlyTvServiceUtil.stopAudioOnlyInput(context);
+``` \ No newline at end of file
diff --git a/src/com/android/tv/data/BaseProgram.java b/src/com/android/tv/data/BaseProgram.java
index 0fb1e58d..9650fd18 100644
--- a/src/com/android/tv/data/BaseProgram.java
+++ b/src/com/android/tv/data/BaseProgram.java
@@ -21,7 +21,9 @@ import android.media.tv.TvContentRating;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import com.android.tv.R;
+import com.google.common.collect.ImmutableList;
import java.util.Comparator;
+import java.util.Objects;
/**
* Base class for {@link com.android.tv.data.Program} and {@link
@@ -43,6 +45,10 @@ public abstract class BaseProgram {
public static final Comparator<BaseProgram> SEASON_REVERSED_EPISODE_COMPARATOR =
new EpisodeComparator(true);
+ public static final String COLUMN_SERIES_ID = "series_id";
+
+ public static final String COLUMN_STATE = "state";
+
private static class EpisodeComparator implements Comparator<BaseProgram> {
private final boolean mReversedSeason;
@@ -66,7 +72,7 @@ public abstract class BaseProgram {
/** Compares two strings represent season numbers or episode numbers of programs. */
public static int numberCompare(String s1, String s2) {
- if (s1 == s2) {
+ if (Objects.equals(s1, s2)) {
return 0;
} else if (s1 == null) {
return -1;
@@ -92,6 +98,7 @@ public abstract class BaseProgram {
public abstract String getEpisodeTitle();
/** Returns the displayed title of the program episode. */
+ @Nullable
public String getEpisodeDisplayTitle(Context context) {
String episodeNumber = getEpisodeNumber();
String episodeTitle = getEpisodeTitle();
@@ -162,6 +169,7 @@ public abstract class BaseProgram {
public abstract long getDurationMillis();
/** Returns the series ID. */
+ @Nullable
public abstract String getSeriesId();
/** Returns the season number. */
@@ -180,8 +188,7 @@ public abstract class BaseProgram {
public abstract int[] getCanonicalGenreIds();
/** Returns the array of content ratings. */
- @Nullable
- public abstract TvContentRating[] getContentRatings();
+ public abstract ImmutableList<TvContentRating> getContentRatings();
/** Returns channel's ID of the program. */
public abstract long getChannelId();
diff --git a/src/com/android/tv/data/ChannelDataManager.java b/src/com/android/tv/data/ChannelDataManager.java
index 1dfcf125..a5c786cf 100644
--- a/src/com/android/tv/data/ChannelDataManager.java
+++ b/src/com/android/tv/data/ChannelDataManager.java
@@ -23,7 +23,6 @@ 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;
@@ -47,7 +46,7 @@ import com.android.tv.data.api.Channel;
import com.android.tv.util.AsyncDbTask;
import com.android.tv.util.TvInputManagerHelper;
import com.android.tv.util.Utils;
-import java.io.IOException;
+import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@@ -515,7 +514,7 @@ public class ChannelDataManager {
if (mChannelsUpdateTask != null) {
mChannelsUpdateTask.cancel(true);
}
- mChannelsUpdateTask = new QueryAllChannelsTask(mContentResolver);
+ mChannelsUpdateTask = new QueryAllChannelsTask();
mChannelsUpdateTask.executeOnDbThread();
}
@@ -599,8 +598,10 @@ public class ChannelDataManager {
.openAssetFileDescriptor(
TvContract.buildChannelLogoUri(mChannel.getId()), "r")) {
return true;
- } catch (SQLiteException | IOException | NullPointerException e) {
- // File not found or asset file not found.
+ } catch (FileNotFoundException e) {
+ // no need to log just return false
+ } catch (Exception e) {
+ Log.w(TAG, "Unable to find logo for " + mChannel, e);
}
return false;
}
@@ -616,8 +617,8 @@ public class ChannelDataManager {
private final class QueryAllChannelsTask extends AsyncDbTask.AsyncChannelQueryTask {
- QueryAllChannelsTask(ContentResolver contentResolver) {
- super(mDbExecutor, contentResolver);
+ QueryAllChannelsTask() {
+ super(mDbExecutor, mContext);
}
@Override
@@ -736,15 +737,12 @@ public class ChannelDataManager {
return;
}
mDbExecutor.execute(
- new Runnable() {
- @Override
- public void run() {
- String selection = Utils.buildSelectionForIds(Channels._ID, ids);
- ContentValues values = new ContentValues();
- values.put(columnName, columnValue);
- mContentResolver.update(
- TvContract.Channels.CONTENT_URI, values, selection, null);
- }
+ () -> {
+ String selection = Utils.buildSelectionForIds(Channels._ID, ids);
+ ContentValues values = new ContentValues();
+ values.put(columnName, columnValue);
+ mContentResolver.update(
+ TvContract.Channels.CONTENT_URI, values, selection, null);
});
}
diff --git a/src/com/android/tv/data/ChannelImpl.java b/src/com/android/tv/data/ChannelImpl.java
index 703f69c9..f31290d0 100644
--- a/src/com/android/tv/data/ChannelImpl.java
+++ b/src/com/android/tv/data/ChannelImpl.java
@@ -46,12 +46,8 @@ public final class ChannelImpl implements Channel {
/** Compares the channel numbers of channels which belong to the same input. */
public static final Comparator<Channel> CHANNEL_NUMBER_COMPARATOR =
- new Comparator<Channel>() {
- @Override
- public int compare(Channel lhs, Channel rhs) {
- return ChannelNumber.compare(lhs.getDisplayNumber(), rhs.getDisplayNumber());
- }
- };
+ (Channel lhs, Channel rhs) ->
+ ChannelNumber.compare(lhs.getDisplayNumber(), rhs.getDisplayNumber());
private static final int APP_LINK_TYPE_NOT_SET = 0;
private static final String INVALID_PACKAGE_NAME = "packageName";
@@ -74,6 +70,7 @@ public final class ChannelImpl implements 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_NETWORK_AFFILIATION,
TvContract.Channels.COLUMN_INTERNAL_PROVIDER_FLAG2, // Only used in bundled input
};
@@ -102,6 +99,7 @@ public final class ChannelImpl implements Channel {
channel.mAppLinkIconUri = cursor.getString(index++);
channel.mAppLinkPosterArtUri = cursor.getString(index++);
channel.mAppLinkIntentUri = cursor.getString(index++);
+ channel.mNetworkAffiliation = cursor.getString(index++);
if (CommonUtils.isBundledInput(channel.mInputId)) {
channel.mRecordingProhibited = cursor.getInt(index++) != 0;
}
@@ -146,6 +144,7 @@ public final class ChannelImpl implements Channel {
private String mAppLinkPosterArtUri;
private String mAppLinkIntentUri;
private Intent mAppLinkIntent;
+ private String mNetworkAffiliation;
private int mAppLinkType;
private String mLogoUri;
private boolean mRecordingProhibited;
@@ -247,6 +246,11 @@ public final class ChannelImpl implements Channel {
return mAppLinkIntentUri;
}
+ @Override
+ public String getNetworkAffiliation() {
+ return mNetworkAffiliation;
+ }
+
/** Returns channel logo uri which is got from cloud, it's used only for ChannelLogoFetcher. */
@Override
public String getLogoUri() {
@@ -311,6 +315,11 @@ public final class ChannelImpl implements Channel {
mLogoUri = logoUri;
}
+ @Override
+ public void setNetworkAffiliation(String networkAffiliation) {
+ mNetworkAffiliation = networkAffiliation;
+ }
+
/**
* 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
@@ -393,8 +402,10 @@ public final class ChannelImpl implements Channel {
mAppLinkIconUri = channel.getAppLinkIconUri();
mAppLinkPosterArtUri = channel.getAppLinkPosterArtUri();
mAppLinkIntentUri = channel.getAppLinkIntentUri();
+ mNetworkAffiliation = channel.getNetworkAffiliation();
mRecordingProhibited = channel.isRecordingProhibited();
mChannelLogoExist = channel.channelLogoExists();
+ mNetworkAffiliation = channel.getNetworkAffiliation();
}
}
@@ -421,6 +432,7 @@ public final class ChannelImpl implements Channel {
mAppLinkIconUri = other.mAppLinkIconUri;
mAppLinkPosterArtUri = other.mAppLinkPosterArtUri;
mAppLinkIntentUri = other.mAppLinkIntentUri;
+ mNetworkAffiliation = channel.mNetworkAffiliation;
mAppLinkIntent = other.mAppLinkIntent;
mAppLinkType = other.mAppLinkType;
mRecordingProhibited = other.mRecordingProhibited;
@@ -543,6 +555,12 @@ public final class ChannelImpl implements Channel {
return this;
}
+ @VisibleForTesting
+ public Builder setNetworkAffiliation(String networkAffiliation) {
+ mChannel.mNetworkAffiliation = networkAffiliation;
+ return this;
+ }
+
public Builder setAppLinkColor(int appLinkColor) {
mChannel.mAppLinkColor = appLinkColor;
return this;
diff --git a/src/com/android/tv/data/PreviewDataManager.java b/src/com/android/tv/data/PreviewDataManager.java
index 44664dcf..8616aeec 100644
--- a/src/com/android/tv/data/PreviewDataManager.java
+++ b/src/com/android/tv/data/PreviewDataManager.java
@@ -21,7 +21,6 @@ import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.Context;
import android.database.Cursor;
-import android.database.SQLException;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
@@ -31,10 +30,10 @@ import android.os.AsyncTask;
import android.os.Build;
import android.support.annotation.IntDef;
import android.support.annotation.MainThread;
-import android.support.media.tv.ChannelLogoUtils;
-import android.support.media.tv.PreviewProgram;
import android.util.Log;
import android.util.Pair;
+import androidx.tvprovider.media.tv.ChannelLogoUtils;
+import androidx.tvprovider.media.tv.PreviewProgram;
import com.android.tv.R;
import com.android.tv.common.util.PermissionUtils;
import java.lang.annotation.Retention;
@@ -225,14 +224,14 @@ public class PreviewDataManager {
try (Cursor cursor =
mContentResolver.query(
previewChannelsUri,
- android.support.media.tv.Channel.PROJECTION,
+ androidx.tvprovider.media.tv.Channel.PROJECTION,
mChannelSelection,
new String[] {packageName},
null)) {
if (cursor != null) {
while (cursor.moveToNext()) {
- android.support.media.tv.Channel previewChannel =
- android.support.media.tv.Channel.fromCursor(cursor);
+ androidx.tvprovider.media.tv.Channel previewChannel =
+ androidx.tvprovider.media.tv.Channel.fromCursor(cursor);
Long previewChannelType = previewChannel.getInternalProviderFlag1();
if (previewChannelType != null) {
previewData.addPreviewChannelId(
@@ -245,14 +244,14 @@ public class PreviewDataManager {
try (Cursor cursor =
mContentResolver.query(
previewChannelsUri,
- android.support.media.tv.Channel.PROJECTION,
+ androidx.tvprovider.media.tv.Channel.PROJECTION,
null,
null,
null)) {
if (cursor != null) {
while (cursor.moveToNext()) {
- android.support.media.tv.Channel previewChannel =
- android.support.media.tv.Channel.fromCursor(cursor);
+ androidx.tvprovider.media.tv.Channel previewChannel =
+ androidx.tvprovider.media.tv.Channel.fromCursor(cursor);
Long previewChannelType = previewChannel.getInternalProviderFlag1();
if (packageName.equals(previewChannel.getPackageName())
&& previewChannelType != null) {
@@ -283,7 +282,7 @@ public class PreviewDataManager {
}
}
}
- } catch (SQLException e) {
+ } catch (Exception e) {
Log.w(TAG, "Unable to get preview data", e);
}
return previewData;
@@ -554,7 +553,7 @@ public class PreviewDataManager {
/** A utils class for preview data. */
public static final class PreviewDataUtils {
/** Creates a preview channel. */
- public static android.support.media.tv.Channel createPreviewChannel(
+ public static androidx.tvprovider.media.tv.Channel createPreviewChannel(
Context context, @PreviewChannelType long previewChannelType) {
if (previewChannelType == TYPE_RECORDED_PROGRAM_PREVIEW_CHANNEL) {
return createRecordedProgramPreviewChannel(context, previewChannelType);
@@ -562,10 +561,10 @@ public class PreviewDataManager {
return createDefaultPreviewChannel(context, previewChannelType);
}
- private static android.support.media.tv.Channel createDefaultPreviewChannel(
+ private static androidx.tvprovider.media.tv.Channel createDefaultPreviewChannel(
Context context, @PreviewChannelType long previewChannelType) {
- android.support.media.tv.Channel.Builder builder =
- new android.support.media.tv.Channel.Builder();
+ androidx.tvprovider.media.tv.Channel.Builder builder =
+ new androidx.tvprovider.media.tv.Channel.Builder();
CharSequence appLabel =
context.getApplicationInfo().loadLabel(context.getPackageManager());
CharSequence appDescription =
@@ -578,10 +577,10 @@ public class PreviewDataManager {
return builder.build();
}
- private static android.support.media.tv.Channel createRecordedProgramPreviewChannel(
+ private static androidx.tvprovider.media.tv.Channel createRecordedProgramPreviewChannel(
Context context, @PreviewChannelType long previewChannelType) {
- android.support.media.tv.Channel.Builder builder =
- new android.support.media.tv.Channel.Builder();
+ androidx.tvprovider.media.tv.Channel.Builder builder =
+ new androidx.tvprovider.media.tv.Channel.Builder();
builder.setType(TvContract.Channels.TYPE_PREVIEW)
.setDisplayName(
context.getResources()
diff --git a/src/com/android/tv/data/PreviewProgramContent.java b/src/com/android/tv/data/PreviewProgramContent.java
index b5156408..8d4b88cf 100644
--- a/src/com/android/tv/data/PreviewProgramContent.java
+++ b/src/com/android/tv/data/PreviewProgramContent.java
@@ -19,9 +19,9 @@ package com.android.tv.data;
import android.content.Context;
import android.net.Uri;
import android.support.annotation.VisibleForTesting;
-import android.support.media.tv.TvContractCompat;
import android.text.TextUtils;
import android.util.Pair;
+import androidx.tvprovider.media.tv.TvContractCompat;
import com.android.tv.TvSingletons;
import com.android.tv.data.api.Channel;
import com.android.tv.dvr.data.RecordedProgram;
diff --git a/src/com/android/tv/data/Program.java b/src/com/android/tv/data/Program.java
index 2c64cdbb..b688927a 100644
--- a/src/com/android/tv/data/Program.java
+++ b/src/com/android/tv/data/Program.java
@@ -30,6 +30,7 @@ import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
import android.support.annotation.VisibleForTesting;
+import android.support.annotation.WorkerThread;
import android.text.TextUtils;
import android.util.Log;
import com.android.tv.common.BuildConfig;
@@ -37,8 +38,10 @@ import com.android.tv.common.TvContentRatingCache;
import com.android.tv.common.util.CollectionUtils;
import com.android.tv.common.util.CommonUtils;
import com.android.tv.data.api.Channel;
+import com.android.tv.util.TvProviderUtils;
import com.android.tv.util.Utils;
import com.android.tv.util.images.ImageLoader;
+import com.google.common.collect.ImmutableList;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
@@ -86,6 +89,16 @@ public final class Program extends BaseProgram implements Comparable<Program>, P
public static final String[] PROJECTION = createProjection();
+ public static final String[] PARTIAL_PROJECTION = {
+ TvContract.Programs._ID,
+ TvContract.Programs.COLUMN_CHANNEL_ID,
+ TvContract.Programs.COLUMN_TITLE,
+ TvContract.Programs.COLUMN_EPISODE_TITLE,
+ TvContract.Programs.COLUMN_CANONICAL_GENRE,
+ TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS,
+ TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS,
+ };
+
private static String[] createProjection() {
return CollectionUtils.concatAll(
PROJECTION_BASE,
@@ -94,7 +107,10 @@ public final class Program extends BaseProgram implements Comparable<Program>, P
: PROJECTION_DEPRECATED_IN_NYC);
}
- /** Returns the column index for {@code column}, -1 if the column doesn't exist. */
+ /**
+ * Returns the column index for {@code column},-1 if the column doesn't exist in {@link
+ * #PROJECTION}.
+ */
public static int getColumnIndex(String column) {
for (int i = 0; i < PROJECTION.length; ++i) {
if (PROJECTION[i].equals(column)) {
@@ -104,11 +120,7 @@ public final class Program extends BaseProgram implements Comparable<Program>, P
return -1;
}
- /**
- * Creates {@code Program} object from cursor.
- *
- * <p>The query that created the cursor MUST use {@link #PROJECTION}.
- */
+ /** Creates {@code Program} object from cursor. */
public static Program fromCursor(Cursor cursor) {
// Columns read must match the order of match {@link #PROJECTION}
Builder builder = new Builder();
@@ -143,6 +155,27 @@ public final class Program extends BaseProgram implements Comparable<Program>, P
builder.setSeasonNumber(cursor.getString(index++));
builder.setEpisodeNumber(cursor.getString(index++));
}
+ if (TvProviderUtils.getProgramHasSeriesIdColumn()) {
+ String seriesId = cursor.getString(index);
+ if (!TextUtils.isEmpty(seriesId)) {
+ builder.setSeriesId(seriesId);
+ }
+ }
+ return builder.build();
+ }
+
+ /** Creates {@code Program} object from cursor. */
+ public static Program fromCursorPartialProjection(Cursor cursor) {
+ // Columns read must match the order of match {@link #PARTIAL_PROJECTION}
+ Builder builder = new Builder();
+ int index = 0;
+ builder.setId(cursor.getLong(index++));
+ builder.setChannelId(cursor.getLong(index++));
+ builder.setTitle(cursor.getString(index++));
+ builder.setEpisodeTitle(cursor.getString(index++));
+ builder.setCanonicalGenres(cursor.getString(index++));
+ builder.setStartTimeUtcMillis(cursor.getLong(index++));
+ builder.setEndTimeUtcMillis(cursor.getLong(index++));
return builder.build();
}
@@ -169,10 +202,14 @@ public final class Program extends BaseProgram implements Comparable<Program>, P
program.mCanonicalGenreIds = in.createIntArray();
int length = in.readInt();
if (length > 0) {
- program.mContentRatings = new TvContentRating[length];
+ ImmutableList.Builder<TvContentRating> ratingsBuilder =
+ ImmutableList.builderWithExpectedSize(length);
for (int i = 0; i < length; ++i) {
- program.mContentRatings[i] = TvContentRating.unflattenFromString(in.readString());
+ ratingsBuilder.add(TvContentRating.unflattenFromString(in.readString()));
}
+ program.mContentRatings = ratingsBuilder.build();
+ } else {
+ program.mContentRatings = ImmutableList.of();
}
program.mRecordingProhibited = in.readByte() != (byte) 0;
return program;
@@ -202,6 +239,7 @@ public final class Program extends BaseProgram implements Comparable<Program>, P
private String mEpisodeNumber;
private long mStartTimeUtcMillis;
private long mEndTimeUtcMillis;
+ private String mDurationString;
private String mDescription;
private String mLongDescription;
private int mVideoWidth;
@@ -210,7 +248,7 @@ public final class Program extends BaseProgram implements Comparable<Program>, P
private String mPosterArtUri;
private String mThumbnailUri;
private int[] mCanonicalGenreIds;
- private TvContentRating[] mContentRatings;
+ private ImmutableList<TvContentRating> mContentRatings;
private boolean mRecordingProhibited;
private Program() {
@@ -278,6 +316,15 @@ public final class Program extends BaseProgram implements Comparable<Program>, P
return mEndTimeUtcMillis;
}
+ public String getDurationString(Context context) {
+ // TODO(b/71717446): expire the calculated string
+ if (mDurationString == null) {
+ mDurationString =
+ Utils.getDurationString(context, mStartTimeUtcMillis, mEndTimeUtcMillis, true);
+ }
+ return mDurationString;
+ }
+
/** Returns the program duration. */
@Override
public long getDurationMillis() {
@@ -310,7 +357,7 @@ public final class Program extends BaseProgram implements Comparable<Program>, P
@Nullable
@Override
- public TvContentRating[] getContentRatings() {
+ public ImmutableList<TvContentRating> getContentRatings() {
return mContentRatings;
}
@@ -379,7 +426,7 @@ public final class Program extends BaseProgram implements Comparable<Program>, P
mVideoHeight,
mPosterArtUri,
mThumbnailUri,
- Arrays.hashCode(mContentRatings),
+ mContentRatings,
Arrays.hashCode(mCanonicalGenreIds),
mSeasonNumber,
mSeasonTitle,
@@ -407,7 +454,7 @@ public final class Program extends BaseProgram implements Comparable<Program>, P
&& mVideoHeight == program.mVideoHeight
&& Objects.equals(mPosterArtUri, program.mPosterArtUri)
&& Objects.equals(mThumbnailUri, program.mThumbnailUri)
- && Arrays.equals(mContentRatings, program.mContentRatings)
+ && Objects.equals(mContentRatings, program.mContentRatings)
&& Arrays.equals(mCanonicalGenreIds, program.mCanonicalGenreIds)
&& Objects.equals(mSeasonNumber, program.mSeasonNumber)
&& Objects.equals(mSeasonTitle, program.mSeasonTitle)
@@ -474,7 +521,8 @@ public final class Program extends BaseProgram implements Comparable<Program>, P
*/
@SuppressLint("InlinedApi")
@SuppressWarnings("deprecation")
- public static ContentValues toContentValues(Program program) {
+ @WorkerThread
+ public static ContentValues toContentValues(Program program, Context context) {
ContentValues values = new ContentValues();
values.put(TvContract.Programs.COLUMN_CHANNEL_ID, program.getChannelId());
if (!TextUtils.isEmpty(program.getPackageName())) {
@@ -495,6 +543,10 @@ public final class Program extends BaseProgram implements Comparable<Program>, P
putValue(values, TvContract.Programs.COLUMN_SEASON_NUMBER, program.getSeasonNumber());
putValue(values, TvContract.Programs.COLUMN_EPISODE_NUMBER, program.getEpisodeNumber());
}
+ if (TvProviderUtils.checkSeriesIdColumn(context, Programs.CONTENT_URI)) {
+ putValue(values, COLUMN_SERIES_ID, program.getSeriesId());
+ }
+
putValue(values, TvContract.Programs.COLUMN_SHORT_DESCRIPTION, program.getDescription());
putValue(values, TvContract.Programs.COLUMN_LONG_DESCRIPTION, program.getLongDescription());
putValue(values, TvContract.Programs.COLUMN_POSTER_ART_URI, program.getPosterArtUri());
@@ -554,6 +606,7 @@ public final class Program extends BaseProgram implements Comparable<Program>, P
mEpisodeNumber = other.mEpisodeNumber;
mStartTimeUtcMillis = other.mStartTimeUtcMillis;
mEndTimeUtcMillis = other.mEndTimeUtcMillis;
+ mDurationString = null; // Recreate Duration when needed.
mDescription = other.mDescription;
mLongDescription = other.mLongDescription;
mVideoWidth = other.mVideoWidth;
@@ -582,6 +635,7 @@ public final class Program extends BaseProgram implements Comparable<Program>, P
mProgram.mEpisodeNumber = null;
mProgram.mStartTimeUtcMillis = -1;
mProgram.mEndTimeUtcMillis = -1;
+ mProgram.mDurationString = null;
mProgram.mDescription = null;
mProgram.mLongDescription = null;
mProgram.mRecordingProhibited = false;
@@ -771,7 +825,7 @@ public final class Program extends BaseProgram implements Comparable<Program>, P
* @param contentRatings the content ratings
* @return a reference to this object
*/
- public Builder setContentRatings(TvContentRating[] contentRatings) {
+ public Builder setContentRatings(ImmutableList<TvContentRating> contentRatings) {
mProgram.mContentRatings = contentRatings;
return this;
}
@@ -947,7 +1001,7 @@ public final class Program extends BaseProgram implements Comparable<Program>, P
out.writeString(mPosterArtUri);
out.writeString(mThumbnailUri);
out.writeIntArray(mCanonicalGenreIds);
- out.writeInt(mContentRatings == null ? 0 : mContentRatings.length);
+ out.writeInt(mContentRatings == null ? 0 : mContentRatings.size());
if (mContentRatings != null) {
for (TvContentRating rating : mContentRatings) {
out.writeString(rating.flattenToString());
diff --git a/src/com/android/tv/data/ProgramDataManager.java b/src/com/android/tv/data/ProgramDataManager.java
index 4631806c..2f20c89a 100644
--- a/src/com/android/tv/data/ProgramDataManager.java
+++ b/src/com/android/tv/data/ProgramDataManager.java
@@ -35,14 +35,17 @@ import android.util.LongSparseArray;
import android.util.LruCache;
import com.android.tv.TvSingletons;
import com.android.tv.common.SoftPreconditions;
-import com.android.tv.common.config.api.RemoteConfig;
-import com.android.tv.common.config.api.RemoteConfigValue;
import com.android.tv.common.memory.MemoryManageable;
import com.android.tv.common.util.Clock;
import com.android.tv.data.api.Channel;
+import com.android.tv.perf.EventNames;
+import com.android.tv.perf.PerformanceMonitor;
+import com.android.tv.perf.TimerEvent;
import com.android.tv.util.AsyncDbTask;
import com.android.tv.util.MultiLongSparseArray;
+import com.android.tv.util.TvProviderUtils;
import com.android.tv.util.Utils;
+import com.android.tv.common.flags.BackendKnobsFlags;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@@ -71,8 +74,6 @@ public class ProgramDataManager implements MemoryManageable {
// TODO: need to optimize consecutive DB updates.
private static final long CURRENT_PROGRAM_UPDATE_WAIT_MS = TimeUnit.SECONDS.toMillis(5);
@VisibleForTesting static final long PROGRAM_GUIDE_SNAP_TIME_MS = TimeUnit.MINUTES.toMillis(30);
- private static final RemoteConfigValue<Long> PROGRAM_GUIDE_MAX_HOURS =
- RemoteConfigValue.create("live_channels_program_guide_max_hours", 48);
// TODO: Use TvContract constants, once they become public.
private static final String PARAM_START_TIME = "start_time";
@@ -90,10 +91,13 @@ public class ProgramDataManager implements MemoryManageable {
private static final int MSG_UPDATE_ONE_CURRENT_PROGRAM = 1001;
private static final int MSG_UPDATE_PREFETCH_PROGRAM = 1002;
+ private final Context mContext;
private final Clock mClock;
private final ContentResolver mContentResolver;
private final Executor mDbExecutor;
- private final RemoteConfig mRemoteConfig;
+ private final BackendKnobsFlags mBackendKnobsFlags;
+ private final PerformanceMonitor mPerformanceMonitor;
+ private final ChannelDataManager mChannelDataManager;
private boolean mStarted;
// Updated only on the main thread.
private volatile boolean mCurrentProgramsLoadFinished;
@@ -104,15 +108,15 @@ public class ProgramDataManager implements MemoryManageable {
private final MultiLongSparseArray<OnCurrentProgramUpdatedListener>
mChannelId2ProgramUpdatedListeners = new MultiLongSparseArray<>();
private final Handler mHandler;
- private final Set<Listener> mListeners = new ArraySet<>();
-
+ private final Set<Callback> mCallbacks = new ArraySet<>();
+ private Map<Long, ArrayList<Program>> mChannelIdProgramCache = new ConcurrentHashMap<>();
+ private final Set<Long> mCompleteInfoChannelIds = new HashSet<>();
private final ContentObserver mProgramObserver;
private boolean mPrefetchEnabled;
private long mProgramPrefetchUpdateWaitMs;
private long mLastPrefetchTaskRunMs;
private ProgramsPrefetchTask mProgramsPrefetchTask;
- private Map<Long, ArrayList<Program>> mChannelIdProgramCache = new HashMap<>();
// Any program that ends prior to this time will be removed from the cache
// when a channel's current program is updated.
@@ -125,25 +129,34 @@ public class ProgramDataManager implements MemoryManageable {
@MainThread
public ProgramDataManager(Context context) {
this(
+ context,
TvSingletons.getSingletons(context).getDbExecutor(),
context.getContentResolver(),
Clock.SYSTEM,
Looper.myLooper(),
- TvSingletons.getSingletons(context).getRemoteConfig());
+ TvSingletons.getSingletons(context).getBackendKnobs(),
+ TvSingletons.getSingletons(context).getPerformanceMonitor(),
+ TvSingletons.getSingletons(context).getChannelDataManager());
}
@VisibleForTesting
ProgramDataManager(
+ Context context,
Executor executor,
ContentResolver contentResolver,
Clock time,
Looper looper,
- RemoteConfig remoteConfig) {
+ BackendKnobsFlags backendKnobsFlags,
+ PerformanceMonitor performanceMonitor,
+ ChannelDataManager channelDataManager) {
+ mContext = context;
mDbExecutor = executor;
mClock = time;
mContentResolver = contentResolver;
mHandler = new MyHandler(looper);
- mRemoteConfig = remoteConfig;
+ mBackendKnobsFlags = backendKnobsFlags;
+ mPerformanceMonitor = performanceMonitor;
+ mChannelDataManager = channelDataManager;
mProgramObserver =
new ContentObserver(mHandler) {
@Override
@@ -246,24 +259,43 @@ public class ProgramDataManager implements MemoryManageable {
}
}
- /** A listener interface to receive notification on program data retrieval from DB. */
- public interface Listener {
+ public void prefetchChannel(long channelId) {
+ if (mCompleteInfoChannelIds.add(channelId)) {
+ long startTimeMs =
+ Utils.floorTime(
+ mClock.currentTimeMillis() - PROGRAM_GUIDE_SNAP_TIME_MS,
+ PROGRAM_GUIDE_SNAP_TIME_MS);
+ long endTimeMs = startTimeMs + TimeUnit.HOURS.toMillis(getFetchDuration());
+ new SingleChannelPrefetchTask(channelId, startTimeMs, endTimeMs).executeOnDbThread();
+ }
+ }
+
+ /** A Callback interface to receive notification on program data retrieval from DB. */
+ public interface Callback {
/**
* Called when a Program data is now available through getProgram() after the DB operation
* is done which wasn't before. This would be called only if fetched data is around the
* selected program.
*/
void onProgramUpdated();
+
+ /**
+ * Called when we update complete program data of specific channel during scrolling. Data is
+ * loaded from DB on request basis.
+ *
+ * @param channelId
+ */
+ void onSingleChannelUpdated(long channelId);
}
- /** Adds the {@link Listener}. */
- public void addListener(Listener listener) {
- mListeners.add(listener);
+ /** Adds the {@link Callback}. */
+ public void addCallback(Callback callback) {
+ mCallbacks.add(callback);
}
- /** Removes the {@link Listener}. */
- public void removeListener(Listener listener) {
- mListeners.remove(listener);
+ /** Removes the {@link Callback}. */
+ public void removeCallback(Callback callback) {
+ mCallbacks.remove(callback);
}
/** Enables or Disables program prefetch. */
@@ -451,7 +483,7 @@ public class ProgramDataManager implements MemoryManageable {
}
clearTask(mProgramUpdateTaskMap);
mHandler.removeMessages(MSG_UPDATE_ONE_CURRENT_PROGRAM);
- mProgramsUpdateTask = new ProgramsUpdateTask(mContentResolver, mClock.currentTimeMillis());
+ mProgramsUpdateTask = new ProgramsUpdateTask(mClock.currentTimeMillis());
mProgramsUpdateTask.executeOnDbThread();
}
@@ -461,20 +493,29 @@ public class ProgramDataManager implements MemoryManageable {
private final long mEndTimeMs;
private boolean mSuccess;
+ private TimerEvent mFromEmptyCacheTimeEvent;
public ProgramsPrefetchTask() {
super(mDbExecutor);
long time = mClock.currentTimeMillis();
mStartTimeMs =
Utils.floorTime(time - PROGRAM_GUIDE_SNAP_TIME_MS, PROGRAM_GUIDE_SNAP_TIME_MS);
- mEndTimeMs =
- mStartTimeMs
- + TimeUnit.HOURS.toMillis(PROGRAM_GUIDE_MAX_HOURS.get(mRemoteConfig));
+ mEndTimeMs = mStartTimeMs + TimeUnit.HOURS.toMillis(getFetchDuration());
mSuccess = false;
}
@Override
+ protected void onPreExecute() {
+ if (mChannelIdCurrentProgramMap.isEmpty()) {
+ // No current program guide is shown.
+ // Measure the delay before users can see program guides.
+ mFromEmptyCacheTimeEvent = mPerformanceMonitor.startTimer();
+ }
+ }
+
+ @Override
protected Map<Long, ArrayList<Program>> doInBackground(Void... params) {
+ TimerEvent asyncTimeEvent = mPerformanceMonitor.startTimer();
Map<Long, ArrayList<Program>> programMap = new HashMap<>();
if (DEBUG) {
Log.d(
@@ -497,8 +538,19 @@ public class ProgramDataManager implements MemoryManageable {
return null;
}
programMap.clear();
- try (Cursor c =
- mContentResolver.query(uri, Program.PROJECTION, null, null, SORT_BY_TIME)) {
+
+ String[] projection =
+ mBackendKnobsFlags.enablePartialProgramFetch()
+ ? Program.PARTIAL_PROJECTION
+ : Program.PROJECTION;
+ if (TvProviderUtils.checkSeriesIdColumn(mContext, Programs.CONTENT_URI)) {
+ if (Utils.isProgramsUri(uri)) {
+ projection =
+ TvProviderUtils.addExtraColumnsToProjection(
+ projection, TvProviderUtils.EXTRA_PROGRAM_COLUMN_SERIES_ID);
+ }
+ }
+ try (Cursor c = mContentResolver.query(uri, projection, null, null, SORT_BY_TIME)) {
if (c == null) {
continue;
}
@@ -510,7 +562,10 @@ public class ProgramDataManager implements MemoryManageable {
}
return null;
}
- Program program = Program.fromCursor(c);
+ Program program =
+ mBackendKnobsFlags.enablePartialProgramFetch()
+ ? Program.fromCursorPartialProjection(c)
+ : Program.fromCursor(c);
if (Program.isDuplicate(program, lastReadProgram)) {
duplicateCount++;
continue;
@@ -520,6 +575,15 @@ public class ProgramDataManager implements MemoryManageable {
ArrayList<Program> programs = programMap.get(program.getChannelId());
if (programs == null) {
programs = new ArrayList<>();
+ if (mBackendKnobsFlags.enablePartialProgramFetch()) {
+ // To skip already loaded complete data.
+ Program currentProgramInfo =
+ mChannelIdCurrentProgramMap.get(program.getChannelId());
+ if (currentProgramInfo != null
+ && Program.isDuplicate(program, currentProgramInfo)) {
+ program = currentProgramInfo;
+ }
+ }
programMap.put(program.getChannelId(), programs);
}
programs.add(program);
@@ -534,12 +598,17 @@ public class ProgramDataManager implements MemoryManageable {
Log.d(TAG, "Database is changed while querying. Will retry.");
}
} catch (SecurityException e) {
- Log.d(TAG, "Security exception during program data query", e);
+ Log.w(TAG, "Security exception during program data query", e);
+ } catch (Exception e) {
+ Log.w(TAG, "Error during program data query", e);
}
}
if (DEBUG) {
Log.d(TAG, "Ends programs prefetch for " + programMap.size() + " channels");
}
+ mPerformanceMonitor.stopTimer(
+ asyncTimeEvent,
+ EventNames.PROGRAM_DATA_MANAGER_PROGRAMS_PREFETCH_TASK_DO_IN_BACKGROUND);
return programMap;
}
@@ -552,8 +621,6 @@ public class ProgramDataManager implements MemoryManageable {
}
long nextMessageDelayedTime;
if (mSuccess) {
- mChannelIdProgramCache = programs;
- notifyProgramUpdated();
long currentTime = mClock.currentTimeMillis();
mLastPrefetchTaskRunMs = currentTime;
nextMessageDelayedTime =
@@ -561,6 +628,22 @@ public class ProgramDataManager implements MemoryManageable {
mLastPrefetchTaskRunMs + PROGRAM_GUIDE_SNAP_TIME_MS,
PROGRAM_GUIDE_SNAP_TIME_MS)
- currentTime;
+ // Issue second pre-fetch immediately after the first partial update
+ if (mChannelIdProgramCache.isEmpty()) {
+ nextMessageDelayedTime = 0;
+ }
+ mChannelIdProgramCache = programs;
+ if (mBackendKnobsFlags.enablePartialProgramFetch()) {
+ // Since cache has partial data we need to reset the map of complete data.
+ mCompleteInfoChannelIds.clear();
+ }
+ notifyProgramUpdated();
+ if (mFromEmptyCacheTimeEvent != null) {
+ mPerformanceMonitor.stopTimer(
+ mFromEmptyCacheTimeEvent,
+ EventNames.PROGRAM_GUIDE_SHOW_FROM_EMPTY_CACHE);
+ mFromEmptyCacheTimeEvent = null;
+ }
} else {
nextMessageDelayedTime = PERIODIC_PROGRAM_UPDATE_MIN_MS;
}
@@ -571,17 +654,78 @@ public class ProgramDataManager implements MemoryManageable {
}
}
+ private long getFetchDuration() {
+ if (mChannelIdProgramCache.isEmpty()) {
+ return Math.max(1L, mBackendKnobsFlags.programGuideInitialFetchHours());
+ } else {
+ long durationHours;
+ int channelCount = mChannelDataManager.getChannelCount();
+ long knobsMaxHours = mBackendKnobsFlags.programGuideMaxHours();
+ long targetChannelCount = mBackendKnobsFlags.epgTargetChannelCount();
+ if (channelCount <= targetChannelCount) {
+ durationHours = Math.max(48L, knobsMaxHours);
+ } else {
+ // 2 days <= duration <= 14 days (336 hours)
+ durationHours = knobsMaxHours * targetChannelCount / channelCount;
+ if (durationHours < 48L) {
+ durationHours = 48L;
+ } else if (durationHours > 336L) {
+ durationHours = 336L;
+ }
+ }
+ return durationHours;
+ }
+ }
+
+ private class SingleChannelPrefetchTask extends AsyncDbTask.AsyncQueryTask<ArrayList<Program>> {
+ long mChannelId;
+
+ public SingleChannelPrefetchTask(long channelId, long startTimeMs, long endTimeMs) {
+ super(
+ mDbExecutor,
+ mContext,
+ TvContract.buildProgramsUriForChannel(channelId, startTimeMs, endTimeMs),
+ Program.PROJECTION,
+ null,
+ null,
+ SORT_BY_TIME);
+ mChannelId = channelId;
+ }
+
+ @Override
+ protected ArrayList<Program> onQuery(Cursor c) {
+ ArrayList<Program> programMap = new ArrayList<>();
+ while (c.moveToNext()) {
+ Program program = Program.fromCursor(c);
+ programMap.add(program);
+ }
+ return programMap;
+ }
+
+ @Override
+ protected void onPostExecute(ArrayList<Program> programs) {
+ mChannelIdProgramCache.put(mChannelId, programs);
+ notifySingleChannelUpdated(mChannelId);
+ }
+ }
+
private void notifyProgramUpdated() {
- for (Listener listener : mListeners) {
- listener.onProgramUpdated();
+ for (Callback callback : mCallbacks) {
+ callback.onProgramUpdated();
+ }
+ }
+
+ private void notifySingleChannelUpdated(long channelId) {
+ for (Callback callback : mCallbacks) {
+ callback.onSingleChannelUpdated(channelId);
}
}
private class ProgramsUpdateTask extends AsyncDbTask.AsyncQueryTask<List<Program>> {
- public ProgramsUpdateTask(ContentResolver contentResolver, long time) {
+ public ProgramsUpdateTask(long time) {
super(
mDbExecutor,
- contentResolver,
+ mContext,
Programs.CONTENT_URI
.buildUpon()
.appendQueryParameter(PARAM_START_TIME, String.valueOf(time))
@@ -633,6 +777,9 @@ public class ProgramDataManager implements MemoryManageable {
for (Long channelId : removedChannelIds) {
if (mPrefetchEnabled) {
mChannelIdProgramCache.remove(channelId);
+ if (mBackendKnobsFlags.enablePartialProgramFetch()) {
+ mCompleteInfoChannelIds.remove(channelId);
+ }
}
mChannelIdCurrentProgramMap.remove(channelId);
notifyCurrentProgramUpdate(channelId, null);
@@ -645,11 +792,10 @@ public class ProgramDataManager implements MemoryManageable {
private class UpdateCurrentProgramForChannelTask extends AsyncDbTask.AsyncQueryTask<Program> {
private final long mChannelId;
- private UpdateCurrentProgramForChannelTask(
- ContentResolver contentResolver, long channelId, long time) {
+ private UpdateCurrentProgramForChannelTask(long channelId, long time) {
super(
mDbExecutor,
- contentResolver,
+ mContext,
TvContract.buildProgramsUriForChannel(channelId, time, time),
Program.PROJECTION,
null,
@@ -695,7 +841,7 @@ public class ProgramDataManager implements MemoryManageable {
}
UpdateCurrentProgramForChannelTask task =
new UpdateCurrentProgramForChannelTask(
- mContentResolver, channelId, mClock.currentTimeMillis());
+ channelId, mClock.currentTimeMillis());
mProgramUpdateTaskMap.put(channelId, task);
task.executeOnDbThread();
break;
diff --git a/src/com/android/tv/data/WatchedHistoryManager.java b/src/com/android/tv/data/WatchedHistoryManager.java
index 7187efd1..9c1d423f 100644
--- a/src/com/android/tv/data/WatchedHistoryManager.java
+++ b/src/com/android/tv/data/WatchedHistoryManager.java
@@ -34,6 +34,7 @@ import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Scanner;
+import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
/**
@@ -73,24 +74,20 @@ public class WatchedHistoryManager {
// onNewRecordAdded will be called in the same thread as the thread
// which created this instance.
mHandler.post(
- new Runnable() {
- @Override
- public void run() {
- for (long i = mLastIndex + 1; i <= lastIndex; ++i) {
- WatchedRecord record =
- decode(
- mSharedPreferences.getString(
- getSharedPreferencesKey(i),
- null));
- if (record != null) {
- mWatchedHistory.add(record);
- if (mListener != null) {
- mListener.onNewRecordAdded(record);
- }
+ () -> {
+ for (long i = mLastIndex + 1; i <= lastIndex; ++i) {
+ WatchedRecord record =
+ decode(
+ mSharedPreferences.getString(
+ getSharedPreferencesKey(i), null));
+ if (record != null) {
+ mWatchedHistory.add(record);
+ if (mListener != null) {
+ mListener.onNewRecordAdded(record);
}
}
- mLastIndex = lastIndex;
}
+ mLastIndex = lastIndex;
});
}
}
@@ -100,16 +97,18 @@ public class WatchedHistoryManager {
private Listener mListener;
private final int mMaxHistorySize;
private final Handler mHandler;
+ private final Executor mExecutor;
public WatchedHistoryManager(Context context) {
- this(context, MAX_HISTORY_SIZE);
+ this(context, MAX_HISTORY_SIZE, AsyncTask.THREAD_POOL_EXECUTOR);
}
@VisibleForTesting
- WatchedHistoryManager(Context context, int maxHistorySize) {
+ WatchedHistoryManager(Context context, int maxHistorySize, Executor executor) {
mContext = context.getApplicationContext();
mMaxHistorySize = maxHistorySize;
mHandler = new Handler();
+ mExecutor = executor;
}
/** Starts the manager. It loads history data from {@link SharedPreferences}. */
@@ -130,7 +129,7 @@ public class WatchedHistoryManager {
protected void onPostExecute(Void params) {
onLoadFinished();
}
- }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }.executeOnExecutor(mExecutor);
} else {
loadWatchedHistory();
onLoadFinished();
diff --git a/src/com/android/tv/data/api/Channel.java b/src/com/android/tv/data/api/Channel.java
index 496331cf..fb00952c 100644
--- a/src/com/android/tv/data/api/Channel.java
+++ b/src/com/android/tv/data/api/Channel.java
@@ -85,6 +85,8 @@ public interface Channel {
String getAppLinkIntentUri();
+ String getNetworkAffiliation();
+
String getLogoUri();
boolean isRecordingProhibited();
@@ -109,6 +111,8 @@ public interface Channel {
void setLogoUri(String logoUri);
+ void setNetworkAffiliation(String networkAffiliation);
+
boolean channelLogoExists();
void loadBitmap(
diff --git a/src/com/android/tv/data/epg/AutoValue_EpgReader_EpgChannel.java b/src/com/android/tv/data/epg/AutoValue_EpgReader_EpgChannel.java
deleted file mode 100644
index 795ad5c4..00000000
--- a/src/com/android/tv/data/epg/AutoValue_EpgReader_EpgChannel.java
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.tv.data.epg;
-
-import com.android.tv.data.api.Channel;
-
-/**
- * Hand copy of generated Autovalue class.
- *
- * TODO get autovalue working
- */
-final class AutoValue_EpgReader_EpgChannel extends EpgReader.EpgChannel {
-
- private final Channel channel;
- private final String epgChannelId;
-
- AutoValue_EpgReader_EpgChannel(
- Channel channel,
- String epgChannelId) {
- if (channel == null) {
- throw new NullPointerException("Null channel");
- }
- this.channel = channel;
- if (epgChannelId == null) {
- throw new NullPointerException("Null epgChannelId");
- }
- this.epgChannelId = epgChannelId;
- }
-
- @Override
- public Channel getChannel() {
- return channel;
- }
-
- @Override
- public String getEpgChannelId() {
- return epgChannelId;
- }
-
- @Override
- public String toString() {
- return "EpgChannel{"
- + "channel=" + channel + ", "
- + "epgChannelId=" + epgChannelId
- + "}";
- }
-
- @Override
- public boolean equals(Object o) {
- if (o == this) {
- return true;
- }
- if (o instanceof EpgReader.EpgChannel) {
- EpgReader.EpgChannel that = (EpgReader.EpgChannel) o;
- return (this.channel.equals(that.getChannel()))
- && (this.epgChannelId.equals(that.getEpgChannelId()));
- }
- return false;
- }
-
- @Override
- public int hashCode() {
- int h = 1;
- h *= 1000003;
- h ^= this.channel.hashCode();
- h *= 1000003;
- h ^= this.epgChannelId.hashCode();
- return h;
- }
-
-}
-
diff --git a/src/com/android/tv/data/epg/EpgFetchHelper.java b/src/com/android/tv/data/epg/EpgFetchHelper.java
index 3c7112ec..3843ca99 100644
--- a/src/com/android/tv/data/epg/EpgFetchHelper.java
+++ b/src/com/android/tv/data/epg/EpgFetchHelper.java
@@ -17,6 +17,7 @@
package com.android.tv.data.epg;
import android.content.ContentProviderOperation;
+import android.content.ContentValues;
import android.content.Context;
import android.content.OperationApplicationException;
import android.database.Cursor;
@@ -30,9 +31,13 @@ import android.util.Log;
import com.android.tv.common.CommonConstants;
import com.android.tv.common.util.Clock;
import com.android.tv.data.Program;
+import com.android.tv.data.api.Channel;
+import com.android.tv.features.TvFeatures;
+import com.android.tv.util.TvProviderUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import java.util.Set;
import java.util.concurrent.TimeUnit;
/** The helper class for {@link EpgFetcher} */
@@ -101,7 +106,7 @@ class EpgFetchHelper {
ops.add(
ContentProviderOperation.newUpdate(
TvContract.buildProgramUri(oldProgram.getId()))
- .withValues(Program.toContentValues(newProgram))
+ .withValues(Program.toContentValues(newProgram, context))
.build());
oldProgramsIndex++;
newProgramsIndex++;
@@ -127,7 +132,7 @@ class EpgFetchHelper {
if (addNewProgram) {
ops.add(
ContentProviderOperation.newInsert(Programs.CONTENT_URI)
- .withValues(Program.toContentValues(newProgram))
+ .withValues(Program.toContentValues(newProgram, context))
.build());
}
// Throttle the batch operation not to cause TransactionTooLargeException.
@@ -155,14 +160,57 @@ class EpgFetchHelper {
return updated;
}
+ @WorkerThread
+ static void updateNetworkAffiliation(Context context, Set<EpgReader.EpgChannel> channels) {
+ if (!TvFeatures.STORE_NETWORK_AFFILIATION.isEnabled(context)) {
+ return;
+ }
+ ArrayList<ContentProviderOperation> ops = new ArrayList<>();
+ for (EpgReader.EpgChannel epgChannel : channels) {
+ if (!epgChannel.getDbUpdateNeeded()) {
+ continue;
+ }
+ Channel channel = epgChannel.getChannel();
+
+ ContentValues values = new ContentValues();
+ values.put(
+ TvContract.Channels.COLUMN_NETWORK_AFFILIATION,
+ channel.getNetworkAffiliation());
+ ops.add(
+ ContentProviderOperation.newUpdate(TvContract.buildChannelUri(channel.getId()))
+ .withValues(values)
+ .build());
+ if (ops.size() >= BATCH_OPERATION_COUNT) {
+ try {
+ context.getContentResolver().applyBatch(TvContract.AUTHORITY, ops);
+ } catch (RemoteException | OperationApplicationException e) {
+ Log.e(TAG, "Failed to update channels.", e);
+ }
+ ops.clear();
+ }
+ }
+ try {
+ context.getContentResolver().applyBatch(TvContract.AUTHORITY, ops);
+ } catch (RemoteException | OperationApplicationException e) {
+ Log.e(TAG, "Failed to update channels.", e);
+ }
+ }
+
+ @WorkerThread
private static List<Program> queryPrograms(
Context context, long channelId, long startTimeMs, long endTimeMs) {
+ String[] projection = Program.PROJECTION;
+ if (TvProviderUtils.checkSeriesIdColumn(context, Programs.CONTENT_URI)) {
+ projection =
+ TvProviderUtils.addExtraColumnsToProjection(
+ projection, TvProviderUtils.EXTRA_PROGRAM_COLUMN_SERIES_ID);
+ }
try (Cursor c =
context.getContentResolver()
.query(
TvContract.buildProgramsUriForChannel(
channelId, startTimeMs, endTimeMs),
- Program.PROJECTION,
+ projection,
null,
null,
Programs.COLUMN_START_TIME_UTC_MILLIS)) {
diff --git a/src/com/android/tv/data/epg/EpgFetcherImpl.java b/src/com/android/tv/data/epg/EpgFetcherImpl.java
index 2aaaa5b2..b191421f 100644
--- a/src/com/android/tv/data/epg/EpgFetcherImpl.java
+++ b/src/com/android/tv/data/epg/EpgFetcherImpl.java
@@ -38,11 +38,10 @@ import android.support.annotation.VisibleForTesting;
import android.support.annotation.WorkerThread;
import android.text.TextUtils;
import android.util.Log;
-import com.android.tv.TvFeatures;
import com.android.tv.TvSingletons;
import com.android.tv.common.BuildConfig;
import com.android.tv.common.SoftPreconditions;
-import com.android.tv.common.config.api.RemoteConfigValue;
+import com.android.tv.common.buildtype.HasBuildType;
import com.android.tv.common.util.Clock;
import com.android.tv.common.util.CommonUtils;
import com.android.tv.common.util.LocationUtils;
@@ -55,12 +54,15 @@ import com.android.tv.data.ChannelLogoFetcher;
import com.android.tv.data.Lineup;
import com.android.tv.data.Program;
import com.android.tv.data.api.Channel;
+import com.android.tv.features.TvFeatures;
import com.android.tv.perf.EventNames;
import com.android.tv.perf.PerformanceMonitor;
import com.android.tv.perf.TimerEvent;
import com.android.tv.util.Utils;
import com.google.android.tv.partner.support.EpgInput;
import com.google.android.tv.partner.support.EpgInputs;
+import com.google.common.collect.ImmutableSet;
+import com.android.tv.common.flags.BackendKnobsFlags;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
@@ -100,8 +102,7 @@ public class EpgFetcherImpl implements EpgFetcher {
private static final long FETCH_DURING_SCAN_DURATION_SEC = TimeUnit.HOURS.toSeconds(3);
private static final long FAST_FETCH_DURATION_SEC = TimeUnit.DAYS.toSeconds(2);
- private static final RemoteConfigValue<Long> ROUTINE_INTERVAL_HOUR =
- RemoteConfigValue.create("live_channels_epg_fetcher_interval_hour", 4);
+ private static final long DEFAULT_ROUTINE_INTERVAL_HOUR = 4;
private static final int MSG_PREPARE_FETCH_DURING_SCAN = 1;
private static final int MSG_CHANNEL_UPDATED_DURING_SCAN = 2;
@@ -115,6 +116,9 @@ public class EpgFetcherImpl implements EpgFetcher {
private final ChannelDataManager mChannelDataManager;
private final EpgReader mEpgReader;
private final PerformanceMonitor mPerformanceMonitor;
+ private final EpgInputWhiteList mEpgInputWhiteList;
+ private final BackendKnobsFlags mBackendKnobsFlags;
+ private final HasBuildType.BuildType mBuildType;
private FetchAsyncTask mFetchTask;
private FetchDuringScanHandler mFetchDuringScanHandler;
private long mEpgTimeStamp;
@@ -124,9 +128,6 @@ public class EpgFetcherImpl implements EpgFetcher {
// A flag to block the re-entrance of onChannelScanStarted and onChannelScanFinished.
private boolean mScanStarted;
- private final long mRoutineIntervalMs;
- private final long mEpgDataExpiredTimeLimitMs;
- private final long mFastFetchDurationSec;
private Clock mClock;
public static EpgFetcher create(Context context) {
@@ -136,36 +137,54 @@ public class EpgFetcherImpl implements EpgFetcher {
PerformanceMonitor performanceMonitor = tvSingletons.getPerformanceMonitor();
EpgReader epgReader = tvSingletons.providesEpgReader().get();
Clock clock = tvSingletons.getClock();
- long routineIntervalMs = ROUTINE_INTERVAL_HOUR.get(tvSingletons.getRemoteConfig());
-
+ EpgInputWhiteList epgInputWhiteList =
+ new EpgInputWhiteList(tvSingletons.getCloudEpgFlags());
+ BackendKnobsFlags backendKnobsFlags = tvSingletons.getBackendKnobs();
+ HasBuildType.BuildType buildType = tvSingletons.getBuildType();
return new EpgFetcherImpl(
context,
+ epgInputWhiteList,
channelDataManager,
epgReader,
performanceMonitor,
clock,
- routineIntervalMs);
+ backendKnobsFlags,
+ buildType);
}
@VisibleForTesting
EpgFetcherImpl(
Context context,
+ EpgInputWhiteList epgInputWhiteList,
ChannelDataManager channelDataManager,
EpgReader epgReader,
PerformanceMonitor performanceMonitor,
Clock clock,
- long routineIntervalMs) {
+ BackendKnobsFlags backendKnobsFlags,
+ HasBuildType.BuildType buildType) {
mContext = context;
mChannelDataManager = channelDataManager;
mEpgReader = epgReader;
mPerformanceMonitor = performanceMonitor;
mClock = clock;
- mRoutineIntervalMs =
- routineIntervalMs <= 0
- ? TimeUnit.HOURS.toMillis(ROUTINE_INTERVAL_HOUR.getDefaultValue())
- : TimeUnit.HOURS.toMillis(routineIntervalMs);
- mEpgDataExpiredTimeLimitMs = routineIntervalMs * 2;
- mFastFetchDurationSec = FAST_FETCH_DURATION_SEC + routineIntervalMs / 1000;
+ mEpgInputWhiteList = epgInputWhiteList;
+ mBackendKnobsFlags = backendKnobsFlags;
+ mBuildType = buildType;
+ }
+
+ private long getFastFetchDurationSec() {
+ return FAST_FETCH_DURATION_SEC + getRoutineIntervalMs() / 1000;
+ }
+
+ private long getEpgDataExpiredTimeLimitMs() {
+ return getRoutineIntervalMs() * 2;
+ }
+
+ private long getRoutineIntervalMs() {
+ long routineIntervalHours = mBackendKnobsFlags.epgFetcherIntervalHour();
+ return routineIntervalHours <= 0
+ ? TimeUnit.HOURS.toMillis(DEFAULT_ROUTINE_INTERVAL_HOUR)
+ : TimeUnit.HOURS.toMillis(routineIntervalHours);
}
private static Set<Channel> getExistingChannelsForMyPackage(Context context) {
@@ -214,7 +233,7 @@ public class EpgFetcherImpl implements EpgFetcher {
new JobInfo.Builder(
EPG_ROUTINELY_FETCHING_JOB_ID,
new ComponentName(mContext, EpgFetchService.class))
- .setPeriodic(mRoutineIntervalMs)
+ .setPeriodic(getRoutineIntervalMs())
.setBackoffCriteria(INITIAL_BACKOFF_MS, JobInfo.BACKOFF_POLICY_EXPONENTIAL)
.setPersisted(true)
.build();
@@ -238,7 +257,7 @@ public class EpgFetcherImpl implements EpgFetcher {
@Override
protected void onPostExecute(Long result) {
if (mClock.currentTimeMillis() - EpgFetchHelper.getLastEpgUpdatedTimestamp(mContext)
- > mEpgDataExpiredTimeLimitMs) {
+ > getEpgDataExpiredTimeLimitMs()) {
Log.i(TAG, "EPG data expired. Start fetching immediately.");
fetchImmediately();
}
@@ -346,6 +365,19 @@ public class EpgFetcherImpl implements EpgFetcher {
if (DEBUG) Log.d(TAG, "Cannot start routine service: scanning channels.");
return false;
}
+ if (!TvFeatures.CLOUD_EPG_FOR_3RD_PARTY.isEnabled(mContext)
+ && mBuildType != HasBuildType.BuildType.AOSP) {
+ if (getTunerChannelCount() == 0) {
+ if (DEBUG) Log.d(TAG, "Cannot start routine service: no internal tuner channels.");
+ return false;
+ }
+ if (!TextUtils.isEmpty(EpgFetchHelper.getLastLineupId(mContext))) {
+ return true;
+ }
+ if (!TextUtils.isEmpty(PostalCodeUtils.getLastPostalCode(mContext))) {
+ return true;
+ }
+ }
return true;
}
@@ -505,6 +537,17 @@ public class EpgFetcherImpl implements EpgFetcher {
return numbers.size();
}
+ private boolean isInputInWhiteList(EpgInput epgInput) {
+ if (mBuildType == HasBuildType.BuildType.AOSP) {
+ return false;
+ }
+ return (BuildConfig.ENG
+ && epgInput.getInputId()
+ .equals(
+ "com.example.partnersupportsampletvinput/.SampleTvInputService"))
+ || mEpgInputWhiteList.isInputWhiteListed(epgInput.getInputId());
+ }
+
@VisibleForTesting
class FetchAsyncTask extends AsyncTask<Void, Void, Integer> {
private final JobService mService;
@@ -532,12 +575,45 @@ public class EpgFetcherImpl implements EpgFetcher {
Integer builtInResult = fetchEpgForBuiltInTuner();
boolean anyCloudEpgFailure = false;
boolean anyCloudEpgSuccess = false;
+ if (TvFeatures.CLOUD_EPG_FOR_3RD_PARTY.isEnabled(mContext)
+ && mBuildType != HasBuildType.BuildType.AOSP) {
+ for (EpgInput epgInput : getEpgInputs()) {
+ if (DEBUG) Log.d(TAG, "Start EPG fetch for " + epgInput);
+ if (isCancelled()) {
+ break;
+ }
+ if (isInputInWhiteList(epgInput)) {
+ // TODO(b/66191312) check timestamp and result code and decide if update
+ // is needed.
+ Set<Channel> channels = getExistingChannelsFor(epgInput.getInputId());
+ Integer result = fetchEpgFor(epgInput.getLineupId(), channels);
+ anyCloudEpgFailure = anyCloudEpgFailure || result != null;
+ anyCloudEpgSuccess = anyCloudEpgSuccess || result == null;
+ updateCloudEpgInput(epgInput, result);
+ } else {
+ Log.w(
+ TAG,
+ "Fetching the EPG for "
+ + epgInput.getInputId()
+ + " is not supported.");
+ }
+ }
+ }
+ if (builtInResult == null || builtInResult == REASON_NO_BUILT_IN_CHANNELS) {
+ return anyCloudEpgFailure
+ ? ((Integer) REASON_CLOUD_EPG_FAILURE)
+ : anyCloudEpgSuccess ? null : builtInResult;
+ }
return builtInResult;
} finally {
TrafficStats.setThreadStatsTag(oldTag);
}
}
+ private void updateCloudEpgInput(EpgInput unusedEpgInput, Integer unusedResult) {
+ // TODO(b/66191312) write the result and timestamp to the input table
+ }
+
private Set<Channel> getExistingChannelsFor(String inputId) {
Set<Channel> result = new HashSet<>();
try (Cursor cursor =
@@ -548,13 +624,24 @@ public class EpgFetcherImpl implements EpgFetcher {
null,
null,
null)) {
- while (cursor.moveToNext()) {
- result.add(ChannelImpl.fromCursor(cursor));
+ if (cursor != null) {
+ while (cursor.moveToNext()) {
+ result.add(ChannelImpl.fromCursor(cursor));
+ }
}
return result;
}
}
+ private Set<EpgInput> getEpgInputs() {
+ if (mBuildType == HasBuildType.BuildType.AOSP) {
+ return ImmutableSet.of();
+ }
+ Set<EpgInput> epgInputs = EpgInputs.queryEpgInputs(mContext.getContentResolver());
+ if (DEBUG) Log.d(TAG, "getEpgInputs " + epgInputs);
+ return epgInputs;
+ }
+
private Integer fetchEpgForBuiltInTuner() {
try {
Integer failureReason = prepareFetchEpg(false);
@@ -606,19 +693,16 @@ public class EpgFetcherImpl implements EpgFetcher {
Log.i(TAG, "Failed to get EPG channels for " + lineupId);
return REASON_NO_EPG_DATA_RETURNED;
}
+ EpgFetchHelper.updateNetworkAffiliation(mContext, channels);
if (mClock.currentTimeMillis() - EpgFetchHelper.getLastEpgUpdatedTimestamp(mContext)
- > mEpgDataExpiredTimeLimitMs) {
- batchFetchEpg(channels, mFastFetchDurationSec);
+ > getEpgDataExpiredTimeLimitMs()) {
+ batchFetchEpg(channels, getFastFetchDurationSec());
}
new Handler(mContext.getMainLooper())
.post(
- new Runnable() {
- @Override
- public void run() {
+ () ->
ChannelLogoFetcher.startFetchingChannelLogos(
- mContext, asChannelList(channels));
- }
- });
+ mContext, asChannelList(channels)));
for (EpgReader.EpgChannel epgChannel : channels) {
if (this.isCancelled()) {
return null;
@@ -780,6 +864,9 @@ public class EpgFetcherImpl implements EpgFetcher {
mFetchedChannelIdsDuringScan.add(epgChannel.getChannel().getId());
}
}
+ if (!newChannels.isEmpty()) {
+ EpgFetchHelper.updateNetworkAffiliation(mContext, newChannels);
+ }
batchFetchEpg(newChannels, FETCH_DURING_SCAN_DURATION_SEC);
}
@@ -798,14 +885,7 @@ public class EpgFetcherImpl implements EpgFetcher {
// Clear timestamp to make routine service start right away.
EpgFetchHelper.setLastEpgUpdatedTimestamp(mContext, 0);
Log.i(TAG, "EPG Fetching during channel scanning finished.");
- new Handler(Looper.getMainLooper())
- .post(
- new Runnable() {
- @Override
- public void run() {
- fetchImmediately();
- }
- });
+ new Handler(Looper.getMainLooper()).post(EpgFetcherImpl.this::fetchImmediately);
}
}
}
diff --git a/src/com/android/tv/data/epg/EpgInputWhiteList.java b/src/com/android/tv/data/epg/EpgInputWhiteList.java
index eada8b24..24b4fe3d 100644
--- a/src/com/android/tv/data/epg/EpgInputWhiteList.java
+++ b/src/com/android/tv/data/epg/EpgInputWhiteList.java
@@ -21,8 +21,8 @@ import android.support.annotation.VisibleForTesting;
import android.text.TextUtils;
import android.util.Log;
import com.android.tv.common.BuildConfig;
-import com.android.tv.common.config.api.RemoteConfig;
import com.android.tv.common.experiments.Experiments;
+import com.android.tv.common.flags.CloudEpgFlags;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
@@ -33,7 +33,6 @@ import java.util.Set;
public final class EpgInputWhiteList {
private static final boolean DEBUG = false;
private static final String TAG = "EpgInputWhiteList";
- @VisibleForTesting public static final String KEY = "live_channels_3rd_party_epg_inputs";
private static final String QA_DEV_INPUTS =
"com.example.partnersupportsampletvinput/.SampleTvInputService,"
+ "com.android.tv.tuner.sample.dvb/.tvinput.SampleDvbTunerTvInputService";
@@ -44,10 +43,10 @@ public final class EpgInputWhiteList {
return inputId == null ? null : inputId.substring(0, inputId.indexOf("/"));
}
- private final RemoteConfig remoteConfig;
+ private final CloudEpgFlags cloudEpgFlags;
- public EpgInputWhiteList(RemoteConfig remoteConfig) {
- this.remoteConfig = remoteConfig;
+ public EpgInputWhiteList(CloudEpgFlags cloudEpgFlags) {
+ this.cloudEpgFlags = cloudEpgFlags;
}
public boolean isInputWhiteListed(String inputId) {
@@ -72,7 +71,7 @@ public final class EpgInputWhiteList {
}
private Set<String> getWhiteListedInputs() {
- Set<String> result = toInputSet(remoteConfig.getString(KEY));
+ Set<String> result = toInputSet(cloudEpgFlags.thirdPartyEpgInputsCsv());
if (BuildConfig.ENG || Experiments.ENABLE_QA_FEATURES.get()) {
HashSet<String> moreInputs = new HashSet<>(toInputSet(QA_DEV_INPUTS));
if (result.isEmpty()) {
diff --git a/src/com/android/tv/data/epg/EpgReader.java b/src/com/android/tv/data/epg/EpgReader.java
index 7147905a..c9fcd979 100644
--- a/src/com/android/tv/data/epg/EpgReader.java
+++ b/src/com/android/tv/data/epg/EpgReader.java
@@ -23,6 +23,7 @@ import com.android.tv.data.Lineup;
import com.android.tv.data.Program;
import com.android.tv.data.api.Channel;
import com.android.tv.dvr.data.SeriesInfo;
+import com.google.auto.value.AutoValue;
import java.util.Collection;
import java.util.List;
import java.util.Map;
@@ -33,15 +34,18 @@ import java.util.Set;
public interface EpgReader {
/** Value class that holds a EpgChannelId and its corresponding {@link Channel} */
- // TODO(b/72052568): Get autovalue to work in aosp master
+ @AutoValue
abstract class EpgChannel {
- public static EpgChannel createEpgChannel(Channel channel, String epgChannelId) {
- return new AutoValue_EpgReader_EpgChannel(channel, epgChannelId);
+ public static EpgChannel createEpgChannel(Channel channel, String epgChannelId,
+ boolean dbUpdateNeeded) {
+ return new AutoValue_EpgReader_EpgChannel(channel, epgChannelId, dbUpdateNeeded);
}
public abstract Channel getChannel();
public abstract String getEpgChannelId();
+
+ public abstract boolean getDbUpdateNeeded();
}
/** Checks if the reader is available. */
diff --git a/src/com/android/tv/dialog/PinDialogFragment.java b/src/com/android/tv/dialog/PinDialogFragment.java
index 71f45fbe..87308093 100644
--- a/src/com/android/tv/dialog/PinDialogFragment.java
+++ b/src/com/android/tv/dialog/PinDialogFragment.java
@@ -16,37 +16,26 @@
package com.android.tv.dialog;
-import android.animation.Animator;
-import android.animation.AnimatorInflater;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
-import android.animation.ValueAnimator;
import android.app.ActivityManager;
import android.app.Dialog;
-import android.content.Context;
import android.content.DialogInterface;
import android.content.SharedPreferences;
-import android.content.res.Resources;
import android.media.tv.TvContentRating;
import android.os.Bundle;
import android.os.Handler;
import android.preference.PreferenceManager;
import android.text.TextUtils;
-import android.util.AttributeSet;
import android.util.Log;
-import android.util.TypedValue;
-import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
-import android.widget.FrameLayout;
import android.widget.TextView;
import android.widget.Toast;
import com.android.tv.R;
import com.android.tv.TvSingletons;
import com.android.tv.common.SoftPreconditions;
+import com.android.tv.dialog.picker.PinPicker;
import com.android.tv.util.TvSettings;
public class PinDialogFragment extends SafeDismissDialogFragment {
@@ -77,17 +66,12 @@ public class PinDialogFragment extends SafeDismissDialogFragment {
private static final int MAX_WRONG_PIN_COUNT = 5;
private static final int DISABLE_PIN_DURATION_MILLIS = 60 * 1000; // 1 minute
- private static final String INITIAL_TEXT = "—";
private static final String TRACKER_LABEL = "Pin dialog";
private static final String ARGS_TYPE = "args_type";
private static final String ARGS_RATING = "args_rating";
public static final String DIALOG_TAG = PinDialogFragment.class.getName();
- private static final int NUMBER_PICKERS_RES_ID[] = {
- R.id.first, R.id.second, R.id.third, R.id.fourth
- };
-
private int mType;
private int mRequestType;
private boolean mPinChecked;
@@ -96,7 +80,7 @@ public class PinDialogFragment extends SafeDismissDialogFragment {
private TextView mWrongPinView;
private View mEnterPinView;
private TextView mTitleView;
- private PinNumberPicker[] mPickers;
+ private PinPicker mPicker;
private SharedPreferences mSharedPreferences;
private String mPrevPin;
private String mPin;
@@ -140,7 +124,6 @@ public class PinDialogFragment extends SafeDismissDialogFragment {
public Dialog onCreateDialog(Bundle savedInstanceState) {
Dialog dlg = super.onCreateDialog(savedInstanceState);
dlg.getWindow().getAttributes().windowAnimations = R.style.pin_dialog_animation;
- PinNumberPicker.loadResources(dlg.getContext());
return dlg;
}
@@ -171,6 +154,14 @@ public class PinDialogFragment extends SafeDismissDialogFragment {
mWrongPinView = (TextView) v.findViewById(R.id.wrong_pin);
mEnterPinView = v.findViewById(R.id.enter_pin);
mTitleView = (TextView) mEnterPinView.findViewById(R.id.title);
+ mPicker = v.findViewById(R.id.pin_picker);
+ mPicker.setOnClickListener(
+ view -> {
+ String pin = getPinInput();
+ if (!TextUtils.isEmpty(pin)) {
+ done(pin);
+ }
+ });
if (TextUtils.isEmpty(getPin())) {
// If PIN isn't set, user should set a PIN.
// Successfully setting a new set is considered as entering correct PIN.
@@ -210,31 +201,13 @@ public class PinDialogFragment extends SafeDismissDialogFragment {
}
}
- mPickers = new PinNumberPicker[NUMBER_PICKERS_RES_ID.length];
- for (int i = 0; i < NUMBER_PICKERS_RES_ID.length; i++) {
- mPickers[i] = (PinNumberPicker) v.findViewById(NUMBER_PICKERS_RES_ID[i]);
- mPickers[i].setValueRangeAndResetText(0, 9);
- mPickers[i].setPinDialogFragment(this);
- mPickers[i].updateFocus(false);
- }
- for (int i = 0; i < NUMBER_PICKERS_RES_ID.length - 1; i++) {
- mPickers[i].setNextNumberPicker(mPickers[i + 1]);
- }
-
if (mType != PIN_DIALOG_TYPE_NEW_PIN) {
updateWrongPin();
}
+ mPicker.requestFocus();
return v;
}
- private final Runnable mUpdateEnterPinRunnable =
- new Runnable() {
- @Override
- public void run() {
- updateWrongPin();
- }
- };
-
private void updateWrongPin() {
if (getActivity() == null) {
// The activity is already detached. No need to update.
@@ -257,7 +230,8 @@ public class PinDialogFragment extends SafeDismissDialogFragment {
R.plurals.pin_enter_countdown,
remainingSeconds,
remainingSeconds));
- mHandler.postDelayed(mUpdateEnterPinRunnable, 1000);
+
+ mHandler.postDelayed(this::updateWrongPin, 1000);
}
}
@@ -364,383 +338,11 @@ public class PinDialogFragment extends SafeDismissDialogFragment {
}
private String getPinInput() {
- String result = "";
- try {
- for (PinNumberPicker pnp : mPickers) {
- pnp.updateText();
- result += pnp.getValue();
- }
- } catch (IllegalStateException e) {
- result = "";
- }
- return result;
+ return mPicker.getPinInput();
}
private void resetPinInput() {
- for (PinNumberPicker pnp : mPickers) {
- pnp.setValueRangeAndResetText(0, 9);
- }
- mPickers[0].requestFocus();
- }
-
- public static class PinNumberPicker extends FrameLayout {
- private static final int NUMBER_VIEWS_RES_ID[] = {
- R.id.previous2_number,
- R.id.previous_number,
- R.id.current_number,
- R.id.next_number,
- R.id.next2_number
- };
- private static final int CURRENT_NUMBER_VIEW_INDEX = 2;
- private static final int NOT_INITIALIZED = Integer.MIN_VALUE;
-
- private static Animator sFocusedNumberEnterAnimator;
- private static Animator sFocusedNumberExitAnimator;
- private static Animator sAdjacentNumberEnterAnimator;
- private static Animator sAdjacentNumberExitAnimator;
-
- private static float sAlphaForFocusedNumber;
- private static float sAlphaForAdjacentNumber;
-
- private int mMinValue;
- private int mMaxValue;
- private int mCurrentValue;
- // a value for setting mCurrentValue at the end of scroll animation.
- private int mNextValue;
- private final int mNumberViewHeight;
- private PinDialogFragment mDialog;
- private PinNumberPicker mNextNumberPicker;
- private boolean mCancelAnimation;
-
- private final View mNumberViewHolder;
- // When the PinNumberPicker has focus, mBackgroundView will show the focused background.
- // Also, this view is used for handling the text change animation of the current number
- // view which is required when the current number view text is changing from INITIAL_TEXT
- // to "0".
- private final TextView mBackgroundView;
- private final TextView[] mNumberViews;
- private final AnimatorSet mFocusGainAnimator;
- private final AnimatorSet mFocusLossAnimator;
- private final AnimatorSet mScrollAnimatorSet;
-
- public PinNumberPicker(Context context) {
- this(context, null);
- }
-
- public PinNumberPicker(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public PinNumberPicker(Context context, AttributeSet attrs, int defStyleAttr) {
- this(context, attrs, defStyleAttr, 0);
- }
-
- public PinNumberPicker(
- Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
- super(context, attrs, defStyleAttr, defStyleRes);
- View view = inflate(context, R.layout.pin_number_picker, this);
- mNumberViewHolder = view.findViewById(R.id.number_view_holder);
- mBackgroundView = (TextView) view.findViewById(R.id.focused_background);
- mNumberViews = new TextView[NUMBER_VIEWS_RES_ID.length];
- for (int i = 0; i < NUMBER_VIEWS_RES_ID.length; ++i) {
- mNumberViews[i] = (TextView) view.findViewById(NUMBER_VIEWS_RES_ID[i]);
- }
- Resources resources = context.getResources();
- mNumberViewHeight =
- resources.getDimensionPixelSize(R.dimen.pin_number_picker_text_view_height);
-
- mNumberViewHolder.setOnFocusChangeListener(
- new OnFocusChangeListener() {
- @Override
- public void onFocusChange(View v, boolean hasFocus) {
- updateFocus(true);
- }
- });
-
- mNumberViewHolder.setOnKeyListener(
- new OnKeyListener() {
- @Override
- public boolean onKey(View v, int keyCode, KeyEvent event) {
- if (event.getAction() == KeyEvent.ACTION_DOWN) {
- switch (keyCode) {
- case KeyEvent.KEYCODE_DPAD_UP:
- case KeyEvent.KEYCODE_DPAD_DOWN:
- {
- if (mCancelAnimation) {
- mScrollAnimatorSet.end();
- }
- if (!mScrollAnimatorSet.isRunning()) {
- mCancelAnimation = false;
- if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {
- mNextValue =
- adjustValueInValidRange(
- mCurrentValue + 1);
- startScrollAnimation(true);
- } else {
- mNextValue =
- adjustValueInValidRange(
- mCurrentValue - 1);
- startScrollAnimation(false);
- }
- }
- return true;
- }
- }
- } else if (event.getAction() == KeyEvent.ACTION_UP) {
- switch (keyCode) {
- case KeyEvent.KEYCODE_DPAD_UP:
- case KeyEvent.KEYCODE_DPAD_DOWN:
- {
- mCancelAnimation = true;
- return true;
- }
- }
- }
- return false;
- }
- });
- mNumberViewHolder.setScrollY(mNumberViewHeight);
-
- mFocusGainAnimator = new AnimatorSet();
- mFocusGainAnimator.playTogether(
- ObjectAnimator.ofFloat(
- mNumberViews[CURRENT_NUMBER_VIEW_INDEX - 1],
- "alpha",
- 0f,
- sAlphaForAdjacentNumber),
- ObjectAnimator.ofFloat(
- mNumberViews[CURRENT_NUMBER_VIEW_INDEX],
- "alpha",
- sAlphaForFocusedNumber,
- 0f),
- ObjectAnimator.ofFloat(
- mNumberViews[CURRENT_NUMBER_VIEW_INDEX + 1],
- "alpha",
- 0f,
- sAlphaForAdjacentNumber),
- ObjectAnimator.ofFloat(mBackgroundView, "alpha", 0f, 1f));
- mFocusGainAnimator.setDuration(
- context.getResources().getInteger(android.R.integer.config_shortAnimTime));
- mFocusGainAnimator.addListener(
- new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animator) {
- mNumberViews[CURRENT_NUMBER_VIEW_INDEX].setText(
- mBackgroundView.getText());
- mNumberViews[CURRENT_NUMBER_VIEW_INDEX].setAlpha(
- sAlphaForFocusedNumber);
- mBackgroundView.setText("");
- }
- });
-
- mFocusLossAnimator = new AnimatorSet();
- mFocusLossAnimator.playTogether(
- ObjectAnimator.ofFloat(
- mNumberViews[CURRENT_NUMBER_VIEW_INDEX - 1],
- "alpha",
- sAlphaForAdjacentNumber,
- 0f),
- ObjectAnimator.ofFloat(
- mNumberViews[CURRENT_NUMBER_VIEW_INDEX + 1],
- "alpha",
- sAlphaForAdjacentNumber,
- 0f),
- ObjectAnimator.ofFloat(mBackgroundView, "alpha", 1f, 0f));
- mFocusLossAnimator.setDuration(
- context.getResources().getInteger(android.R.integer.config_shortAnimTime));
-
- mScrollAnimatorSet = new AnimatorSet();
- mScrollAnimatorSet.setDuration(
- context.getResources().getInteger(R.integer.pin_number_scroll_duration));
- mScrollAnimatorSet.addListener(
- new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- // Set mCurrent value when scroll animation is finished.
- mCurrentValue = mNextValue;
- updateText();
- mNumberViewHolder.setScrollY(mNumberViewHeight);
- mNumberViews[CURRENT_NUMBER_VIEW_INDEX - 1].setAlpha(
- sAlphaForAdjacentNumber);
- mNumberViews[CURRENT_NUMBER_VIEW_INDEX].setAlpha(
- sAlphaForFocusedNumber);
- mNumberViews[CURRENT_NUMBER_VIEW_INDEX + 1].setAlpha(
- sAlphaForAdjacentNumber);
- }
- });
- }
-
- static void loadResources(Context context) {
- if (sFocusedNumberEnterAnimator == null) {
- TypedValue outValue = new TypedValue();
- context.getResources()
- .getValue(R.dimen.pin_alpha_for_focused_number, outValue, true);
- sAlphaForFocusedNumber = outValue.getFloat();
- context.getResources()
- .getValue(R.dimen.pin_alpha_for_adjacent_number, outValue, true);
- sAlphaForAdjacentNumber = outValue.getFloat();
-
- sFocusedNumberEnterAnimator =
- AnimatorInflater.loadAnimator(context, R.animator.pin_focused_number_enter);
- sFocusedNumberExitAnimator =
- AnimatorInflater.loadAnimator(context, R.animator.pin_focused_number_exit);
- sAdjacentNumberEnterAnimator =
- AnimatorInflater.loadAnimator(
- context, R.animator.pin_adjacent_number_enter);
- sAdjacentNumberExitAnimator =
- AnimatorInflater.loadAnimator(context, R.animator.pin_adjacent_number_exit);
- }
- }
-
- @Override
- public boolean dispatchKeyEvent(KeyEvent event) {
- if (event.getAction() == KeyEvent.ACTION_UP) {
- int keyCode = event.getKeyCode();
- if (keyCode >= KeyEvent.KEYCODE_0 && keyCode <= KeyEvent.KEYCODE_9) {
- mNextValue = adjustValueInValidRange(keyCode - KeyEvent.KEYCODE_0);
- updateFocus(false);
- } else if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER
- || keyCode == KeyEvent.KEYCODE_ENTER) {
- if (mNextNumberPicker == null) {
- String pin = mDialog.getPinInput();
- if (!TextUtils.isEmpty(pin)) {
- mDialog.done(pin);
- }
- } else {
- mNextNumberPicker.requestFocus();
- }
- return true;
- }
- }
- return super.dispatchKeyEvent(event);
- }
-
- void startScrollAnimation(boolean scrollUp) {
- mFocusGainAnimator.end();
- mFocusLossAnimator.end();
- final ValueAnimator scrollAnimator =
- ValueAnimator.ofInt(0, scrollUp ? mNumberViewHeight : -mNumberViewHeight);
- scrollAnimator.addUpdateListener(
- new ValueAnimator.AnimatorUpdateListener() {
- @Override
- public void onAnimationUpdate(ValueAnimator animation) {
- int value = (Integer) animation.getAnimatedValue();
- mNumberViewHolder.setScrollY(value + mNumberViewHeight);
- }
- });
- scrollAnimator.setDuration(
- getResources().getInteger(R.integer.pin_number_scroll_duration));
-
- if (scrollUp) {
- sAdjacentNumberExitAnimator.setTarget(mNumberViews[CURRENT_NUMBER_VIEW_INDEX - 1]);
- sFocusedNumberExitAnimator.setTarget(mNumberViews[CURRENT_NUMBER_VIEW_INDEX]);
- sFocusedNumberEnterAnimator.setTarget(mNumberViews[CURRENT_NUMBER_VIEW_INDEX + 1]);
- sAdjacentNumberEnterAnimator.setTarget(mNumberViews[CURRENT_NUMBER_VIEW_INDEX + 2]);
- } else {
- sAdjacentNumberEnterAnimator.setTarget(mNumberViews[CURRENT_NUMBER_VIEW_INDEX - 2]);
- sFocusedNumberEnterAnimator.setTarget(mNumberViews[CURRENT_NUMBER_VIEW_INDEX - 1]);
- sFocusedNumberExitAnimator.setTarget(mNumberViews[CURRENT_NUMBER_VIEW_INDEX]);
- sAdjacentNumberExitAnimator.setTarget(mNumberViews[CURRENT_NUMBER_VIEW_INDEX + 1]);
- }
-
- mScrollAnimatorSet.playTogether(
- scrollAnimator,
- sAdjacentNumberExitAnimator,
- sFocusedNumberExitAnimator,
- sFocusedNumberEnterAnimator,
- sAdjacentNumberEnterAnimator);
- mScrollAnimatorSet.start();
- }
-
- void setValueRangeAndResetText(int min, int max) {
- if (min > max) {
- throw new IllegalArgumentException(
- "The min value should be greater than or equal to the max value");
- } else if (min == NOT_INITIALIZED) {
- throw new IllegalArgumentException(
- "The min value should be greater than Integer.MIN_VALUE.");
- }
- mMinValue = min;
- mMaxValue = max;
- mNextValue = mCurrentValue = NOT_INITIALIZED;
- for (int i = 0; i < NUMBER_VIEWS_RES_ID.length; ++i) {
- mNumberViews[i].setText(i == CURRENT_NUMBER_VIEW_INDEX ? INITIAL_TEXT : "");
- }
- mBackgroundView.setText(INITIAL_TEXT);
- }
-
- void setPinDialogFragment(PinDialogFragment dlg) {
- mDialog = dlg;
- }
-
- void setNextNumberPicker(PinNumberPicker picker) {
- mNextNumberPicker = picker;
- }
-
- int getValue() {
- if (mCurrentValue < mMinValue || mCurrentValue > mMaxValue) {
- throw new IllegalStateException("Value is not set");
- }
- return mCurrentValue;
- }
-
- void updateFocus(boolean withAnimation) {
- mScrollAnimatorSet.end();
- mFocusGainAnimator.end();
- mFocusLossAnimator.end();
- updateText();
- if (mNumberViewHolder.isFocused()) {
- if (withAnimation) {
- mBackgroundView.setText(String.valueOf(mCurrentValue));
- mFocusGainAnimator.start();
- } else {
- mBackgroundView.setAlpha(1f);
- mNumberViews[CURRENT_NUMBER_VIEW_INDEX - 1].setAlpha(sAlphaForAdjacentNumber);
- mNumberViews[CURRENT_NUMBER_VIEW_INDEX + 1].setAlpha(sAlphaForAdjacentNumber);
- }
- } else {
- if (withAnimation) {
- mFocusLossAnimator.start();
- } else {
- mBackgroundView.setAlpha(0f);
- mNumberViews[CURRENT_NUMBER_VIEW_INDEX - 1].setAlpha(0f);
- mNumberViews[CURRENT_NUMBER_VIEW_INDEX + 1].setAlpha(0f);
- }
- mNumberViewHolder.setScrollY(mNumberViewHeight);
- }
- }
-
- private void updateText() {
- boolean wasNotInitialized = false;
- if (mNumberViewHolder.isFocused() && mCurrentValue == NOT_INITIALIZED) {
- mNextValue = mCurrentValue = mMinValue;
- wasNotInitialized = true;
- }
- if (mCurrentValue >= mMinValue && mCurrentValue <= mMaxValue) {
- for (int i = 0; i < NUMBER_VIEWS_RES_ID.length; ++i) {
- if (wasNotInitialized && i == CURRENT_NUMBER_VIEW_INDEX) {
- // In order to show the text change animation, keep the text of
- // mNumberViews[CURRENT_NUMBER_VIEW_INDEX].
- } else {
- mNumberViews[i].setText(
- String.valueOf(
- adjustValueInValidRange(
- mCurrentValue - CURRENT_NUMBER_VIEW_INDEX + i)));
- }
- }
- }
- }
-
- private int adjustValueInValidRange(int value) {
- int interval = mMaxValue - mMinValue + 1;
- if (value < mMinValue - interval || value > mMaxValue + interval) {
- throw new IllegalArgumentException(
- "The value( " + value + ") is too small or too big to adjust");
- }
- return (value < mMinValue)
- ? value + interval
- : (value > mMaxValue) ? value - interval : value;
- }
+ mPicker.resetPinInput();
}
/**
diff --git a/src/com/android/tv/dialog/picker/PinPicker.java b/src/com/android/tv/dialog/picker/PinPicker.java
new file mode 100644
index 00000000..f501dfd1
--- /dev/null
+++ b/src/com/android/tv/dialog/picker/PinPicker.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.dialog.picker;
+
+import android.content.Context;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.support.v17.leanback.widget.picker.Picker;
+import android.support.v17.leanback.widget.picker.PickerColumn;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import java.util.ArrayList;
+import java.util.List;
+
+/** 4 digit picker */
+public final class PinPicker extends Picker {
+ // TODO(b/116144491): use leanback pin picker.
+
+ private final List<PickerColumn> mPickers = new ArrayList<>();
+ private OnClickListener mOnClickListener;
+
+ // the version of picker I link to does not have this constructor
+ public PinPicker(Context context, AttributeSet attributeSet) {
+ this(context, attributeSet, 0);
+ }
+
+ public PinPicker(Context context, AttributeSet attributeSet, int defStyleAttr) {
+ super(context, attributeSet, defStyleAttr);
+
+ for (int i = 0; i < 4; i++) {
+ PickerColumn pickerColumn = new PickerColumn();
+ pickerColumn.setMinValue(0);
+ pickerColumn.setMaxValue(9);
+ pickerColumn.setLabelFormat("%d");
+ mPickers.add(pickerColumn);
+ }
+ setSeparator(" ");
+ setColumns(mPickers);
+ setActivated(true);
+ setFocusable(true);
+ super.setOnClickListener(this::onClick);
+ }
+
+ public String getPinInput() {
+ String result = "";
+ try {
+ for (PickerColumn column : mPickers) {
+
+ result += column.getCurrentValue();
+ }
+ } catch (IllegalStateException e) {
+ result = "";
+ }
+ return result;
+ }
+
+ @Override
+ public void setOnClickListener(@Nullable OnClickListener l) {
+ mOnClickListener = l;
+ }
+
+ private void onClick(View v) {
+ int selectedColumn = getSelectedColumn();
+ int nextColumn = selectedColumn + 1;
+ // Only call the click listener if we are on the last column
+ // Otherwise move to the next column
+ if (nextColumn == getColumnsCount()) {
+ if (mOnClickListener != null) {
+ mOnClickListener.onClick(v);
+ }
+ } else {
+ setSelectedColumn(nextColumn);
+ onRequestFocusInDescendants(ViewGroup.FOCUS_FORWARD, null);
+ }
+ }
+
+ public void resetPinInput() {
+ setActivated(false);
+ for (int i = 0; i < 4; i++) {
+ setColumnValue(i, 0, true);
+ }
+ setSelectedColumn(0);
+ setActivated(true); // This resets the focus
+ }
+
+ @Override
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ if (event.getAction() == KeyEvent.ACTION_UP) {
+ int keyCode = event.getKeyCode();
+ int digit = digitFromKeyCode(keyCode);
+ if (digit != -1) {
+ int selectedColumn = getSelectedColumn();
+ setColumnValue(selectedColumn, digit, false);
+ int nextColumn = selectedColumn + 1;
+ if (nextColumn < getColumnsCount()) {
+ setSelectedColumn(nextColumn);
+ onRequestFocusInDescendants(ViewGroup.FOCUS_FORWARD, null);
+ } else {
+ callOnClick();
+ }
+ return true;
+ }
+ }
+ return super.dispatchKeyEvent(event);
+ }
+
+ @VisibleForTesting
+ static int digitFromKeyCode(int keyCode) {
+ if (keyCode >= KeyEvent.KEYCODE_0 && keyCode <= KeyEvent.KEYCODE_9) {
+ return keyCode - KeyEvent.KEYCODE_0;
+ } else if (keyCode >= KeyEvent.KEYCODE_NUMPAD_0 && keyCode <= KeyEvent.KEYCODE_NUMPAD_9) {
+ return keyCode - KeyEvent.KEYCODE_NUMPAD_0;
+ }
+ return -1;
+ }
+}
diff --git a/src/com/android/tv/dvr/DvrDataManagerImpl.java b/src/com/android/tv/dvr/DvrDataManagerImpl.java
index 2b4ecbf5..0053650b 100644
--- a/src/com/android/tv/dvr/DvrDataManagerImpl.java
+++ b/src/com/android/tv/dvr/DvrDataManagerImpl.java
@@ -16,7 +16,6 @@
package com.android.tv.dvr;
-import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.ContentResolver;
import android.content.ContentUris;
@@ -49,21 +48,23 @@ 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;
-import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncDeleteSeriesRecordingTask;
-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.DvrDbFuture.AddScheduleFuture;
+import com.android.tv.dvr.provider.DvrDbFuture.AddSeriesRecordingFuture;
+import com.android.tv.dvr.provider.DvrDbFuture.DeleteScheduleFuture;
+import com.android.tv.dvr.provider.DvrDbFuture.DeleteSeriesRecordingFuture;
+import com.android.tv.dvr.provider.DvrDbFuture.DvrQueryScheduleFuture;
+import com.android.tv.dvr.provider.DvrDbFuture.DvrQuerySeriesRecordingFuture;
+import com.android.tv.dvr.provider.DvrDbFuture.UpdateScheduleFuture;
+import com.android.tv.dvr.provider.DvrDbFuture.UpdateSeriesRecordingFuture;
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.Filter;
import com.android.tv.util.TvInputManagerHelper;
import com.android.tv.util.TvUriMatcher;
+import com.google.common.base.Predicate;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.ListenableFuture;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@@ -73,6 +74,7 @@ import java.util.List;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.Executor;
+import java.util.concurrent.Future;
/** DVR Data manager to handle recordings and schedules. */
@MainThread
@@ -106,8 +108,7 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
@Override
public void onChange(boolean selfChange, final @Nullable Uri uri) {
- RecordedProgramsQueryTask task =
- new RecordedProgramsQueryTask(mContext.getContentResolver(), uri);
+ RecordedProgramsQueryTask task = new RecordedProgramsQueryTask(uri);
task.executeOnDbThread();
mPendingTasks.add(task);
}
@@ -116,6 +117,9 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
private boolean mDvrLoadFinished;
private boolean mRecordedProgramLoadFinished;
private final Set<AsyncTask> mPendingTasks = new ArraySet<>();
+ private final Set<Future> mPendingDvrFuture = new ArraySet<>();
+ // TODO(b/79207567) make sure Future is not stopped at writing.
+ private final Set<Future> mNoStopFuture = new ArraySet<>();
private DvrDbSync mDbSync;
private RecordingStorageStatusManager mStorageStatusManager;
@@ -154,13 +158,27 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
}
};
+ private final FutureCallback<Void> removeFromSetOnCompletion =
+ new FutureCallback<Void>() {
+ @Override
+ public void onSuccess(Void result) {
+ mNoStopFuture.remove(this);
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ Log.w(TAG, "Failed to execute.", t);
+ mNoStopFuture.remove(this);
+ }
+ };
+
private static <T> List<T> moveElements(
- HashMap<Long, T> from, HashMap<Long, T> to, Filter<T> filter) {
+ HashMap<Long, T> from, HashMap<Long, T> to, Predicate<T> filter) {
List<T> moved = new ArrayList<>();
Iterator<Entry<Long, T>> iter = from.entrySet().iterator();
while (iter.hasNext()) {
Entry<Long, T> entry = iter.next();
- if (filter.filter(entry.getValue())) {
+ if (filter.apply(entry.getValue())) {
to.put(entry.getKey(), entry.getValue());
iter.remove();
moved.add(entry.getValue());
@@ -181,134 +199,143 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
public void start() {
mInputManager.addCallback(mInputCallback);
mStorageStatusManager.addListener(mStorageMountChangedListener);
- AsyncDvrQuerySeriesRecordingTask dvrQuerySeriesRecordingTask =
- new AsyncDvrQuerySeriesRecordingTask(mContext) {
- @Override
- protected void onCancelled(List<SeriesRecording> seriesRecordings) {
- mPendingTasks.remove(this);
- }
-
- @Override
- protected void onPostExecute(List<SeriesRecording> seriesRecordings) {
- mPendingTasks.remove(this);
- long maxId = 0;
- HashSet<String> seriesIds = new HashSet<>();
- for (SeriesRecording r : seriesRecordings) {
- if (SoftPreconditions.checkState(
- !seriesIds.contains(r.getSeriesId()),
- TAG,
- "Skip loading series recording with duplicate series ID: "
- + r)) {
- seriesIds.add(r.getSeriesId());
- if (isInputAvailable(r.getInputId())) {
- mSeriesRecordings.put(r.getId(), r);
- mSeriesId2SeriesRecordings.put(r.getSeriesId(), r);
- } else {
- mSeriesRecordingsForRemovedInput.put(r.getId(), r);
+ DvrQuerySeriesRecordingFuture dvrQuerySeriesRecordingTask =
+ new DvrQuerySeriesRecordingFuture(mContext);
+ ListenableFuture<List<SeriesRecording>> dvrQuerySeriesRecordingFuture =
+ dvrQuerySeriesRecordingTask.executeOnDbThread(
+ new FutureCallback<List<SeriesRecording>>() {
+ @Override
+ public void onSuccess(List<SeriesRecording> seriesRecordings) {
+ mPendingDvrFuture.remove(this);
+ long maxId = 0;
+ HashSet<String> seriesIds = new HashSet<>();
+ for (SeriesRecording r : seriesRecordings) {
+ if (SoftPreconditions.checkState(
+ !seriesIds.contains(r.getSeriesId()),
+ TAG,
+ "Skip loading series recording with duplicate series ID: "
+ + r)) {
+ seriesIds.add(r.getSeriesId());
+ if (isInputAvailable(r.getInputId())) {
+ mSeriesRecordings.put(r.getId(), r);
+ mSeriesId2SeriesRecordings.put(r.getSeriesId(), r);
+ } else {
+ mSeriesRecordingsForRemovedInput.put(r.getId(), r);
+ }
+ }
+ if (maxId < r.getId()) {
+ maxId = r.getId();
+ }
}
+ IdGenerator.SERIES_RECORDING.setMaxId(maxId);
}
- if (maxId < r.getId()) {
- maxId = r.getId();
- }
- }
- IdGenerator.SERIES_RECORDING.setMaxId(maxId);
- }
- };
- dvrQuerySeriesRecordingTask.executeOnDbThread();
- mPendingTasks.add(dvrQuerySeriesRecordingTask);
- AsyncDvrQueryScheduleTask dvrQueryScheduleTask =
- new AsyncDvrQueryScheduleTask(mContext) {
- @Override
- protected void onCancelled(List<ScheduledRecording> scheduledRecordings) {
- mPendingTasks.remove(this);
- }
- @SuppressLint("SwitchIntDef")
- @Override
- protected void onPostExecute(List<ScheduledRecording> result) {
- mPendingTasks.remove(this);
- long maxId = 0;
- int reasonNotStarted =
- ScheduledRecording
- .FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED;
- List<ScheduledRecording> toUpdate = new ArrayList<>();
- List<ScheduledRecording> toDelete = new ArrayList<>();
- for (ScheduledRecording r : result) {
- if (!isInputAvailable(r.getInputId())) {
- mScheduledRecordingsForRemovedInput.put(r.getId(), r);
- } else if (r.getState() == ScheduledRecording.STATE_RECORDING_DELETED) {
- getDeletedScheduleMap().put(r.getProgramId(), r);
- } else {
- mScheduledRecordings.put(r.getId(), r);
- if (r.getProgramId() != ScheduledRecording.ID_NOT_SET) {
- mProgramId2ScheduledRecordings.put(r.getProgramId(), r);
- }
- // Adjust the state of the schedules before DB loading is finished.
- switch (r.getState()) {
- case ScheduledRecording.STATE_RECORDING_IN_PROGRESS:
- if (r.getEndTimeMs() <= mClock.currentTimeMillis()) {
- int reason =
- ScheduledRecording.FAILED_REASON_NOT_FINISHED;
- toUpdate.add(
- ScheduledRecording.buildFrom(r)
- .setState(
- ScheduledRecording
- .STATE_RECORDING_FAILED)
- .setFailedReason(reason)
- .build());
- } else {
- toUpdate.add(
- ScheduledRecording.buildFrom(r)
- .setState(
- ScheduledRecording
- .STATE_RECORDING_NOT_STARTED)
- .build());
+ @Override
+ public void onFailure(Throwable t) {
+ Log.w(TAG, "Failed to load series recording.", t);
+ mPendingDvrFuture.remove(this);
+ }
+ });
+ mPendingDvrFuture.add(dvrQuerySeriesRecordingFuture);
+ DvrQueryScheduleFuture dvrQueryScheduleTask = new DvrQueryScheduleFuture(mContext);
+ ListenableFuture<List<ScheduledRecording>> dvrQueryScheduleFuture =
+ dvrQueryScheduleTask.executeOnDbThread(
+ new FutureCallback<List<ScheduledRecording>>() {
+ @Override
+ public void onSuccess(List<ScheduledRecording> result) {
+ mPendingDvrFuture.remove(this);
+ long maxId = 0;
+ int reasonNotStarted =
+ ScheduledRecording
+ .FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED;
+ List<ScheduledRecording> toUpdate = new ArrayList<>();
+ List<ScheduledRecording> toDelete = new ArrayList<>();
+ for (ScheduledRecording r : result) {
+ if (!isInputAvailable(r.getInputId())) {
+ mScheduledRecordingsForRemovedInput.put(r.getId(), r);
+ } else if (r.getState()
+ == ScheduledRecording.STATE_RECORDING_DELETED) {
+ getDeletedScheduleMap().put(r.getProgramId(), r);
+ } else {
+ mScheduledRecordings.put(r.getId(), r);
+ if (r.getProgramId() != ScheduledRecording.ID_NOT_SET) {
+ mProgramId2ScheduledRecordings.put(r.getProgramId(), r);
}
- break;
- case ScheduledRecording.STATE_RECORDING_NOT_STARTED:
- if (r.getEndTimeMs() <= mClock.currentTimeMillis()) {
- toUpdate.add(
- ScheduledRecording.buildFrom(r)
- .setState(
- ScheduledRecording
- .STATE_RECORDING_FAILED)
- .setFailedReason(reasonNotStarted)
- .build());
+ // Adjust the state of the schedules before DB loading is
+ // finished.
+ switch (r.getState()) {
+ case ScheduledRecording.STATE_RECORDING_IN_PROGRESS:
+ if (r.getEndTimeMs()
+ <= mClock.currentTimeMillis()) {
+ int reason =
+ ScheduledRecording
+ .FAILED_REASON_NOT_FINISHED;
+ toUpdate.add(
+ ScheduledRecording.buildFrom(r)
+ .setState(
+ ScheduledRecording
+ .STATE_RECORDING_FAILED)
+ .setFailedReason(reason)
+ .build());
+ } else {
+ toUpdate.add(
+ ScheduledRecording.buildFrom(r)
+ .setState(
+ ScheduledRecording
+ .STATE_RECORDING_NOT_STARTED)
+ .build());
+ }
+ break;
+ case ScheduledRecording.STATE_RECORDING_NOT_STARTED:
+ if (r.getEndTimeMs()
+ <= mClock.currentTimeMillis()) {
+ toUpdate.add(
+ ScheduledRecording.buildFrom(r)
+ .setState(
+ ScheduledRecording
+ .STATE_RECORDING_FAILED)
+ .setFailedReason(
+ reasonNotStarted)
+ .build());
+ }
+ break;
+ case ScheduledRecording.STATE_RECORDING_CANCELED:
+ toDelete.add(r);
+ break;
+ default: // fall out
}
- break;
- case ScheduledRecording.STATE_RECORDING_CANCELED:
- toDelete.add(r);
- break;
- default: // fall out
+ }
+ if (maxId < r.getId()) {
+ maxId = r.getId();
+ }
+ }
+ if (!toUpdate.isEmpty()) {
+ updateScheduledRecording(ScheduledRecording.toArray(toUpdate));
+ }
+ if (!toDelete.isEmpty()) {
+ removeScheduledRecording(ScheduledRecording.toArray(toDelete));
+ }
+ IdGenerator.SCHEDULED_RECORDING.setMaxId(maxId);
+ if (mRecordedProgramLoadFinished) {
+ validateSeriesRecordings();
+ }
+ mDvrLoadFinished = true;
+ notifyDvrScheduleLoadFinished();
+ if (isInitialized()) {
+ mDbSync = new DvrDbSync(mContext, DvrDataManagerImpl.this);
+ mDbSync.start();
+ SeriesRecordingScheduler.getInstance(mContext).start();
}
}
- if (maxId < r.getId()) {
- maxId = r.getId();
+
+ @Override
+ public void onFailure(Throwable t) {
+ Log.w(TAG, "Failed to load scheduled recording.", t);
+ mPendingDvrFuture.remove(this);
}
- }
- if (!toUpdate.isEmpty()) {
- updateScheduledRecording(ScheduledRecording.toArray(toUpdate));
- }
- if (!toDelete.isEmpty()) {
- removeScheduledRecording(ScheduledRecording.toArray(toDelete));
- }
- IdGenerator.SCHEDULED_RECORDING.setMaxId(maxId);
- if (mRecordedProgramLoadFinished) {
- validateSeriesRecordings();
- }
- mDvrLoadFinished = true;
- notifyDvrScheduleLoadFinished();
- if (isInitialized()) {
- mDbSync = new DvrDbSync(mContext, DvrDataManagerImpl.this);
- mDbSync.start();
- SeriesRecordingScheduler.getInstance(mContext).start();
- }
- }
- };
- dvrQueryScheduleTask.executeOnDbThread();
- mPendingTasks.add(dvrQueryScheduleTask);
- RecordedProgramsQueryTask mRecordedProgramQueryTask =
- new RecordedProgramsQueryTask(mContext.getContentResolver(), null);
+ });
+ mPendingDvrFuture.add(dvrQueryScheduleFuture);
+ RecordedProgramsQueryTask mRecordedProgramQueryTask = new RecordedProgramsQueryTask(null);
mRecordedProgramQueryTask.executeOnDbThread();
ContentResolver cr = mContext.getContentResolver();
cr.registerContentObserver(RecordedPrograms.CONTENT_URI, true, mContentObserver);
@@ -329,6 +356,12 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
i.remove();
task.cancel(true);
}
+ Iterator<Future> id = mPendingDvrFuture.iterator();
+ while (id.hasNext()) {
+ Future future = id.next();
+ id.remove();
+ future.cancel(true);
+ }
}
private void onRecordedProgramsLoadedFinished(Uri uri, List<RecordedProgram> recordedPrograms) {
@@ -607,7 +640,10 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
if (mDvrLoadFinished) {
notifyScheduledRecordingAdded(schedules);
}
- new AsyncAddScheduleTask(mContext).executeOnDbThread(schedules);
+ ListenableFuture addScheduleFuture =
+ new AddScheduleFuture(mContext)
+ .executeOnDbThread(removeFromSetOnCompletion, schedules);
+ mNoStopFuture.add(addScheduleFuture);
removeDeletedSchedules(schedules);
}
@@ -626,7 +662,10 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
if (mDvrLoadFinished) {
notifySeriesRecordingAdded(seriesRecordings);
}
- new AsyncAddSeriesRecordingTask(mContext).executeOnDbThread(seriesRecordings);
+ ListenableFuture addSeriesRecordingFuture =
+ new AddSeriesRecordingFuture(mContext)
+ .executeOnDbThread(removeFromSetOnCompletion, seriesRecordings);
+ mNoStopFuture.add(addSeriesRecordingFuture);
}
@Override
@@ -683,12 +722,20 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
}
}
if (!schedulesToDelete.isEmpty()) {
- new AsyncDeleteScheduleTask(mContext)
- .executeOnDbThread(ScheduledRecording.toArray(schedulesToDelete));
+ ListenableFuture deleteScheduleFuture =
+ new DeleteScheduleFuture(mContext)
+ .executeOnDbThread(
+ removeFromSetOnCompletion,
+ ScheduledRecording.toArray(schedulesToDelete));
+ mNoStopFuture.add(deleteScheduleFuture);
}
if (!schedulesNotToDelete.isEmpty()) {
- new AsyncUpdateScheduleTask(mContext)
- .executeOnDbThread(ScheduledRecording.toArray(schedulesNotToDelete));
+ ListenableFuture updateScheduleFuture =
+ new UpdateScheduleFuture(mContext)
+ .executeOnDbThread(
+ removeFromSetOnCompletion,
+ ScheduledRecording.toArray(schedulesNotToDelete));
+ mNoStopFuture.add(updateScheduleFuture);
}
}
@@ -726,7 +773,10 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
if (mDvrLoadFinished) {
notifySeriesRecordingRemoved(seriesRecordings);
}
- new AsyncDeleteSeriesRecordingTask(mContext).executeOnDbThread(seriesRecordings);
+ ListenableFuture deleteSeriesRecordingFuture =
+ new DeleteSeriesRecordingFuture(mContext)
+ .executeOnDbThread(removeFromSetOnCompletion, seriesRecordings);
+ mNoStopFuture.add(deleteSeriesRecordingFuture);
removeDeletedSchedules(seriesRecordings);
}
@@ -778,7 +828,10 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
notifyScheduledRecordingStatusChanged(scheduleArray);
}
if (updateDb) {
- new AsyncUpdateScheduleTask(mContext).executeOnDbThread(scheduleArray);
+ ListenableFuture updateScheduleFuture =
+ new UpdateScheduleFuture(mContext)
+ .executeOnDbThread(removeFromSetOnCompletion, scheduleArray);
+ mNoStopFuture.add(updateScheduleFuture);
}
checkAndRemoveEmptySeriesRecording(seriesRecordingIdsToCheck);
removeDeletedSchedules(schedules);
@@ -802,7 +855,10 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
if (mDvrLoadFinished) {
notifySeriesRecordingChanged(seriesRecordings);
}
- new AsyncUpdateSeriesRecordingTask(mContext).executeOnDbThread(seriesRecordings);
+ ListenableFuture updateSeriesRecordingFuture =
+ new UpdateSeriesRecordingFuture(mContext)
+ .executeOnDbThread(removeFromSetOnCompletion, seriesRecordings);
+ mNoStopFuture.add(updateSeriesRecordingFuture);
}
private boolean isInputAvailable(String inputId) {
@@ -820,8 +876,12 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
}
}
if (!schedulesToDelete.isEmpty()) {
- new AsyncDeleteScheduleTask(mContext)
- .executeOnDbThread(ScheduledRecording.toArray(schedulesToDelete));
+ ListenableFuture deleteScheduleFuture =
+ new DeleteScheduleFuture(mContext)
+ .executeOnDbThread(
+ removeFromSetOnCompletion,
+ ScheduledRecording.toArray(schedulesToDelete));
+ mNoStopFuture.add(deleteScheduleFuture);
}
}
@@ -841,8 +901,12 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
}
}
if (!schedulesToDelete.isEmpty()) {
- new AsyncDeleteScheduleTask(mContext)
- .executeOnDbThread(ScheduledRecording.toArray(schedulesToDelete));
+ ListenableFuture deleteScheduleFuture =
+ new DeleteScheduleFuture(mContext)
+ .executeOnDbThread(
+ removeFromSetOnCompletion,
+ ScheduledRecording.toArray(schedulesToDelete));
+ mNoStopFuture.add(deleteScheduleFuture);
}
}
@@ -852,38 +916,25 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
moveElements(
mScheduledRecordingsForRemovedInput,
mScheduledRecordings,
- new Filter<ScheduledRecording>() {
- @Override
- public boolean filter(ScheduledRecording r) {
- return r.getInputId().equals(inputId);
- }
- });
+ r -> r.getInputId().equals(inputId));
List<RecordedProgram> movedRecordedPrograms =
moveElements(
mRecordedProgramsForRemovedInput,
mRecordedPrograms,
- new Filter<RecordedProgram>() {
- @Override
- public boolean filter(RecordedProgram r) {
- return r.getInputId().equals(inputId);
- }
- });
+ r -> r.getInputId().equals(inputId));
List<SeriesRecording> removedSeriesRecordings = new ArrayList<>();
List<SeriesRecording> movedSeriesRecordings =
moveElements(
mSeriesRecordingsForRemovedInput,
mSeriesRecordings,
- new Filter<SeriesRecording>() {
- @Override
- public boolean filter(SeriesRecording r) {
- if (r.getInputId().equals(inputId)) {
- if (!isEmptySeriesRecording(r)) {
- return true;
- }
- removedSeriesRecordings.add(r);
+ r -> {
+ if (r.getInputId().equals(inputId)) {
+ if (!isEmptySeriesRecording(r)) {
+ return true;
}
- return false;
+ removedSeriesRecordings.add(r);
}
+ return false;
});
if (!movedSchedules.isEmpty()) {
for (ScheduledRecording schedule : movedSchedules) {
@@ -898,8 +949,12 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
for (SeriesRecording r : removedSeriesRecordings) {
mSeriesRecordingsForRemovedInput.remove(r.getId());
}
- new AsyncDeleteSeriesRecordingTask(mContext)
- .executeOnDbThread(SeriesRecording.toArray(removedSeriesRecordings));
+ ListenableFuture deleteSeriesRecordingFuture =
+ new DeleteSeriesRecordingFuture(mContext)
+ .executeOnDbThread(
+ removeFromSetOnCompletion,
+ SeriesRecording.toArray(removedSeriesRecordings));
+ mNoStopFuture.add(deleteSeriesRecordingFuture);
// Notify after all the data are moved.
if (!movedSchedules.isEmpty()) {
notifyScheduledRecordingAdded(ScheduledRecording.toArray(movedSchedules));
@@ -918,32 +973,17 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
moveElements(
mScheduledRecordings,
mScheduledRecordingsForRemovedInput,
- new Filter<ScheduledRecording>() {
- @Override
- public boolean filter(ScheduledRecording r) {
- return r.getInputId().equals(inputId);
- }
- });
+ r -> r.getInputId().equals(inputId));
List<SeriesRecording> movedSeriesRecordings =
moveElements(
mSeriesRecordings,
mSeriesRecordingsForRemovedInput,
- new Filter<SeriesRecording>() {
- @Override
- public boolean filter(SeriesRecording r) {
- return r.getInputId().equals(inputId);
- }
- });
+ r -> r.getInputId().equals(inputId));
List<RecordedProgram> movedRecordedPrograms =
moveElements(
mRecordedPrograms,
mRecordedProgramsForRemovedInput,
- new Filter<RecordedProgram>() {
- @Override
- public boolean filter(RecordedProgram r) {
- return r.getInputId().equals(inputId);
- }
- });
+ r -> r.getInputId().equals(inputId));
if (!movedSchedules.isEmpty()) {
for (ScheduledRecording schedule : movedSchedules) {
mProgramId2ScheduledRecordings.remove(schedule.getProgramId());
@@ -1002,10 +1042,18 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
i.remove();
}
}
- new AsyncDeleteScheduleTask(mContext)
- .executeOnDbThread(ScheduledRecording.toArray(schedulesToDelete));
- new AsyncDeleteSeriesRecordingTask(mContext)
- .executeOnDbThread(SeriesRecording.toArray(seriesRecordingsToDelete));
+ ListenableFuture deleteScheduleFuture =
+ new DeleteScheduleFuture(mContext)
+ .executeOnDbThread(
+ removeFromSetOnCompletion,
+ ScheduledRecording.toArray(schedulesToDelete));
+ mNoStopFuture.add(deleteScheduleFuture);
+ ListenableFuture deleteSeriesRecordingFuture =
+ new DeleteSeriesRecordingFuture(mContext)
+ .executeOnDbThread(
+ removeFromSetOnCompletion,
+ SeriesRecording.toArray(seriesRecordingsToDelete));
+ mNoStopFuture.add(deleteSeriesRecordingFuture);
new AsyncDbTask<Void, Void, Void>(mDbExecutor) {
@Override
protected Void doInBackground(Void... params) {
@@ -1036,7 +1084,10 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
}
if (!removedSeriesRecordings.isEmpty()) {
SeriesRecording[] removed = SeriesRecording.toArray(removedSeriesRecordings);
- new AsyncDeleteSeriesRecordingTask(mContext).executeOnDbThread(removed);
+ ListenableFuture deleteSeriesRecordingFuture =
+ new DeleteSeriesRecordingFuture(mContext)
+ .executeOnDbThread(removeFromSetOnCompletion, removed);
+ mNoStopFuture.add(deleteSeriesRecordingFuture);
if (mDvrLoadFinished) {
notifySeriesRecordingRemoved(removed);
}
@@ -1046,8 +1097,8 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
private final class RecordedProgramsQueryTask extends AsyncRecordedProgramQueryTask {
private final Uri mUri;
- public RecordedProgramsQueryTask(ContentResolver contentResolver, Uri uri) {
- super(mDbExecutor, contentResolver, uri == null ? RecordedPrograms.CONTENT_URI : uri);
+ public RecordedProgramsQueryTask(Uri uri) {
+ super(mDbExecutor, mContext, uri == null ? RecordedPrograms.CONTENT_URI : uri);
mUri = uri;
}
diff --git a/src/com/android/tv/dvr/DvrManager.java b/src/com/android/tv/dvr/DvrManager.java
index 63a245a3..cc9ad37a 100644
--- a/src/com/android/tv/dvr/DvrManager.java
+++ b/src/com/android/tv/dvr/DvrManager.java
@@ -29,6 +29,7 @@ import android.os.AsyncTask;
import android.os.Build;
import android.os.Handler;
import android.os.RemoteException;
+import android.support.annotation.AnyThread;
import android.support.annotation.MainThread;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
@@ -441,14 +442,7 @@ public class DvrManager {
}
synchronized (mListener) {
for (final Entry<Listener, Handler> entry : mListener.entrySet()) {
- entry.getValue()
- .post(
- new Runnable() {
- @Override
- public void run() {
- entry.getKey().onStopRecordingRequested(recording);
- }
- });
+ entry.getValue().post(() -> entry.getKey().onStopRecordingRequested(recording));
}
}
}
@@ -484,26 +478,26 @@ public class DvrManager {
}
/** Removes the recorded program. It deletes the file if possible. */
- public void removeRecordedProgram(Uri recordedProgramUri) {
+ public void removeRecordedProgram(Uri recordedProgramUri, boolean deleteFile) {
if (!SoftPreconditions.checkState(mDataManager.isInitialized())) {
return;
}
- removeRecordedProgram(ContentUris.parseId(recordedProgramUri));
+ removeRecordedProgram(ContentUris.parseId(recordedProgramUri), deleteFile);
}
/** Removes the recorded program. It deletes the file if possible. */
- public void removeRecordedProgram(long recordedProgramId) {
+ public void removeRecordedProgram(long recordedProgramId, boolean deleteFile) {
if (!SoftPreconditions.checkState(mDataManager.isInitialized())) {
return;
}
RecordedProgram recordedProgram = mDataManager.getRecordedProgram(recordedProgramId);
if (recordedProgram != null) {
- removeRecordedProgram(recordedProgram);
+ removeRecordedProgram(recordedProgram, deleteFile);
}
}
/** Removes the recorded program. It deletes the file if possible. */
- public void removeRecordedProgram(final RecordedProgram recordedProgram) {
+ public void removeRecordedProgram(final RecordedProgram recordedProgram, boolean deleteFile) {
if (!SoftPreconditions.checkState(mDataManager.isInitialized())) {
return;
}
@@ -516,7 +510,7 @@ public class DvrManager {
@Override
protected void onPostExecute(Integer deletedCounts) {
- if (deletedCounts > 0) {
+ if (deletedCounts > 0 && deleteFile) {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
@@ -529,7 +523,7 @@ public class DvrManager {
}.executeOnDbThread();
}
- public void removeRecordedPrograms(List<Long> recordedProgramIds) {
+ public void removeRecordedPrograms(List<Long> recordedProgramIds, boolean deleteFiles) {
final ArrayList<ContentProviderOperation> dbOperations = new ArrayList<>();
final List<Uri> dataUris = new ArrayList<>();
for (Long rId : recordedProgramIds) {
@@ -554,7 +548,7 @@ public class DvrManager {
@Override
protected void onPostExecute(Boolean success) {
- if (success) {
+ if (success && deleteFiles) {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
@@ -829,37 +823,47 @@ public class DvrManager {
@WorkerThread
private void removeRecordedData(Uri dataUri) {
try {
- if (dataUri != null
- && ContentResolver.SCHEME_FILE.equals(dataUri.getScheme())
- && dataUri.getPath() != null) {
+ if (isFile(dataUri)) {
File recordedProgramPath = new File(dataUri.getPath());
if (!recordedProgramPath.exists()) {
if (DEBUG) Log.d(TAG, "File to delete not exist: " + recordedProgramPath);
} else {
- CommonUtils.deleteDirOrFile(recordedProgramPath);
- if (DEBUG) {
- Log.d(TAG, "Sucessfully deleted files of the recorded program: " + dataUri);
+ if (CommonUtils.deleteDirOrFile(recordedProgramPath)) {
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "Successfully deleted files of the recorded program: "
+ + dataUri);
+ }
+ } else {
+ Log.w(TAG, "Unable to delete recording data at " + dataUri);
}
}
}
} catch (SecurityException e) {
- if (DEBUG) {
- Log.d(
- TAG,
- "To delete this recorded program, please manually delete video data at"
- + "\nadb shell rm -rf "
- + dataUri);
- }
+ Log.w(TAG, "Unable to delete recording data at " + dataUri, e);
}
}
+ @AnyThread
+ public static boolean isFromBundledInput(RecordedProgram mRecordedProgram) {
+ return CommonUtils.isInBundledPackageSet(mRecordedProgram.getPackageName());
+ }
+
+ @AnyThread
+ public static boolean isFile(Uri dataUri) {
+ return dataUri != null
+ && ContentResolver.SCHEME_FILE.equals(dataUri.getScheme())
+ && dataUri.getPath() != null;
+ }
+
/**
* Remove all the records related to the input.
*
* <p>Note that this should be called after the input was removed.
*/
public void forgetStorage(String inputId) {
- if (mDataManager.isInitialized()) {
+ if (mDataManager != null && mDataManager.isInitialized()) {
mDataManager.forgetStorage(inputId);
}
}
diff --git a/src/com/android/tv/dvr/DvrScheduleManager.java b/src/com/android/tv/dvr/DvrScheduleManager.java
index d5126b12..7202dce0 100644
--- a/src/com/android/tv/dvr/DvrScheduleManager.java
+++ b/src/com/android/tv/dvr/DvrScheduleManager.java
@@ -923,12 +923,8 @@ public class DvrScheduleManager {
List<ConflictInfo> result = new ArrayList<>(conflicts.values());
Collections.sort(
result,
- new Comparator<ConflictInfo>() {
- @Override
- public int compare(ConflictInfo lhs, ConflictInfo rhs) {
- return RESULT_COMPARATOR.compare(lhs.schedule, rhs.schedule);
- }
- });
+ (ConflictInfo lhs, ConflictInfo rhs) ->
+ RESULT_COMPARATOR.compare(lhs.schedule, rhs.schedule));
return result;
}
diff --git a/src/com/android/tv/dvr/DvrStorageStatusManager.java b/src/com/android/tv/dvr/DvrStorageStatusManager.java
index ed8d6903..dc347a9e 100644
--- a/src/com/android/tv/dvr/DvrStorageStatusManager.java
+++ b/src/com/android/tv/dvr/DvrStorageStatusManager.java
@@ -24,8 +24,9 @@ import android.media.tv.TvInputInfo;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.RemoteException;
-import android.support.media.tv.TvContractCompat;
+import android.support.annotation.Nullable;
import android.util.Log;
+import androidx.tvprovider.media.tv.TvContractCompat;
import com.android.tv.TvSingletons;
import com.android.tv.common.recording.RecordingStorageStatusManager;
import com.android.tv.common.util.CommonUtils;
@@ -123,6 +124,8 @@ public class DvrStorageStatusManager extends RecordingStorageStatusManager {
}
}
+
+ @Nullable
private List<ContentProviderOperation> getDeleteOps() {
List<ContentProviderOperation> ops = new ArrayList<>();
@@ -165,6 +168,9 @@ public class DvrStorageStatusManager extends RecordingStorageStatusManager {
}
}
return ops;
+ } catch (Exception e) {
+ Log.w(TAG, "Error when getting delete ops at CleanUpDbTask", e);
+ return null;
}
}
}
diff --git a/src/com/android/tv/dvr/DvrTvView.java b/src/com/android/tv/dvr/DvrTvView.java
new file mode 100644
index 00000000..be1f418b
--- /dev/null
+++ b/src/com/android/tv/dvr/DvrTvView.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.tv.dvr;
+
+import android.content.Context;
+import android.media.PlaybackParams;
+import android.media.session.PlaybackState;
+import android.media.tv.TvTrackInfo;
+import android.media.tv.TvView;
+import android.net.Uri;
+import android.support.annotation.Nullable;
+import com.android.tv.InputSessionManager;
+import com.android.tv.InputSessionManager.TvViewSession;
+import com.android.tv.TvSingletons;
+import com.android.tv.common.compat.TvViewCompat.TvInputCallbackCompat;
+import com.android.tv.dvr.ui.playback.DvrPlayer;
+import com.android.tv.ui.AppLayerTvView;
+import com.android.tv.ui.api.TunableTvViewPlayingApi;
+import java.util.List;
+
+/**
+ * A {@link TvView} wrapper to handle events and TvView session.
+ */
+public class DvrTvView implements TunableTvViewPlayingApi {
+
+ private final AppLayerTvView mTvView;
+ private DvrPlayer mDvrPlayer;
+ private String mInputId;
+ private Uri mRecordedProgramUri;
+ private TvInputCallbackCompat mTvInputCallbackCompat;
+ private InputSessionManager mInputSessionManager;
+ private TvViewSession mSession;
+
+ public DvrTvView(Context context, AppLayerTvView tvView, DvrPlayer player) {
+ mTvView = tvView;
+ mDvrPlayer = player;
+ mInputSessionManager = TvSingletons.getSingletons(context).getInputSessionManager();
+ }
+
+ @Override
+ public boolean isPlaying() {
+ return mDvrPlayer.getPlaybackState() == PlaybackState.STATE_PLAYING;
+ }
+
+ @Override
+ public void setStreamVolume(float volume) {
+ mTvView.setStreamVolume(volume);
+ }
+
+ @Override
+ public void setTimeShiftListener(TimeShiftListener listener) {
+ // TimeShiftListener is never called from DvrTvView because TimeShift is always available
+ // and onRecordStartTimeChanged is not called during playback.
+ }
+
+ @Override
+ public boolean isTimeShiftAvailable() {
+ return true;
+ }
+
+ @Override
+ public void timeShiftPlay() {
+ if (mInputId != null && mRecordedProgramUri != null) {
+ mTvView.timeShiftPlay(mInputId, mRecordedProgramUri);
+ }
+ }
+
+ public void timeShiftPlay(String inputId, Uri recordedProgramUri) {
+ mInputId = inputId;
+ mRecordedProgramUri = recordedProgramUri;
+ mSession.timeShiftPlay(inputId, recordedProgramUri);
+ }
+
+ @Override
+ public void timeShiftPause() {
+ mTvView.timeShiftPause();
+ }
+
+ @Override
+ public void timeShiftRewind(int speed) {
+ PlaybackParams params = new PlaybackParams();
+ params.setSpeed(speed * -1);
+ mTvView.timeShiftSetPlaybackParams(params);
+ }
+
+ @Override
+ public void timeShiftFastForward(int speed) {
+ PlaybackParams params = new PlaybackParams();
+ params.setSpeed(speed);
+ mTvView.timeShiftSetPlaybackParams(params);
+ }
+
+ @Override
+ public void timeShiftSeekTo(long timeMs) {
+ mTvView.timeShiftSeekTo(timeMs);
+ }
+
+ @Override
+ public long timeShiftGetCurrentPositionMs() {
+ return mDvrPlayer.getPlaybackPosition();
+ }
+
+ public void setCaptionEnabled(boolean enabled) {
+ mTvView.setCaptionEnabled(enabled);
+ }
+
+ public void timeShiftResume() {
+ mTvView.timeShiftResume();
+ }
+
+ public void reset() {
+ mSession.reset();
+ }
+
+ public List<TvTrackInfo> getTracks(int type) {
+ return mTvView.getTracks(type);
+ }
+
+ public void selectTrack(int type, String trackId) {
+ mTvView.selectTrack(type, trackId);
+ }
+
+ public void timeShiftSetPlaybackParams(PlaybackParams params) {
+ mTvView.timeShiftSetPlaybackParams(params);
+ }
+
+ public void setTimeShiftPositionCallback(@Nullable TvView.TimeShiftPositionCallback callback) {
+ mTvView.setTimeShiftPositionCallback(callback);
+ }
+
+ public void setCallback(@Nullable TvInputCallbackCompat callback) {
+ mTvInputCallbackCompat = callback;
+ mTvView.setCallback(callback);
+ }
+
+ public void init() {
+ mSession = mInputSessionManager.createTvViewSession(mTvView, this, mTvInputCallbackCompat);
+ }
+
+ public void release() {
+ mInputSessionManager.releaseTvViewSession(mSession);
+ mInputSessionManager = null;
+ mDvrPlayer = null;
+ }
+}
diff --git a/src/com/android/tv/dvr/data/RecordedProgram.java b/src/com/android/tv/dvr/data/RecordedProgram.java
index e1fbca8c..899e65ac 100644
--- a/src/com/android/tv/dvr/data/RecordedProgram.java
+++ b/src/com/android/tv/dvr/data/RecordedProgram.java
@@ -22,31 +22,38 @@ import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.media.tv.TvContentRating;
-import android.media.tv.TvContract;
+import android.media.tv.TvContract.Programs.Genres;
import android.media.tv.TvContract.RecordedPrograms;
import android.net.Uri;
import android.os.Build;
+import android.support.annotation.CheckResult;
import android.support.annotation.Nullable;
+import android.support.annotation.WorkerThread;
import android.text.TextUtils;
+import android.util.Log;
import com.android.tv.common.R;
import com.android.tv.common.TvContentRatingCache;
+import com.android.tv.common.data.RecordedProgramState;
import com.android.tv.common.util.CommonUtils;
+import com.android.tv.common.util.StringUtils;
import com.android.tv.data.BaseProgram;
import com.android.tv.data.GenreItems;
import com.android.tv.data.InternalDataUtils;
-import java.util.Arrays;
+import com.android.tv.util.TvProviderUtils;
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
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 {
+@AutoValue
+public abstract class RecordedProgram extends BaseProgram {
public static final int ID_NOT_SET = -1;
+ private static final String TAG = "RecordedProgram";
public static final String[] PROJECTION = {
- // These are in exactly the order listed in RecordedPrograms
RecordedPrograms._ID,
RecordedPrograms.COLUMN_PACKAGE_NAME,
RecordedPrograms.COLUMN_INPUT_ID,
@@ -73,10 +80,6 @@ public class RecordedProgram extends BaseProgram {
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,
};
@@ -89,834 +92,372 @@ public class RecordedProgram extends BaseProgram {
.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++))
+ .setTitle(StringUtils.nullToEmpty(cursor.getString(index++)))
+ .setSeasonNumber(StringUtils.nullToEmpty(cursor.getString(index++)))
+ .setSeasonTitle(StringUtils.nullToEmpty(cursor.getString(index++)))
+ .setEpisodeNumber(StringUtils.nullToEmpty(cursor.getString(index++)))
+ .setEpisodeTitle(StringUtils.nullToEmpty(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++))
+ .setDescription(StringUtils.nullToEmpty(cursor.getString(index++)))
+ .setLongDescription(StringUtils.nullToEmpty(cursor.getString(index++)))
.setVideoWidth(cursor.getInt(index++))
.setVideoHeight(cursor.getInt(index++))
- .setAudioLanguage(cursor.getString(index++))
+ .setAudioLanguage(StringUtils.nullToEmpty(cursor.getString(index++)))
.setContentRatings(
TvContentRatingCache.getInstance()
.getRatings(cursor.getString(index++)))
- .setPosterArtUri(cursor.getString(index++))
- .setThumbnailUri(cursor.getString(index++))
+ .setPosterArtUri(StringUtils.nullToEmpty(cursor.getString(index++)))
+ .setThumbnailUri(StringUtils.nullToEmpty(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 (CommonUtils.isInBundledPackageSet(builder.mPackageName)) {
+ if (CommonUtils.isInBundledPackageSet(builder.getPackageName())) {
InternalDataUtils.deserializeInternalProviderData(cursor.getBlob(index), builder);
}
+ index++;
+ if (TvProviderUtils.getRecordedProgramHasSeriesIdColumn()) {
+ builder.setSeriesId(StringUtils.nullToEmpty(cursor.getString(index++)));
+ }
+ if (TvProviderUtils.getRecordedProgramHasStateColumn()) {
+ builder.setState(cursor.getString(index++));
+ }
return builder.build();
}
- public static ContentValues toValues(RecordedProgram recordedProgram) {
+ @WorkerThread
+ public static ContentValues toValues(Context context, RecordedProgram recordedProgram) {
ContentValues values = new ContentValues();
- if (recordedProgram.mId != ID_NOT_SET) {
- values.put(RecordedPrograms._ID, recordedProgram.mId);
+ if (recordedProgram.getId() != ID_NOT_SET) {
+ values.put(RecordedPrograms._ID, recordedProgram.getId());
}
- 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_INPUT_ID, recordedProgram.getInputId());
+ values.put(RecordedPrograms.COLUMN_CHANNEL_ID, recordedProgram.getChannelId());
+ values.put(RecordedPrograms.COLUMN_TITLE, recordedProgram.getTitle());
+ values.put(
+ RecordedPrograms.COLUMN_SEASON_DISPLAY_NUMBER, recordedProgram.getSeasonNumber());
+ values.put(RecordedPrograms.COLUMN_SEASON_TITLE, recordedProgram.getSeasonTitle());
+ values.put(
+ RecordedPrograms.COLUMN_EPISODE_DISPLAY_NUMBER, recordedProgram.getEpisodeNumber());
+ values.put(RecordedPrograms.COLUMN_EPISODE_TITLE, recordedProgram.getEpisodeTitle());
+ values.put(
+ RecordedPrograms.COLUMN_START_TIME_UTC_MILLIS,
+ recordedProgram.getStartTimeUtcMillis());
values.put(
- RecordedPrograms.COLUMN_START_TIME_UTC_MILLIS, recordedProgram.mStartTimeUtcMillis);
- values.put(RecordedPrograms.COLUMN_END_TIME_UTC_MILLIS, recordedProgram.mEndTimeUtcMillis);
+ RecordedPrograms.COLUMN_END_TIME_UTC_MILLIS, recordedProgram.getEndTimeUtcMillis());
values.put(
RecordedPrograms.COLUMN_BROADCAST_GENRE,
- safeEncode(recordedProgram.mBroadcastGenres));
+ safeEncode(recordedProgram.getBroadcastGenres()));
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) {
+ safeEncode(recordedProgram.getCanonicalGenres()));
+ values.put(RecordedPrograms.COLUMN_SHORT_DESCRIPTION, recordedProgram.getDescription());
+ values.put(RecordedPrograms.COLUMN_LONG_DESCRIPTION, recordedProgram.getLongDescription());
+ if (recordedProgram.getVideoWidth() == 0) {
values.putNull(RecordedPrograms.COLUMN_VIDEO_WIDTH);
} else {
- values.put(RecordedPrograms.COLUMN_VIDEO_WIDTH, recordedProgram.mVideoWidth);
+ values.put(RecordedPrograms.COLUMN_VIDEO_WIDTH, recordedProgram.getVideoWidth());
}
- if (recordedProgram.mVideoHeight == 0) {
+ if (recordedProgram.getVideoHeight() == 0) {
values.putNull(RecordedPrograms.COLUMN_VIDEO_HEIGHT);
} else {
- values.put(RecordedPrograms.COLUMN_VIDEO_HEIGHT, recordedProgram.mVideoHeight);
+ values.put(RecordedPrograms.COLUMN_VIDEO_HEIGHT, recordedProgram.getVideoHeight());
}
- values.put(RecordedPrograms.COLUMN_AUDIO_LANGUAGE, recordedProgram.mAudioLanguage);
+ values.put(RecordedPrograms.COLUMN_AUDIO_LANGUAGE, recordedProgram.getAudioLanguage());
values.put(
RecordedPrograms.COLUMN_CONTENT_RATING,
- TvContentRatingCache.contentRatingsToString(recordedProgram.mContentRatings));
- values.put(RecordedPrograms.COLUMN_POSTER_ART_URI, recordedProgram.mPosterArtUri);
- values.put(RecordedPrograms.COLUMN_THUMBNAIL_URI, recordedProgram.mThumbnailUri);
- values.put(RecordedPrograms.COLUMN_SEARCHABLE, recordedProgram.mSearchable ? 1 : 0);
+ TvContentRatingCache.contentRatingsToString(recordedProgram.getContentRatings()));
+ values.put(RecordedPrograms.COLUMN_POSTER_ART_URI, recordedProgram.getPosterArtUri());
+ values.put(RecordedPrograms.COLUMN_THUMBNAIL_URI, recordedProgram.getThumbnailUri());
+ values.put(RecordedPrograms.COLUMN_SEARCHABLE, recordedProgram.isSearchable() ? 1 : 0);
values.put(
- RecordedPrograms.COLUMN_RECORDING_DATA_URI, safeToString(recordedProgram.mDataUri));
- values.put(RecordedPrograms.COLUMN_RECORDING_DATA_BYTES, recordedProgram.mDataBytes);
+ RecordedPrograms.COLUMN_RECORDING_DATA_URI,
+ safeToString(recordedProgram.getDataUri()));
+ values.put(RecordedPrograms.COLUMN_RECORDING_DATA_BYTES, recordedProgram.getDataBytes());
values.put(
- RecordedPrograms.COLUMN_RECORDING_DURATION_MILLIS, recordedProgram.mDurationMillis);
+ RecordedPrograms.COLUMN_RECORDING_DURATION_MILLIS,
+ recordedProgram.getDurationMillis());
values.put(
RecordedPrograms.COLUMN_RECORDING_EXPIRE_TIME_UTC_MILLIS,
- recordedProgram.mExpireTimeUtcMillis);
+ recordedProgram.getExpireTimeUtcMillis());
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);
+ values.put(RecordedPrograms.COLUMN_VERSION_NUMBER, recordedProgram.getVersionNumber());
+ if (TvProviderUtils.checkSeriesIdColumn(context, RecordedPrograms.CONTENT_URI)) {
+ values.put(COLUMN_SERIES_ID, recordedProgram.getSeriesId());
+ }
+ if (TvProviderUtils.checkStateColumn(context, RecordedPrograms.CONTENT_URI)) {
+ values.put(COLUMN_STATE, recordedProgram.getState().toString());
+ }
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 TvContentRating[] mContentRatings;
- 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;
- }
+ /** Builder for {@link RecordedProgram}s. */
+ @AutoValue.Builder
+ public abstract static class Builder {
- public Builder setPackageName(String packageName) {
- mPackageName = packageName;
- return this;
- }
+ public abstract Builder setId(long id);
- public Builder setInputId(String inputId) {
- mInputId = inputId;
- return this;
- }
+ public abstract Builder setPackageName(String packageName);
- public Builder setChannelId(long channelId) {
- mChannelId = channelId;
- return this;
- }
+ abstract String getPackageName();
- public Builder setTitle(String title) {
- mTitle = title;
- return this;
- }
+ public abstract Builder setInputId(String inputId);
- public Builder setSeriesId(String seriesId) {
- mSeriesId = seriesId;
- return this;
- }
+ public abstract Builder setChannelId(long channelId);
- public Builder setSeasonNumber(String seasonNumber) {
- mSeasonNumber = seasonNumber;
- return this;
- }
+ abstract String getTitle();
- public Builder setSeasonTitle(String seasonTitle) {
- mSeasonTitle = seasonTitle;
- return this;
- }
+ public abstract Builder setTitle(String title);
- public Builder setEpisodeNumber(String episodeNumber) {
- mEpisodeNumber = episodeNumber;
- return this;
- }
+ abstract String getSeriesId();
- public Builder setEpisodeTitle(String episodeTitle) {
- mEpisodeTitle = episodeTitle;
- return this;
- }
+ public abstract Builder setSeriesId(String seriesId);
- public Builder setStartTimeUtcMillis(long startTimeUtcMillis) {
- mStartTimeUtcMillis = startTimeUtcMillis;
- return this;
- }
+ public abstract Builder setSeasonNumber(String seasonNumber);
- public Builder setEndTimeUtcMillis(long endTimeUtcMillis) {
- mEndTimeUtcMillis = endTimeUtcMillis;
- return this;
- }
+ public abstract Builder setSeasonTitle(String seasonTitle);
+
+ @Nullable
+ abstract String getEpisodeNumber();
+
+ public abstract Builder setEpisodeNumber(String episodeNumber);
- public Builder setBroadcastGenres(String broadcastGenres) {
- if (TextUtils.isEmpty(broadcastGenres)) {
- mBroadcastGenres = null;
- return this;
+ public abstract Builder setEpisodeTitle(String episodeTitle);
+
+ public abstract Builder setStartTimeUtcMillis(long startTimeUtcMillis);
+
+ public abstract Builder setEndTimeUtcMillis(long endTimeUtcMillis);
+
+ public abstract Builder setState(RecordedProgramState state);
+
+ public Builder setState(@Nullable String state) {
+
+ if (!TextUtils.isEmpty(state)) {
+ try {
+ return setState(RecordedProgramState.valueOf(state));
+ } catch (IllegalArgumentException e) {
+ Log.w(TAG, "Unknown recording state " + state, e);
+ }
}
- return setBroadcastGenres(TvContract.Programs.Genres.decode(broadcastGenres));
+ return setState(RecordedProgramState.NOT_SET);
}
- private Builder setBroadcastGenres(String[] broadcastGenres) {
- mBroadcastGenres = broadcastGenres;
- return this;
+ public Builder setBroadcastGenres(@Nullable String broadcastGenres) {
+ return setBroadcastGenres(
+ TextUtils.isEmpty(broadcastGenres)
+ ? ImmutableList.of()
+ : ImmutableList.copyOf(Genres.decode(broadcastGenres)));
}
+ public abstract Builder setBroadcastGenres(ImmutableList<String> broadcastGenres);
+
public Builder setCanonicalGenres(String canonicalGenres) {
- if (TextUtils.isEmpty(canonicalGenres)) {
- mCanonicalGenres = null;
- return this;
- }
- return setCanonicalGenres(TvContract.Programs.Genres.decode(canonicalGenres));
+ return setCanonicalGenres(
+ TextUtils.isEmpty(canonicalGenres)
+ ? ImmutableList.of()
+ : ImmutableList.copyOf(Genres.decode(canonicalGenres)));
}
- private Builder setCanonicalGenres(String[] canonicalGenres) {
- mCanonicalGenres = canonicalGenres;
- return this;
- }
+ public abstract Builder setCanonicalGenres(ImmutableList<String> canonicalGenres);
- public Builder setShortDescription(String shortDescription) {
- mShortDescription = shortDescription;
- return this;
- }
+ public abstract Builder setDescription(String shortDescription);
- public Builder setLongDescription(String longDescription) {
- mLongDescription = longDescription;
- return this;
- }
+ public abstract Builder setLongDescription(String longDescription);
- public Builder setVideoWidth(int videoWidth) {
- mVideoWidth = videoWidth;
- return this;
- }
+ public abstract Builder setVideoWidth(int videoWidth);
- public Builder setVideoHeight(int videoHeight) {
- mVideoHeight = videoHeight;
- return this;
- }
+ public abstract Builder setVideoHeight(int videoHeight);
- public Builder setAudioLanguage(String audioLanguage) {
- mAudioLanguage = audioLanguage;
- return this;
- }
+ public abstract Builder setAudioLanguage(String audioLanguage);
- public Builder setContentRatings(TvContentRating[] contentRatings) {
- mContentRatings = contentRatings;
- return this;
- }
+ public abstract Builder setContentRatings(ImmutableList<TvContentRating> contentRatings);
- private Uri toUri(String uriString) {
+ private Uri toUri(@Nullable String uriString) {
try {
return uriString == null ? null : Uri.parse(uriString);
} catch (Exception e) {
- return null;
+ return Uri.EMPTY;
}
}
- public Builder setPosterArtUri(String posterArtUri) {
- mPosterArtUri = posterArtUri;
- return this;
- }
+ public abstract Builder setPosterArtUri(String posterArtUri);
- public Builder setThumbnailUri(String thumbnailUri) {
- mThumbnailUri = thumbnailUri;
- return this;
- }
+ public abstract Builder setThumbnailUri(String thumbnailUri);
- public Builder setSearchable(boolean searchable) {
- mSearchable = searchable;
- return this;
- }
+ public abstract Builder setSearchable(boolean searchable);
- public Builder setDataUri(String dataUri) {
+ public Builder setDataUri(@Nullable 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 abstract Builder setDataUri(Uri dataUri);
- public Builder setInternalProviderFlag1(int internalProviderFlag1) {
- mInternalProviderFlag1 = internalProviderFlag1;
- return this;
- }
+ public abstract Builder setDataBytes(long dataBytes);
- public Builder setInternalProviderFlag2(int internalProviderFlag2) {
- mInternalProviderFlag2 = internalProviderFlag2;
- return this;
- }
+ public abstract Builder setDurationMillis(long durationMillis);
- public Builder setInternalProviderFlag3(int internalProviderFlag3) {
- mInternalProviderFlag3 = internalProviderFlag3;
- return this;
- }
+ public abstract Builder setExpireTimeUtcMillis(long expireTimeUtcMillis);
- public Builder setInternalProviderFlag4(int internalProviderFlag4) {
- mInternalProviderFlag4 = internalProviderFlag4;
- return this;
- }
+ public abstract Builder setVersionNumber(int versionNumber);
- public Builder setVersionNumber(int versionNumber) {
- mVersionNumber = versionNumber;
- return this;
- }
+ abstract RecordedProgram autoBuild();
public RecordedProgram build() {
- if (TextUtils.isEmpty(mTitle)) {
+ if (TextUtils.isEmpty(getTitle())) {
// If title is null, series cannot be generated for this program.
setSeriesId(null);
- } else if (TextUtils.isEmpty(mSeriesId) && !TextUtils.isEmpty(mEpisodeNumber)) {
+ } else if (TextUtils.isEmpty(getSeriesId()) && !TextUtils.isEmpty(getEpisodeNumber())) {
// If series ID is not set, generate it for the episodic program of other TV input.
- setSeriesId(BaseProgram.generateSeriesId(mPackageName, mTitle));
+ setSeriesId(BaseProgram.generateSeriesId(getPackageName(), getTitle()));
}
- return new RecordedProgram(
- mId,
- mPackageName,
- mInputId,
- mChannelId,
- mTitle,
- mSeriesId,
- mSeasonNumber,
- mSeasonTitle,
- mEpisodeNumber,
- mEpisodeTitle,
- mStartTimeUtcMillis,
- mEndTimeUtcMillis,
- mBroadcastGenres,
- mCanonicalGenres,
- mShortDescription,
- mLongDescription,
- mVideoWidth,
- mVideoHeight,
- mAudioLanguage,
- mContentRatings,
- mPosterArtUri,
- mThumbnailUri,
- mSearchable,
- mDataUri,
- mDataBytes,
- mDurationMillis,
- mExpireTimeUtcMillis,
- mInternalProviderFlag1,
- mInternalProviderFlag2,
- mInternalProviderFlag3,
- mInternalProviderFlag4,
- mVersionNumber);
+ return (autoBuild());
}
}
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())
- .setContentRatings(orig.getContentRatings())
- .setPosterArtUri(orig.getPosterArtUri())
- .setThumbnailUri(orig.getThumbnailUri())
- .setSearchable(orig.isSearchable())
- .setInternalProviderFlag1(orig.getInternalProviderFlag1())
- .setInternalProviderFlag2(orig.getInternalProviderFlag2())
- .setInternalProviderFlag3(orig.getInternalProviderFlag3())
- .setInternalProviderFlag4(orig.getInternalProviderFlag4())
- .setVersionNumber(orig.getVersionNumber());
+ return new AutoValue_RecordedProgram.Builder()
+ .setId(ID_NOT_SET)
+ .setChannelId(ID_NOT_SET)
+ .setAudioLanguage("")
+ .setBroadcastGenres("")
+ .setCanonicalGenres("")
+ .setContentRatings(ImmutableList.of())
+ .setDataUri("")
+ .setDurationMillis(0)
+ .setDescription("")
+ .setDataBytes(0)
+ .setLongDescription("")
+ .setEndTimeUtcMillis(0)
+ .setEpisodeNumber("")
+ .setEpisodeTitle("")
+ .setExpireTimeUtcMillis(0)
+ .setPackageName("")
+ .setPosterArtUri("")
+ .setSeasonNumber("")
+ .setSeasonTitle("")
+ .setSearchable(false)
+ .setSeriesId("")
+ .setStartTimeUtcMillis(0)
+ .setState(RecordedProgramState.NOT_SET)
+ .setThumbnailUri("")
+ .setTitle("")
+ .setVersionNumber(0)
+ .setVideoHeight(0)
+ .setVideoWidth(0);
}
public static final Comparator<RecordedProgram> START_TIME_THEN_ID_COMPARATOR =
- new Comparator<RecordedProgram>() {
- @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);
+ (RecordedProgram lhs, RecordedProgram rhs) -> {
+ int res = Long.compare(lhs.getStartTimeUtcMillis(), rhs.getStartTimeUtcMillis());
+ if (res != 0) {
+ return res;
}
+ return Long.compare(lhs.getId(), rhs.getId());
};
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 TvContentRating[] mContentRatings;
- 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,
- TvContentRating[] contentRatings,
- String posterArtUri,
- String thumbnailUri,
- boolean searchable,
- Uri dataUri,
- long dataBytes,
- long durationMillis,
- long expireTimeUtcMillis,
- int internalProviderFlag1,
- int internalProviderFlag2,
- int internalProviderFlag3,
- int internalProviderFlag4,
- int versionNumber) {
- mId = id;
- mPackageName = packageName;
- mInputId = inputId;
- 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;
- mContentRatings = contentRatings;
- 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 abstract String getAudioLanguage();
- public String[] getBroadcastGenres() {
- return mBroadcastGenres;
- }
+ public abstract ImmutableList<String> getBroadcastGenres();
- public String[] getCanonicalGenres() {
- return mCanonicalGenres;
- }
+ public abstract ImmutableList<String> getCanonicalGenres();
/** 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]);
+
+ ImmutableList<String> canonicalGenres = getCanonicalGenres();
+ int[] genreIds = new int[getCanonicalGenres().size()];
+ for (int i = 0; i < canonicalGenres.size(); i++) {
+ genreIds[i] = GenreItems.getId(canonicalGenres.get(i));
}
return genreIds;
}
- @Override
- public long getChannelId() {
- return mChannelId;
- }
-
- @Nullable
- @Override
- public TvContentRating[] getContentRatings() {
- return mContentRatings;
- }
-
- 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 abstract Uri getDataUri();
- @Override
- public String getEpisodeTitle() {
- return mEpisodeTitle;
- }
+ public abstract long getDataBytes();
@Nullable
public String getEpisodeDisplayNumber(Context context) {
- if (!TextUtils.isEmpty(mEpisodeNumber)) {
- if (TextUtils.equals(mSeasonNumber, "0")) {
+ if (!TextUtils.isEmpty(getEpisodeNumber())) {
+ if (TextUtils.equals(getSeasonNumber(), "0")) {
// Do not show "S0: ".
- return String.format(
- context.getResources()
- .getString(R.string.display_episode_number_format_no_season_number),
- mEpisodeNumber);
+ return context.getResources()
+ .getString(
+ R.string.display_episode_number_format_no_season_number,
+ getEpisodeNumber());
} else {
- return String.format(
- context.getResources().getString(R.string.display_episode_number_format),
- mSeasonNumber,
- mEpisodeNumber);
+ return context.getResources()
+ .getString(
+ R.string.display_episode_number_format,
+ getSeasonNumber(),
+ getEpisodeNumber());
}
}
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;
- }
+ public abstract long getExpireTimeUtcMillis();
- @Override
- public String getLongDescription() {
- return mLongDescription;
- }
+ public abstract String getPackageName();
- @Override
- public String getPosterArtUri() {
- return mPosterArtUri;
- }
+ public abstract String getInputId();
@Override
public boolean isValid() {
return true;
}
- public boolean isSearchable() {
- return mSearchable;
- }
-
- @Override
- public String getSeriesId() {
- return mSeriesId;
- }
-
- @Override
- public String getSeasonNumber() {
- return mSeasonNumber;
+ public boolean isVisible() {
+ switch (getState()) {
+ case NOT_SET:
+ case FINISHED:
+ return true;
+ default:
+ return false;
+ }
}
- public String getSeasonTitle() {
- return mSeasonTitle;
+ public boolean isPartial() {
+ return getState() == RecordedProgramState.PARTIAL;
}
- @Override
- public long getStartTimeUtcMillis() {
- return mStartTimeUtcMillis;
- }
+ public abstract boolean isSearchable();
- @Override
- public String getThumbnailUri() {
- return mThumbnailUri;
- }
+ public abstract String getSeasonTitle();
- @Override
- public String getTitle() {
- return mTitle;
- }
+ public abstract RecordedProgramState getState();
public Uri getUri() {
- return ContentUris.withAppendedId(RecordedPrograms.CONTENT_URI, mId);
+ return ContentUris.withAppendedId(RecordedPrograms.CONTENT_URI, getId());
}
- public int getVersionNumber() {
- return mVersionNumber;
- }
+ public abstract int getVersionNumber();
- public int getVideoHeight() {
- return mVideoHeight;
- }
+ public abstract int getVideoHeight();
- public int getVideoWidth() {
- return mVideoWidth;
- }
+ public abstract int getVideoWidth();
/** 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)
- && Arrays.equals(mContentRatings, that.mContentRatings)
- && Objects.equals(mPosterArtUri, that.mPosterArtUri)
- && Objects.equals(mThumbnailUri, that.mThumbnailUri);
+ return getEndTimeUtcMillis() - getStartTimeUtcMillis() - getDurationMillis()
+ > CLIPPED_THRESHOLD_MS;
}
- /** Hashes based on the ID. */
- @Override
- public int hashCode() {
- return Objects.hash(mId);
- }
+ public abstract Builder toBuilder();
- @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
- + '\''
- + ", mContentRatings='"
- + TvContentRatingCache.contentRatingsToString(mContentRatings)
- + '\''
- + ", 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
- + '}';
+ @CheckResult
+ public RecordedProgram withId(long id) {
+ return toBuilder().setId(id).build();
}
@Nullable
@@ -925,8 +466,8 @@ public class RecordedProgram extends BaseProgram {
}
@Nullable
- private static String safeEncode(@Nullable String[] genres) {
- return genres == null ? null : TvContract.Programs.Genres.encode(genres);
+ private static String safeEncode(@Nullable ImmutableList<String> genres) {
+ return genres == null ? null : Genres.encode(genres.toArray(new String[0]));
}
/** Returns an array containing all of the elements in the list. */
diff --git a/src/com/android/tv/dvr/data/ScheduledRecording.java b/src/com/android/tv/dvr/data/ScheduledRecording.java
index 7c2d12d9..ba6d3cf9 100644
--- a/src/com/android/tv/dvr/data/ScheduledRecording.java
+++ b/src/com/android/tv/dvr/data/ScheduledRecording.java
@@ -56,39 +56,22 @@ public final class ScheduledRecording implements Parcelable {
/** Compares the start time in ascending order. */
public static final Comparator<ScheduledRecording> START_TIME_COMPARATOR =
- new Comparator<ScheduledRecording>() {
- @Override
- public int compare(ScheduledRecording lhs, ScheduledRecording rhs) {
- return Long.compare(lhs.mStartTimeMs, rhs.mStartTimeMs);
- }
- };
+ (ScheduledRecording lhs, ScheduledRecording rhs) ->
+ Long.compare(lhs.mStartTimeMs, rhs.mStartTimeMs);
/** Compares the end time in ascending order. */
public static final Comparator<ScheduledRecording> END_TIME_COMPARATOR =
- new Comparator<ScheduledRecording>() {
- @Override
- public int compare(ScheduledRecording lhs, ScheduledRecording rhs) {
- return Long.compare(lhs.mEndTimeMs, rhs.mEndTimeMs);
- }
- };
+ (ScheduledRecording lhs, ScheduledRecording rhs) ->
+ Long.compare(lhs.mEndTimeMs, rhs.mEndTimeMs);
/** Compares ID in ascending order. The schedule with the larger ID was created later. */
public static final Comparator<ScheduledRecording> ID_COMPARATOR =
- new Comparator<ScheduledRecording>() {
- @Override
- public int compare(ScheduledRecording lhs, ScheduledRecording rhs) {
- return Long.compare(lhs.mId, rhs.mId);
- }
- };
+ (ScheduledRecording lhs, ScheduledRecording rhs) -> Long.compare(lhs.mId, rhs.mId);
/** Compares the priority in ascending order. */
public static final Comparator<ScheduledRecording> PRIORITY_COMPARATOR =
- new Comparator<ScheduledRecording>() {
- @Override
- public int compare(ScheduledRecording lhs, ScheduledRecording rhs) {
- return Long.compare(lhs.mPriority, rhs.mPriority);
- }
- };
+ (ScheduledRecording lhs, ScheduledRecording rhs) ->
+ Long.compare(lhs.mPriority, rhs.mPriority);
/**
* Compares start time in ascending order and then priority in descending order and then ID in
@@ -359,15 +342,22 @@ public final class ScheduledRecording implements Parcelable {
})
public @interface RecordingFailedReason {}
+ // next number for failed reason: 11
public static final int FAILED_REASON_OTHER = 0;
- public static final int FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED = 1;
public static final int FAILED_REASON_NOT_FINISHED = 2;
public static final int FAILED_REASON_SCHEDULER_STOPPED = 3;
public static final int FAILED_REASON_INVALID_CHANNEL = 4;
public static final int FAILED_REASON_MESSAGE_NOT_SENT = 5;
public static final int FAILED_REASON_CONNECTION_FAILED = 6;
+
+ // for the following reasons, show advice to users
+ // TODO(b/72638597): add failure condition of "weak signal"
+
+ // failed reason is FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED when tuner or external
+ // storage is disconnected
+ public static final int FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED = 1;
+ // failed reason is FAILED_REASON_RESOURCE_BUSY when antenna is disconnected or signal is weak
public static final int FAILED_REASON_RESOURCE_BUSY = 7;
- // For the following reasons, show advice to users
public static final int FAILED_REASON_INPUT_UNAVAILABLE = 8;
public static final int FAILED_REASON_INPUT_DVR_UNSUPPORTED = 9;
public static final int FAILED_REASON_INSUFFICIENT_SPACE = 10;
@@ -679,7 +669,8 @@ public final class ScheduledRecording implements Parcelable {
}
/** Returns the failed reason of the {@link ScheduledRecording}. */
- @Nullable @RecordingFailedReason
+ @Nullable
+ @RecordingFailedReason
public Integer getFailedReason() {
return mFailedReason;
}
@@ -812,10 +803,7 @@ public final class ScheduledRecording implements Parcelable {
}
}
- /**
- * Converts a string to a failed reason integer, defaulting to {@link
- * #FAILED_REASON_OTHER}.
- */
+ /** Converts a string to a failed reason integer, defaulting to {@link #FAILED_REASON_OTHER}. */
private static Integer recordingFailedReason(String reason) {
if (TextUtils.isEmpty(reason)) {
return null;
@@ -985,6 +973,11 @@ public final class ScheduledRecording implements Parcelable {
return mState == STATE_RECORDING_FINISHED;
}
+ /** Returns {@code true} if the recording is failed, otherwise @{code false}. */
+ public boolean isFailed() {
+ return mState == STATE_RECORDING_FAILED;
+ }
+
@Override
public boolean equals(Object obj) {
if (!(obj instanceof ScheduledRecording)) {
diff --git a/src/com/android/tv/dvr/data/SeriesRecording.java b/src/com/android/tv/dvr/data/SeriesRecording.java
index 96b3425a..6cb0e836 100644
--- a/src/com/android/tv/dvr/data/SeriesRecording.java
+++ b/src/com/android/tv/dvr/data/SeriesRecording.java
@@ -49,9 +49,8 @@ public class SeriesRecording implements Parcelable {
@Retention(RetentionPolicy.SOURCE)
@IntDef(
- flag = true,
- value = {OPTION_CHANNEL_ONE, OPTION_CHANNEL_ALL}
- )
+ 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;
@@ -60,9 +59,8 @@ public class SeriesRecording implements Parcelable {
@Retention(RetentionPolicy.SOURCE)
@IntDef(
- flag = true,
- value = {STATE_SERIES_NORMAL, STATE_SERIES_STOPPED}
- )
+ flag = true,
+ value = {STATE_SERIES_NORMAL, STATE_SERIES_STOPPED})
public @interface SeriesState {}
/** The state indicates that the series recording is a normal one. */
@@ -73,26 +71,18 @@ public class SeriesRecording implements Parcelable {
/** Compare priority in descending order. */
public static final Comparator<SeriesRecording> PRIORITY_COMPARATOR =
- new Comparator<SeriesRecording>() {
- @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;
+ (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<SeriesRecording> ID_COMPARATOR =
- new Comparator<SeriesRecording>() {
- @Override
- public int compare(SeriesRecording lhs, SeriesRecording rhs) {
- return Long.compare(lhs.mId, rhs.mId);
- }
- };
+ (SeriesRecording lhs, SeriesRecording rhs) -> Long.compare(lhs.mId, rhs.mId);
/**
* Creates a new Builder with the values set from the series information of {@link BaseProgram}.
diff --git a/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java b/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java
index 41e5a66a..ebf133db 100644
--- a/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java
+++ b/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java
@@ -79,6 +79,8 @@ public class DvrDatabaseHelper extends SQLiteOpenHelper {
+ " TEXT,"
+ Schedules.COLUMN_STATE
+ " TEXT NOT NULL,"
+ + Schedules.COLUMN_FAILED_REASON
+ + " TEXT,"
+ Schedules.COLUMN_SERIES_RECORDING_ID
+ " INTEGER,"
+ "FOREIGN KEY("
@@ -261,6 +263,7 @@ public class DvrDatabaseHelper extends SQLiteOpenHelper {
if (DEBUG) Log.d(TAG, "Executing SQL: " + SQL_DROP_SERIES_RECORDINGS);
db.execSQL(SQL_DROP_SERIES_RECORDINGS);
onCreate(db);
+ return;
}
if (oldVersion < 18) {
db.execSQL("ALTER TABLE " + Schedules.TABLE_NAME + " ADD COLUMN "
diff --git a/src/com/android/tv/dvr/provider/AsyncDvrDbTask.java b/src/com/android/tv/dvr/provider/DvrDbFuture.java
index 7d2af9c3..ae8c480b 100644
--- a/src/com/android/tv/dvr/provider/AsyncDvrDbTask.java
+++ b/src/com/android/tv/dvr/provider/DvrDbFuture.java
@@ -18,109 +18,111 @@ package com.android.tv.dvr.provider;
import android.content.Context;
import android.database.Cursor;
-import android.os.AsyncTask;
import android.support.annotation.Nullable;
+import android.util.Log;
import com.android.tv.common.concurrent.NamedThreadFactory;
import com.android.tv.dvr.data.ScheduledRecording;
import com.android.tv.dvr.data.SeriesRecording;
import com.android.tv.dvr.provider.DvrContract.Schedules;
import com.android.tv.dvr.provider.DvrContract.SeriesRecordings;
+import com.android.tv.util.MainThreadExecutor;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
import java.util.ArrayList;
import java.util.List;
-import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
-/** {@link AsyncTask} that defaults to executing on its own single threaded Executor Service. */
-public abstract class AsyncDvrDbTask<Params, Progress, Result>
- extends AsyncTask<Params, Progress, Result> {
+/** {@link DvrDbFuture} that defaults to executing on its own single threaded Executor Service. */
+public abstract class DvrDbFuture<ParamsT, ResultT> {
private static final NamedThreadFactory THREAD_FACTORY =
- new NamedThreadFactory(AsyncDvrDbTask.class.getSimpleName());
- private static final ExecutorService DB_EXECUTOR =
- Executors.newSingleThreadExecutor(THREAD_FACTORY);
+ new NamedThreadFactory(DvrDbFuture.class.getSimpleName());
+ private static final ListeningExecutorService DB_EXECUTOR =
+ MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor(THREAD_FACTORY));
private static DvrDatabaseHelper sDbHelper;
-
- private static synchronized DvrDatabaseHelper initializeDbHelper(Context context) {
- if (sDbHelper == null) {
- sDbHelper = new DvrDatabaseHelper(context.getApplicationContext());
- }
- return sDbHelper;
- }
+ private ListenableFuture<ResultT> mFuture;
final Context mContext;
- private AsyncDvrDbTask(Context context) {
+ private DvrDbFuture(Context context) {
mContext = context;
}
- /** Execute the task on the {@link #DB_EXECUTOR} thread. */
+ /** Execute the task on the {@link #DB_EXECUTOR} thread and return Future*/
@SafeVarargs
- public final void executeOnDbThread(Params... params) {
- executeOnExecutor(DB_EXECUTOR, params);
- }
-
- @Override
- protected final Result doInBackground(Params... params) {
- initializeDbHelper(mContext);
- return doInDvrBackground(params);
+ public final ListenableFuture<ResultT> executeOnDbThread(
+ FutureCallback<ResultT> callback, ParamsT... params) {
+ if (sDbHelper == null) {
+ sDbHelper = new DvrDatabaseHelper(mContext.getApplicationContext());
+ }
+ mFuture = DB_EXECUTOR.submit(() -> dbHelperInBackground(params));
+ Futures.addCallback(mFuture, callback, MainThreadExecutor.getInstance());
+ return mFuture;
}
- /** Executes in the background after {@link #initializeDbHelper(Context)} */
+ /** Executes in the background after initializing DbHelper} */
@Nullable
- protected abstract Result doInDvrBackground(Params... params);
+ protected abstract ResultT dbHelperInBackground(ParamsT... params);
+
+ public final boolean isCancelled() {
+ return mFuture.isCancelled();
+ }
/** Inserts schedules. */
- public static class AsyncAddScheduleTask
- extends AsyncDvrDbTask<ScheduledRecording, Void, Void> {
- public AsyncAddScheduleTask(Context context) {
+ public static class AddScheduleFuture
+ extends DvrDbFuture<ScheduledRecording, Void> {
+ public AddScheduleFuture(Context context) {
super(context);
}
@Override
- protected final Void doInDvrBackground(ScheduledRecording... params) {
+ protected final Void dbHelperInBackground(ScheduledRecording... params) {
sDbHelper.insertSchedules(params);
return null;
}
}
/** Update schedules. */
- public static class AsyncUpdateScheduleTask
- extends AsyncDvrDbTask<ScheduledRecording, Void, Void> {
- public AsyncUpdateScheduleTask(Context context) {
+ public static class UpdateScheduleFuture
+ extends DvrDbFuture<ScheduledRecording, Void> {
+ public UpdateScheduleFuture(Context context) {
super(context);
}
@Override
- protected final Void doInDvrBackground(ScheduledRecording... params) {
+ protected final Void dbHelperInBackground(ScheduledRecording... params) {
sDbHelper.updateSchedules(params);
return null;
}
}
/** Delete schedules. */
- public static class AsyncDeleteScheduleTask
- extends AsyncDvrDbTask<ScheduledRecording, Void, Void> {
- public AsyncDeleteScheduleTask(Context context) {
+ public static class DeleteScheduleFuture
+ extends DvrDbFuture<ScheduledRecording, Void> {
+ public DeleteScheduleFuture(Context context) {
super(context);
}
@Override
- protected final Void doInDvrBackground(ScheduledRecording... params) {
+ protected final Void dbHelperInBackground(ScheduledRecording... params) {
sDbHelper.deleteSchedules(params);
return null;
}
}
/** Returns all {@link ScheduledRecording}s. */
- public abstract static class AsyncDvrQueryScheduleTask
- extends AsyncDvrDbTask<Void, Void, List<ScheduledRecording>> {
- public AsyncDvrQueryScheduleTask(Context context) {
+ public static class DvrQueryScheduleFuture
+ extends DvrDbFuture<Void, List<ScheduledRecording>> {
+ public DvrQueryScheduleFuture(Context context) {
super(context);
}
@Override
@Nullable
- protected final List<ScheduledRecording> doInDvrBackground(Void... params) {
+ protected final List<ScheduledRecording> dbHelperInBackground(Void... params) {
if (isCancelled()) {
return null;
}
@@ -135,57 +137,59 @@ public abstract class AsyncDvrDbTask<Params, Progress, Result>
}
/** Inserts series recordings. */
- public static class AsyncAddSeriesRecordingTask
- extends AsyncDvrDbTask<SeriesRecording, Void, Void> {
- public AsyncAddSeriesRecordingTask(Context context) {
+ public static class AddSeriesRecordingFuture
+ extends DvrDbFuture<SeriesRecording, Void> {
+ public AddSeriesRecordingFuture(Context context) {
super(context);
}
@Override
- protected final Void doInDvrBackground(SeriesRecording... params) {
+ protected final Void dbHelperInBackground(SeriesRecording... params) {
sDbHelper.insertSeriesRecordings(params);
return null;
}
}
/** Update series recordings. */
- public static class AsyncUpdateSeriesRecordingTask
- extends AsyncDvrDbTask<SeriesRecording, Void, Void> {
- public AsyncUpdateSeriesRecordingTask(Context context) {
+ public static class UpdateSeriesRecordingFuture
+ extends DvrDbFuture<SeriesRecording, Void> {
+ public UpdateSeriesRecordingFuture(Context context) {
super(context);
}
@Override
- protected final Void doInDvrBackground(SeriesRecording... params) {
+ protected final Void dbHelperInBackground(SeriesRecording... params) {
sDbHelper.updateSeriesRecordings(params);
return null;
}
}
/** Delete series recordings. */
- public static class AsyncDeleteSeriesRecordingTask
- extends AsyncDvrDbTask<SeriesRecording, Void, Void> {
- public AsyncDeleteSeriesRecordingTask(Context context) {
+ public static class DeleteSeriesRecordingFuture
+ extends DvrDbFuture<SeriesRecording, Void> {
+ public DeleteSeriesRecordingFuture(Context context) {
super(context);
}
@Override
- protected final Void doInDvrBackground(SeriesRecording... params) {
+ protected final Void dbHelperInBackground(SeriesRecording... params) {
sDbHelper.deleteSeriesRecordings(params);
return null;
}
}
/** Returns all {@link SeriesRecording}s. */
- public abstract static class AsyncDvrQuerySeriesRecordingTask
- extends AsyncDvrDbTask<Void, Void, List<SeriesRecording>> {
- public AsyncDvrQuerySeriesRecordingTask(Context context) {
+ public static class DvrQuerySeriesRecordingFuture
+ extends DvrDbFuture<Void, List<SeriesRecording>> {
+ private static final String TAG = "DvrQuerySeriesRecording";
+
+ public DvrQuerySeriesRecordingFuture(Context context) {
super(context);
}
@Override
@Nullable
- protected final List<SeriesRecording> doInDvrBackground(Void... params) {
+ protected final List<SeriesRecording> dbHelperInBackground(Void... params) {
if (isCancelled()) {
return null;
}
@@ -195,6 +199,8 @@ public abstract class AsyncDvrDbTask<Params, Progress, Result>
while (c.moveToNext() && !isCancelled()) {
scheduledRecordings.add(SeriesRecording.fromCursor(c));
}
+ } catch (Exception e) {
+ Log.w(TAG, "Can't query dvr series recording data", e);
}
return scheduledRecordings;
}
diff --git a/src/com/android/tv/dvr/provider/DvrDbSync.java b/src/com/android/tv/dvr/provider/DvrDbSync.java
index 42bc8bcc..7658ca45 100644
--- a/src/com/android/tv/dvr/provider/DvrDbSync.java
+++ b/src/com/android/tv/dvr/provider/DvrDbSync.java
@@ -277,7 +277,6 @@ public class DvrDbSync {
}
}
} else {
- long currentTimeMs = System.currentTimeMillis();
ScheduledRecording.Builder builder =
ScheduledRecording.buildFrom(schedule)
.setEndTimeMs(program.getEndTimeUtcMillis())
@@ -361,7 +360,7 @@ public class DvrDbSync {
private final long mProgramId;
QueryProgramTask(long programId) {
- super(mDbExecutor, mContext.getContentResolver(), programId);
+ super(mDbExecutor, mContext, programId);
mProgramId = programId;
}
diff --git a/src/com/android/tv/dvr/provider/EpisodicProgramLoadTask.java b/src/com/android/tv/dvr/provider/EpisodicProgramLoadTask.java
index b7d9f3b3..02e197f1 100644
--- a/src/com/android/tv/dvr/provider/EpisodicProgramLoadTask.java
+++ b/src/com/android/tv/dvr/provider/EpisodicProgramLoadTask.java
@@ -186,7 +186,7 @@ public abstract class EpisodicProgramLoadTask {
SqlParams sqlParams = createSqlParams();
return new AsyncProgramQueryTask(
TvSingletons.getSingletons(mContext).getDbExecutor(),
- mContext.getContentResolver(),
+ mContext,
sqlParams.uri,
sqlParams.selection,
sqlParams.selectionArgs,
@@ -284,7 +284,7 @@ public abstract class EpisodicProgramLoadTask {
@Override
@WorkerThread
- public boolean filter(Cursor c) {
+ public boolean apply(Cursor c) {
if (!mLoadDisallowedProgram
&& mDisallowedProgramIds.contains(c.getLong(PROGRAM_ID_INDEX))) {
return false;
@@ -318,10 +318,10 @@ public abstract class EpisodicProgramLoadTask {
}
@Override
- public boolean filter(Cursor c) {
+ public boolean apply(Cursor c) {
return (mLoadCurrentProgram || c.getLong(START_TIME_INDEX) > System.currentTimeMillis())
&& c.getInt(RECORDING_PROHIBITED_INDEX) != 0
- && super.filter(c);
+ && super.apply(c);
}
}
diff --git a/src/com/android/tv/dvr/recorder/InputTaskScheduler.java b/src/com/android/tv/dvr/recorder/InputTaskScheduler.java
index 1021b2bc..7d9f7fe2 100644
--- a/src/com/android/tv/dvr/recorder/InputTaskScheduler.java
+++ b/src/com/android/tv/dvr/recorder/InputTaskScheduler.java
@@ -279,7 +279,8 @@ public class InputTaskScheduler {
if (schedule.getEndTimeMs() - currentTimeMs
<= MIN_REMAIN_DURATION_PERCENT * schedule.getDuration()) {
Log.e(TAG, "Error! Program ended before recording started:" + schedule);
- fail(schedule,
+ fail(
+ schedule,
ScheduledRecording.FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED);
iter.remove();
}
@@ -394,19 +395,16 @@ public class InputTaskScheduler {
private void fail(ScheduledRecording schedule, int reason) {
// 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,
- reason);
- }
+ () -> {
+ 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,
+ reason);
}
});
}
diff --git a/src/com/android/tv/dvr/recorder/RecordingTask.java b/src/com/android/tv/dvr/recorder/RecordingTask.java
index 07a29e51..98f668a0 100644
--- a/src/com/android/tv/dvr/recorder/RecordingTask.java
+++ b/src/com/android/tv/dvr/recorder/RecordingTask.java
@@ -17,10 +17,11 @@
package com.android.tv.dvr.recorder;
import android.annotation.TargetApi;
+import android.content.ContentUris;
+import android.content.ContentValues;
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;
@@ -36,6 +37,7 @@ import com.android.tv.InputSessionManager.RecordingSession;
import com.android.tv.R;
import com.android.tv.TvSingletons;
import com.android.tv.common.SoftPreconditions;
+import com.android.tv.common.compat.TvRecordingClientCompat.RecordingCallbackCompat;
import com.android.tv.common.util.Clock;
import com.android.tv.common.util.CommonUtils;
import com.android.tv.data.api.Channel;
@@ -55,7 +57,7 @@ import java.util.concurrent.TimeUnit;
*/
@WorkerThread
@TargetApi(Build.VERSION_CODES.N)
-public class RecordingTask extends RecordingCallback
+public class RecordingTask extends RecordingCallbackCompat
implements Handler.Callback, DvrManager.Listener {
private static final String TAG = "RecordingTask";
private static final boolean DEBUG = false;
@@ -223,6 +225,14 @@ public class RecordingTask extends RecordingCallback
}
@Override
+ public void onRecordingStarted(String inputId, String recUri) {
+ if (DEBUG) {
+ Log.d(TAG, "onRecordingStart");
+ }
+ addRecordedProgramId(recUri);
+ }
+
+ @Override
public void onRecordingStopped(Uri recordedProgramUri) {
Log.i(TAG, "Recording Stopped: " + mScheduledRecording);
Log.i(TAG, "Recording Stopped: stored as " + recordedProgramUri);
@@ -340,10 +350,8 @@ public class RecordingTask extends RecordingCallback
}
private void failAndQuit(Integer reason) {
- if (DEBUG) Log.d(TAG, "failAndQuit");
- updateRecordingState(
- ScheduledRecording.STATE_RECORDING_FAILED,
- reason);
+ Log.w(TAG, "Recording " + mScheduledRecording + " failed with code " + reason);
+ updateRecordingState(ScheduledRecording.STATE_RECORDING_FAILED, reason);
mState = State.ERROR;
sendRemove();
}
@@ -450,6 +458,7 @@ public class RecordingTask extends RecordingCallback
private void updateRecordingState(@ScheduledRecording.RecordingState int state) {
updateRecordingState(state, null);
}
+
private void updateRecordingState(
@ScheduledRecording.RecordingState int state, @Nullable Integer reason) {
if (DEBUG) {
@@ -471,9 +480,7 @@ public class RecordingTask extends RecordingCallback
// has been updated. mScheduledRecording will be updated from
// onScheduledRecordingStateChanged.
ScheduledRecording.Builder builder =
- ScheduledRecording
- .buildFrom(schedule)
- .setState(state);
+ ScheduledRecording.buildFrom(schedule).setState(state);
if (state == ScheduledRecording.STATE_RECORDING_FAILED
&& reason != null) {
builder.setFailedReason(reason);
@@ -484,6 +491,43 @@ public class RecordingTask extends RecordingCallback
});
}
+ private void addRecordedProgramId(String recordedProgramUri) {
+ if (DEBUG) {
+ Log.d(TAG, "Adding Recorded Program Id to " + mScheduledRecording);
+ }
+ mRecordedProgramUri = Uri.parse(recordedProgramUri);
+ long id = ContentUris.parseId(mRecordedProgramUri);
+ mScheduledRecording =
+ ScheduledRecording.buildFrom(mScheduledRecording).setRecordedProgramId(id).build();
+ ContentValues values = new ContentValues();
+ values.put(
+ TvContract.RecordedPrograms.COLUMN_RECORDING_DURATION_MILLIS,
+ mScheduledRecording.getEndTimeMs() - mScheduledRecording.getStartTimeMs());
+ values.put(
+ TvContract.RecordedPrograms.COLUMN_END_TIME_UTC_MILLIS,
+ mScheduledRecording.getEndTimeMs());
+ mContext.getContentResolver().update(mRecordedProgramUri, values, null, null);
+ 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.
+ ScheduledRecording.Builder builder =
+ ScheduledRecording.buildFrom(schedule).setRecordedProgramId(id);
+ mDataManager.updateScheduledRecording(builder.build());
+ }
+ }
+ });
+ }
+
@Override
public void onStopRecordingRequested(ScheduledRecording recording) {
if (recording.getId() != mScheduledRecording.getId()) {
@@ -553,7 +597,7 @@ public class RecordingTask extends RecordingCallback
@Override
public void run() {
if (mRecordedProgramUri != null) {
- mDvrManager.removeRecordedProgram(mRecordedProgramUri);
+ mDvrManager.removeRecordedProgram(mRecordedProgramUri, true);
}
}
});
diff --git a/src/com/android/tv/dvr/recorder/SeriesRecordingScheduler.java b/src/com/android/tv/dvr/recorder/SeriesRecordingScheduler.java
index 4f7a789b..696038cf 100644
--- a/src/com/android/tv/dvr/recorder/SeriesRecordingScheduler.java
+++ b/src/com/android/tv/dvr/recorder/SeriesRecordingScheduler.java
@@ -29,7 +29,6 @@ import android.util.Log;
import android.util.LongSparseArray;
import com.android.tv.TvSingletons;
import com.android.tv.common.SoftPreconditions;
-import com.android.tv.common.experiments.Experiments;
import com.android.tv.common.util.CollectionUtils;
import com.android.tv.common.util.SharedPreferencesUtils;
import com.android.tv.data.Program;
@@ -48,7 +47,6 @@ 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;
@@ -261,14 +259,11 @@ public class SeriesRecordingScheduler {
}
private void executeFetchSeriesInfoTask(SeriesRecording seriesRecording) {
- if (Experiments.CLOUD_EPG.get()) {
- FetchSeriesInfoTask task =
- new FetchSeriesInfoTask(
- seriesRecording,
- TvSingletons.getSingletons(mContext).providesEpgReader());
- task.execute();
- mFetchSeriesInfoTasks.put(seriesRecording.getId(), task);
- }
+ FetchSeriesInfoTask task =
+ new FetchSeriesInfoTask(
+ seriesRecording, TvSingletons.getSingletons(mContext).providesEpgReader());
+ task.execute();
+ mFetchSeriesInfoTasks.put(seriesRecording.getId(), task);
}
/** Pauses the updates of the series recordings. */
@@ -442,21 +437,18 @@ public class SeriesRecordingScheduler {
List<Program> programsForEpisode = entry.getValue();
Collections.sort(
programsForEpisode,
- new Comparator<Program>() {
- @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);
+ (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
diff --git a/src/com/android/tv/dvr/ui/ChangeImageTransformWithScaledParent.java b/src/com/android/tv/dvr/ui/ChangeImageTransformWithScaledParent.java
index 32679421..9cd91a64 100644
--- a/src/com/android/tv/dvr/ui/ChangeImageTransformWithScaledParent.java
+++ b/src/com/android/tv/dvr/ui/ChangeImageTransformWithScaledParent.java
@@ -27,13 +27,15 @@ import android.view.View;
import android.widget.ImageView;
import android.widget.ImageView.ScaleType;
import com.android.tv.R;
+import com.android.tv.ui.DetailsActivity;
+
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}.
+ * DetailsActivity}.
*/
public class ChangeImageTransformWithScaledParent extends ChangeImageTransform {
private static final String PROPNAME_MATRIX = "android:changeImageTransform:matrix";
diff --git a/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java b/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java
index fce94230..5e3caa9c 100644
--- a/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java
@@ -71,7 +71,7 @@ public class DvrAlreadyRecordedFragment extends DvrGuidedStepFragment {
public Guidance onCreateGuidance(Bundle savedInstanceState) {
String title = getString(R.string.dvr_already_recorded_dialog_title);
String description = getString(R.string.dvr_already_recorded_dialog_description);
- Drawable image = getResources().getDrawable(R.drawable.ic_warning_white_96dp, null);
+ Drawable image = getResources().getDrawable(R.drawable.quantum_ic_warning_white_96, null);
return new Guidance(title, description, null, image);
}
diff --git a/src/com/android/tv/dvr/ui/DvrAlreadyScheduledFragment.java b/src/com/android/tv/dvr/ui/DvrAlreadyScheduledFragment.java
index 456ad830..a6bbe137 100644
--- a/src/com/android/tv/dvr/ui/DvrAlreadyScheduledFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrAlreadyScheduledFragment.java
@@ -78,7 +78,7 @@ public class DvrAlreadyScheduledFragment extends DvrGuidedStepFragment {
getContext(),
mDuplicate.getStartTimeMs(),
DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_SHOW_DATE));
- Drawable image = getResources().getDrawable(R.drawable.ic_warning_white_96dp, null);
+ Drawable image = getResources().getDrawable(R.drawable.quantum_ic_warning_white_96, null);
return new Guidance(title, description, null, image);
}
diff --git a/src/com/android/tv/dvr/ui/DvrConflictFragment.java b/src/com/android/tv/dvr/ui/DvrConflictFragment.java
index 65759555..649cc89a 100644
--- a/src/com/android/tv/dvr/ui/DvrConflictFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrConflictFragment.java
@@ -205,7 +205,7 @@ public abstract class DvrConflictFragment extends DvrGuidedStepFragment {
if (description == null) {
dismissDialog();
}
- Drawable icon = getResources().getDrawable(R.drawable.ic_error_white_48dp, null);
+ Drawable icon = getResources().getDrawable(R.drawable.quantum_ic_error_white_48, null);
return new Guidance(title, descriptionPrefix + " " + description, null, icon);
}
@@ -265,7 +265,7 @@ public abstract class DvrConflictFragment extends DvrGuidedStepFragment {
if (description == null) {
dismissDialog();
}
- Drawable icon = getResources().getDrawable(R.drawable.ic_error_white_48dp, null);
+ Drawable icon = getResources().getDrawable(R.drawable.quantum_ic_error_white_48, null);
return new Guidance(title, descriptionPrefix + " " + description, null, icon);
}
diff --git a/src/com/android/tv/dvr/ui/DvrFutureProgramInfoFragment.java b/src/com/android/tv/dvr/ui/DvrFutureProgramInfoFragment.java
deleted file mode 100644
index 677a6cbb..00000000
--- a/src/com/android/tv/dvr/ui/DvrFutureProgramInfoFragment.java
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.tv.dvr.ui;
-
-import android.app.Activity;
-import android.os.Bundle;
-import android.support.v17.leanback.widget.GuidanceStylist;
-import android.support.v17.leanback.widget.GuidedAction;
-import com.android.tv.TvSingletons;
-import com.android.tv.data.Program;
-import com.android.tv.dvr.data.ScheduledRecording;
-import com.android.tv.util.Utils;
-import java.util.List;
-
-/**
- * A fragment which shows the formation of a program.
- */
-public class DvrFutureProgramInfoFragment extends DvrGuidedStepFragment {
- private static final long ACTION_ID_VIEW_SCHEDULE = 1;
- private ScheduledRecording mScheduledRecording;
- private Program mProgram;
-
- @Override
- public GuidanceStylist.Guidance onCreateGuidance(Bundle savedInstanceState) {
- long startTime = mProgram.getStartTimeUtcMillis();
- // TODO(b/71717923): use R.string when the strings are finalized
- StringBuilder description = new StringBuilder()
- .append("This program will start at ")
- .append(Utils.getDurationString(getContext(), startTime, startTime, false));
- if (mScheduledRecording != null) {
- description.append("\nThis program has been scheduled for recording.");
- }
- return new GuidanceStylist.Guidance(
- mProgram.getTitle(), description.toString(), null, null);
- }
-
- @Override
- public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) {
- Activity activity = getActivity();
- mProgram = getArguments().getParcelable(DvrHalfSizedDialogFragment.KEY_PROGRAM);
- mScheduledRecording =
- TvSingletons.getSingletons(getContext())
- .getDvrDataManager()
- .getScheduledRecordingForProgramId(mProgram.getId());
- actions.add(
- new GuidedAction.Builder(activity)
- .id(GuidedAction.ACTION_ID_OK)
- .title(android.R.string.ok)
- .build());
- if (mScheduledRecording != null) {
- actions.add(
- new GuidedAction.Builder(activity)
- .id(ACTION_ID_VIEW_SCHEDULE)
- .title("View schedules")
- .build());
- }
-
- }
-
- @Override
- public void onTrackedGuidedActionClicked(GuidedAction action) {
- if (action.getId() == ACTION_ID_VIEW_SCHEDULE) {
- DvrUiHelper.startSchedulesActivity(getContext(), mScheduledRecording);
- return;
- }
- dismissDialog();
- }
-
- @Override
- public String getTrackerPrefix() {
- return "DvrFutureProgramInfoFragment";
- }
-}
diff --git a/src/com/android/tv/dvr/ui/DvrHalfSizedDialogFragment.java b/src/com/android/tv/dvr/ui/DvrHalfSizedDialogFragment.java
index 4a713703..e6b54f67 100644
--- a/src/com/android/tv/dvr/ui/DvrHalfSizedDialogFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrHalfSizedDialogFragment.java
@@ -18,17 +18,20 @@ package com.android.tv.dvr.ui;
import android.app.Activity;
import android.content.Context;
+import android.content.DialogInterface;
import android.os.Bundle;
import android.support.v17.leanback.app.GuidedStepFragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+
import com.android.tv.MainActivity;
import com.android.tv.R;
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;
+import com.android.tv.ui.DetailsActivity;
public class DvrHalfSizedDialogFragment extends HalfSizedDialogFragment {
/** Key for input ID. Type: String. */
@@ -187,11 +190,27 @@ public class DvrHalfSizedDialogFragment extends HalfSizedDialogFragment {
}
}
- /** A dialog fragment for {@link DvrFutureProgramInfoFragment}. */
- public static class DvrFutureProgramInfoDialogFragment extends DvrGuidedStepDialogFragment {
+ /** A dialog fragment for {@link DvrWriteStoragePermissionRationaleFragment}. */
+ public static class DvrWriteStoragePermissionRationaleDialogFragment
+ extends DvrGuidedStepDialogFragment {
@Override
- protected DvrGuidedStepFragment onCreateGuidedStepFragment() {
- return new DvrFutureProgramInfoFragment();
+ protected DvrWriteStoragePermissionRationaleFragment onCreateGuidedStepFragment() {
+ return new DvrWriteStoragePermissionRationaleFragment();
+ }
+
+ @Override
+ public void onDismiss(DialogInterface dialog) {
+ Activity activity = getActivity();
+ if (activity instanceof DetailsActivity) {
+ activity.requestPermissions(
+ new String[] {"android.permission.WRITE_EXTERNAL_STORAGE"},
+ DetailsActivity.REQUEST_DELETE);
+ } else if (activity instanceof DvrSeriesDeletionActivity) {
+ activity.requestPermissions(
+ new String[] {"android.permission.WRITE_EXTERNAL_STORAGE"},
+ DvrSeriesDeletionActivity.REQUEST_DELETE);
+ }
+ super.onDismiss(dialog);
}
}
}
diff --git a/src/com/android/tv/dvr/ui/DvrMissingStorageErrorFragment.java b/src/com/android/tv/dvr/ui/DvrMissingStorageErrorFragment.java
index e5f40260..02b2da1d 100644
--- a/src/com/android/tv/dvr/ui/DvrMissingStorageErrorFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrMissingStorageErrorFragment.java
@@ -25,7 +25,7 @@ import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
import android.support.v17.leanback.widget.GuidedAction;
import android.util.Log;
import com.android.tv.R;
-import com.android.tv.dvr.ui.browse.DvrDetailsActivity;
+import com.android.tv.ui.DetailsActivity;
import java.util.List;
public class DvrMissingStorageErrorFragment extends DvrGuidedStepFragment {
@@ -65,7 +65,7 @@ public class DvrMissingStorageErrorFragment extends DvrGuidedStepFragment {
@Override
public void onTrackedGuidedActionClicked(GuidedAction action) {
Activity activity = getActivity();
- if (activity instanceof DvrDetailsActivity) {
+ if (activity instanceof DetailsActivity) {
activity.finish();
} else {
dismissDialog();
diff --git a/src/com/android/tv/dvr/ui/DvrScheduleFragment.java b/src/com/android/tv/dvr/ui/DvrScheduleFragment.java
index 5251e140..72603d03 100644
--- a/src/com/android/tv/dvr/ui/DvrScheduleFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrScheduleFragment.java
@@ -34,7 +34,6 @@ 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.ui.DvrConflictFragment.DvrProgramConflictFragment;
-import com.android.tv.util.Utils;
import java.util.Collections;
import java.util.List;
@@ -104,12 +103,7 @@ public class DvrScheduleFragment extends DvrGuidedStepFragment {
mProgram.getEndTimeUtcMillis(),
DateUtils.FORMAT_SHOW_TIME));
} else {
- description =
- Utils.getDurationString(
- context,
- mProgram.getStartTimeUtcMillis(),
- mProgram.getEndTimeUtcMillis(),
- true);
+ description = mProgram.getDurationString(context);
}
actions.add(
new GuidedAction.Builder(context)
diff --git a/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java b/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java
index a2ae1f97..a237f1d2 100644
--- a/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java
+++ b/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java
@@ -17,16 +17,34 @@
package com.android.tv.dvr.ui;
import android.app.Activity;
+import android.content.pm.PackageManager;
import android.os.Bundle;
+import android.support.annotation.NonNull;
import android.support.v17.leanback.app.GuidedStepFragment;
+import android.util.Log;
+import android.widget.Toast;
+
import com.android.tv.R;
import com.android.tv.Starter;
+import com.android.tv.TvSingletons;
+import com.android.tv.dvr.DvrManager;
+
+import java.util.ArrayList;
+import java.util.List;
/** Activity to show details view in DVR. */
public class DvrSeriesDeletionActivity extends Activity {
+ private static final String TAG = "DvrSeriesDeletionActivity";
+
/** Name of series id added to the Intent. */
public static final String SERIES_RECORDING_ID = "series_recording_id";
+ public static final int REQUEST_DELETE = 1;
+ public static final long INVALID_SERIES_RECORDING_ID = -1;
+
+ private long mSeriesRecordingId = INVALID_SERIES_RECORDING_ID;
+ private final List<Long> mIdsToDelete = new ArrayList<>();
+
@Override
public void onCreate(Bundle savedInstanceState) {
Starter.start(this);
@@ -34,9 +52,61 @@ 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) {
+ mSeriesRecordingId =
+ getIntent().getLongExtra(SERIES_RECORDING_ID, INVALID_SERIES_RECORDING_ID);
DvrSeriesDeletionFragment deletionFragment = new DvrSeriesDeletionFragment();
deletionFragment.setArguments(getIntent().getExtras());
GuidedStepFragment.addAsRoot(this, deletionFragment, R.id.dvr_settings_view_frame);
}
}
+
+ @Override
+ public void onRequestPermissionsResult(
+ int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ switch (requestCode) {
+ case REQUEST_DELETE:
+ // If request is cancelled, the result arrays are empty.
+ if (grantResults.length > 0
+ && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ deleteSelectedIds(true);
+ } else {
+ // NOTE: If Live TV ever supports both embedded and separate DVR inputs
+ // then we should try to do the delete regardless.
+ Log.i(
+ TAG,
+ "Write permission denied, Not trying to delete the files for series "
+ + mSeriesRecordingId);
+ deleteSelectedIds(false);
+ }
+ break;
+ default:
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ }
+ }
+
+ private void deleteSelectedIds(boolean deleteFiles) {
+ TvSingletons singletons = TvSingletons.getSingletons(this);
+ int recordingSize =
+ singletons.getDvrDataManager().getRecordedPrograms(mSeriesRecordingId).size();
+ if (!mIdsToDelete.isEmpty()) {
+ DvrManager dvrManager = singletons.getDvrManager();
+ dvrManager.removeRecordedPrograms(mIdsToDelete, deleteFiles);
+ }
+ Toast.makeText(
+ this,
+ getResources()
+ .getQuantityString(
+ R.plurals.dvr_msg_episodes_deleted,
+ mIdsToDelete.size(),
+ mIdsToDelete.size(),
+ recordingSize),
+ Toast.LENGTH_LONG)
+ .show();
+ finish();
+ }
+
+ void setIdsToDelete(List<Long> ids) {
+ mIdsToDelete.clear();
+ mIdsToDelete.addAll(ids);
+ }
}
diff --git a/src/com/android/tv/dvr/ui/DvrSeriesDeletionFragment.java b/src/com/android/tv/dvr/ui/DvrSeriesDeletionFragment.java
index 685f0a58..ff213231 100644
--- a/src/com/android/tv/dvr/ui/DvrSeriesDeletionFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrSeriesDeletionFragment.java
@@ -29,6 +29,7 @@ import android.widget.Toast;
import com.android.tv.R;
import com.android.tv.TvSingletons;
import com.android.tv.common.SoftPreconditions;
+import com.android.tv.common.util.PermissionUtils;
import com.android.tv.dvr.DvrDataManager;
import com.android.tv.dvr.DvrManager;
import com.android.tv.dvr.DvrWatchedPositionManager;
@@ -53,10 +54,12 @@ public class DvrSeriesDeletionFragment extends GuidedStepFragment {
private static final long ACTION_ID_SELECT_ALL = -111;
private static final long ACTION_ID_DELETE = -112;
+ private DvrManager mDvrManager;
private DvrDataManager mDvrDataManager;
private DvrWatchedPositionManager mDvrWatchedPositionManager;
private List<RecordedProgram> mRecordings;
private final Set<Long> mWatchedRecordings = new HashSet<>();
+ private final List<Long> mIdsToDelete = new ArrayList<>();
private boolean mAllSelected;
private long mSeriesRecordingId;
private int mOneLineActionHeight;
@@ -67,9 +70,10 @@ public class DvrSeriesDeletionFragment extends GuidedStepFragment {
mSeriesRecordingId =
getArguments().getLong(DvrSeriesDeletionActivity.SERIES_RECORDING_ID, -1);
SoftPreconditions.checkArgument(mSeriesRecordingId != -1);
- mDvrDataManager = TvSingletons.getSingletons(context).getDvrDataManager();
- mDvrWatchedPositionManager =
- TvSingletons.getSingletons(context).getDvrWatchedPositionManager();
+ TvSingletons singletons = TvSingletons.getSingletons(context);
+ mDvrManager = singletons.getDvrManager();
+ mDvrDataManager = singletons.getDvrDataManager();
+ mDvrWatchedPositionManager = singletons.getDvrWatchedPositionManager();
mRecordings = mDvrDataManager.getRecordedPrograms(mSeriesRecordingId);
mOneLineActionHeight =
getResources()
@@ -158,28 +162,7 @@ public class DvrSeriesDeletionFragment extends GuidedStepFragment {
public void onGuidedActionClicked(GuidedAction action) {
long actionId = action.getId();
if (actionId == ACTION_ID_DELETE) {
- List<Long> 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 = TvSingletons.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();
+ delete();
} else if (actionId == GuidedAction.ACTION_ID_CANCEL) {
finishGuidedStepFragments();
} else if (actionId == ACTION_ID_SELECT_WATCHED) {
@@ -234,6 +217,51 @@ public class DvrSeriesDeletionFragment extends GuidedStepFragment {
};
}
+ private void delete() {
+ mIdsToDelete.clear();
+ for (GuidedAction guidedAction : getActions()) {
+ if (guidedAction.getCheckSetId() == GuidedAction.CHECKBOX_CHECK_SET_ID
+ && guidedAction.isChecked()) {
+ mIdsToDelete.add(guidedAction.getId());
+ }
+ }
+ ((DvrSeriesDeletionActivity) getActivity()).setIdsToDelete(mIdsToDelete);
+ if (!PermissionUtils.hasWriteExternalStorage(getContext())
+ && doesAnySelectedRecordedProgramNeedWritePermission()) {
+ DvrUiHelper.showWriteStoragePermissionRationaleDialog(getActivity());
+ } else {
+ deleteSelectedIds();
+ }
+ }
+
+ private boolean doesAnySelectedRecordedProgramNeedWritePermission() {
+ for (RecordedProgram r : mRecordings) {
+ if (mIdsToDelete.contains(r.getId())
+ && DvrManager.isFile(r.getDataUri())
+ && !DvrManager.isFromBundledInput(r)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void deleteSelectedIds() {
+ if (!mIdsToDelete.isEmpty()) {
+ mDvrManager.removeRecordedPrograms(mIdsToDelete, true);
+ }
+ Toast.makeText(
+ getContext(),
+ getResources()
+ .getQuantityString(
+ R.plurals.dvr_msg_episodes_deleted,
+ mIdsToDelete.size(),
+ mIdsToDelete.size(),
+ mRecordings.size()),
+ Toast.LENGTH_LONG)
+ .show();
+ finishGuidedStepFragments();
+ }
+
private String getWatchedString(long watchedPositionMs, long durationMs) {
if (durationMs > WATCHED_TIME_UNIT_THRESHOLD) {
return getResources()
diff --git a/src/com/android/tv/dvr/ui/DvrSeriesScheduledFragment.java b/src/com/android/tv/dvr/ui/DvrSeriesScheduledFragment.java
index edb62c96..c6e26850 100644
--- a/src/com/android/tv/dvr/ui/DvrSeriesScheduledFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrSeriesScheduledFragment.java
@@ -101,9 +101,9 @@ public class DvrSeriesScheduledFragment extends DvrGuidedStepFragment {
String title = getString(R.string.dvr_series_recording_dialog_title);
Drawable icon;
if (!mHasConflict) {
- icon = getResources().getDrawable(R.drawable.ic_check_circle_white_48dp, null);
+ icon = getResources().getDrawable(R.drawable.quantum_ic_check_circle_white_48, null);
} else {
- icon = getResources().getDrawable(R.drawable.ic_error_white_48dp, null);
+ icon = getResources().getDrawable(R.drawable.quantum_ic_error_white_48, null);
}
return new GuidanceStylist.Guidance(title, getDescription(), null, icon);
}
diff --git a/src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java b/src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java
index e93387ab..1ab4c500 100644
--- a/src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java
@@ -126,7 +126,7 @@ public class DvrStopRecordingFragment extends DvrGuidedStepFragment {
} else {
description = getString(R.string.dvr_stop_recording_dialog_description);
}
- Drawable image = getResources().getDrawable(R.drawable.ic_warning_white_96dp, null);
+ Drawable image = getResources().getDrawable(R.drawable.quantum_ic_warning_white_96, null);
return new Guidance(title, description, null, image);
}
diff --git a/src/com/android/tv/dvr/ui/DvrUiHelper.java b/src/com/android/tv/dvr/ui/DvrUiHelper.java
index 16afbdef..a121cf99 100644
--- a/src/com/android/tv/dvr/ui/DvrUiHelper.java
+++ b/src/com/android/tv/dvr/ui/DvrUiHelper.java
@@ -37,10 +37,10 @@ import android.text.TextUtils;
import android.text.style.TextAppearanceSpan;
import android.widget.ImageView;
import android.widget.Toast;
+
import com.android.tv.MainActivity;
import com.android.tv.R;
import com.android.tv.TvSingletons;
-import com.android.tv.common.BuildConfig;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.common.recording.RecordingStorageStatusManager;
import com.android.tv.common.util.CommonUtils;
@@ -57,7 +57,6 @@ import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrAlreadyRecordedDialog
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.DvrFutureProgramInfoDialogFragment;
import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrInsufficientSpaceErrorDialogFragment;
import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrMissingStorageErrorDialogFragment;
import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrNoFreeSpaceErrorDialogFragment;
@@ -65,15 +64,17 @@ import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrProgramConflictDialog
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.DvrHalfSizedDialogFragment.DvrWriteStoragePermissionRationaleDialogFragment;
import com.android.tv.dvr.ui.browse.DvrBrowseActivity;
-import com.android.tv.dvr.ui.browse.DvrDetailsActivity;
import com.android.tv.dvr.ui.list.DvrHistoryActivity;
import com.android.tv.dvr.ui.list.DvrSchedulesActivity;
import com.android.tv.dvr.ui.list.DvrSchedulesFragment;
import com.android.tv.dvr.ui.list.DvrSeriesSchedulesFragment;
import com.android.tv.dvr.ui.playback.DvrPlaybackActivity;
+import com.android.tv.ui.DetailsActivity;
import com.android.tv.util.ToastUtils;
import com.android.tv.util.Utils;
+
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -241,13 +242,9 @@ public class DvrUiHelper {
}
/** Shows program information dialog. */
- public static void showProgramInfoDialog(Activity activity, Program program) {
- if (program == null || !BuildConfig.ENG) {
- return;
- }
- Bundle args = new Bundle();
- args.putParcelable(DvrHalfSizedDialogFragment.KEY_PROGRAM, program);
- showDialogFragment(activity, new DvrFutureProgramInfoDialogFragment(), args, false, true);
+ public static void showWriteStoragePermissionRationaleDialog(Activity activity) {
+ showDialogFragment(activity, new DvrWriteStoragePermissionRationaleDialogFragment(),
+ new Bundle(), false, false);
}
/**
@@ -577,47 +574,43 @@ public class DvrUiHelper {
if (dvrItem == null) {
return;
}
- Intent intent = new Intent(activity, DvrDetailsActivity.class);
+ Intent intent = new Intent(activity, DetailsActivity.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;
+ viewType = DetailsActivity.SCHEDULED_RECORDING_VIEW;
} else if (schedule.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) {
- viewType = DvrDetailsActivity.CURRENT_RECORDING_VIEW;
+ viewType = DetailsActivity.CURRENT_RECORDING_VIEW;
} else if (schedule.getState() == ScheduledRecording.STATE_RECORDING_FINISHED
&& schedule.getRecordedProgramId() != null) {
recordingId = schedule.getRecordedProgramId();
- viewType = DvrDetailsActivity.RECORDED_PROGRAM_VIEW;
+ viewType = DetailsActivity.RECORDED_PROGRAM_VIEW;
} else if (schedule.getState() == ScheduledRecording.STATE_RECORDING_FAILED) {
- viewType = DvrDetailsActivity.SCHEDULED_RECORDING_VIEW;
+ viewType = DetailsActivity.SCHEDULED_RECORDING_VIEW;
hideViewSchedule = true;
- // TODO(b/72638385): pass detailed error message
- intent.putExtra(
- DvrDetailsActivity.EXTRA_FAILED_MESSAGE,
- activity.getString(R.string.dvr_recording_failed));
} else {
return;
}
} else if (dvrItem instanceof RecordedProgram) {
recordingId = ((RecordedProgram) dvrItem).getId();
- viewType = DvrDetailsActivity.RECORDED_PROGRAM_VIEW;
+ viewType = DetailsActivity.RECORDED_PROGRAM_VIEW;
} else if (dvrItem instanceof SeriesRecording) {
recordingId = ((SeriesRecording) dvrItem).getId();
- viewType = DvrDetailsActivity.SERIES_RECORDING_VIEW;
+ viewType = DetailsActivity.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);
+ intent.putExtra(DetailsActivity.RECORDING_ID, recordingId);
+ intent.putExtra(DetailsActivity.DETAILS_VIEW_TYPE, viewType);
+ intent.putExtra(DetailsActivity.HIDE_VIEW_SCHEDULE, hideViewSchedule);
Bundle bundle = null;
if (imageView != null) {
bundle =
ActivityOptionsCompat.makeSceneTransitionAnimation(
- activity, imageView, DvrDetailsActivity.SHARED_ELEMENT_NAME)
+ activity, imageView, DetailsActivity.SHARED_ELEMENT_NAME)
.toBundle();
}
activity.startActivity(intent, bundle);
diff --git a/src/com/android/tv/dvr/ui/DvrWriteStoragePermissionRationaleFragment.java b/src/com/android/tv/dvr/ui/DvrWriteStoragePermissionRationaleFragment.java
new file mode 100644
index 00000000..c93f5831
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/DvrWriteStoragePermissionRationaleFragment.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.dvr.ui;
+
+import android.app.Activity;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.support.v17.leanback.widget.GuidanceStylist;
+import android.support.v17.leanback.widget.GuidedAction;
+
+import com.android.tv.R;
+
+import java.util.List;
+
+/**
+ * A fragment which shows the rationale when requesting android.permission.WRITE_EXTERNAL_STORAGE.
+ */
+public class DvrWriteStoragePermissionRationaleFragment extends DvrGuidedStepFragment {
+ @Override
+ public GuidanceStylist.Guidance onCreateGuidance(Bundle savedInstanceState) {
+ Resources res = getContext().getResources();
+ String title = res.getString(R.string.write_storage_permission_rationale_title);
+ String description = res.getString(R.string.write_storage_permission_rationale_description);
+ return new GuidanceStylist.Guidance(title, description, null, null);
+ }
+
+ @Override
+ public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) {
+ Activity activity = getActivity();
+ actions.add(
+ new GuidedAction.Builder(activity)
+ .id(GuidedAction.ACTION_ID_OK)
+ .title(android.R.string.ok)
+ .build());
+ }
+
+ @Override
+ public void onTrackedGuidedActionClicked(GuidedAction action) {
+ dismissDialog();
+ }
+
+ @Override
+ public String getTrackerPrefix() {
+ return "DvrWriteStoragePermissionRationaleFragment";
+ }
+}
diff --git a/src/com/android/tv/dvr/ui/browse/ActionPresenterSelector.java b/src/com/android/tv/dvr/ui/browse/ActionPresenterSelector.java
index f3a6fea4..41ace9a4 100644
--- a/src/com/android/tv/dvr/ui/browse/ActionPresenterSelector.java
+++ b/src/com/android/tv/dvr/ui/browse/ActionPresenterSelector.java
@@ -27,9 +27,11 @@ 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 {
+/**
+ * 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.
+ */
+public class ActionPresenterSelector extends PresenterSelector {
private final Presenter mOneLineActionPresenter = new OneLineActionPresenter();
private final Presenter mTwoLineActionPresenter = new TwoLineActionPresenter();
private final Presenter[] mPresenters =
diff --git a/src/com/android/tv/dvr/ui/browse/CurrentRecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/CurrentRecordingDetailsFragment.java
index 7e7e1f75..8c311d68 100644
--- a/src/com/android/tv/dvr/ui/browse/CurrentRecordingDetailsFragment.java
+++ b/src/com/android/tv/dvr/ui/browse/CurrentRecordingDetailsFragment.java
@@ -18,23 +18,34 @@ package com.android.tv.dvr.ui.browse;
import android.content.Context;
import android.content.res.Resources;
+import android.media.tv.TvInputManager;
import android.support.v17.leanback.widget.Action;
import android.support.v17.leanback.widget.OnActionClickedListener;
import android.support.v17.leanback.widget.SparseArrayObjectAdapter;
import com.android.tv.R;
import com.android.tv.TvSingletons;
+import com.android.tv.common.flags.has.HasConcurrentDvrPlaybackFlags;
import com.android.tv.dialog.HalfSizedDialogFragment;
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.ScheduledRecording;
import com.android.tv.dvr.ui.DvrStopRecordingFragment;
import com.android.tv.dvr.ui.DvrUiHelper;
+import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags;
/** {@link RecordingDetailsFragment} for current recording in DVR. */
public class CurrentRecordingDetailsFragment extends RecordingDetailsFragment {
private static final int ACTION_STOP_RECORDING = 1;
+ private static final int ACTION_RESUME_PLAYING = 2;
+ private static final int ACTION_PLAY_FROM_BEGINNING = 3;
private DvrDataManager mDvrDataManger;
+ private RecordedProgram mRecordedProgram;
+ private DvrWatchedPositionManager mDvrWatchedPositionManager;
+ private ConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags;
+ private boolean mPaused;
private final DvrDataManager.ScheduledRecordingListener mScheduledRecordingListener =
new DvrDataManager.ScheduledRecordingListener() {
@Override
@@ -68,10 +79,32 @@ public class CurrentRecordingDetailsFragment extends RecordingDetailsFragment {
super.onAttach(context);
mDvrDataManger = TvSingletons.getSingletons(context).getDvrDataManager();
mDvrDataManger.addScheduledRecordingListener(mScheduledRecordingListener);
+ mDvrWatchedPositionManager =
+ TvSingletons.getSingletons(getActivity()).getDvrWatchedPositionManager();
+ mConcurrentDvrPlaybackFlags = HasConcurrentDvrPlaybackFlags.fromContext(context);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ if (mPaused) {
+ updateActions();
+ mPaused = false;
+ }
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ mPaused = true;
}
@Override
protected SparseArrayObjectAdapter onCreateActionsAdapter() {
+ Long recordedProgramId = getRecording().getRecordedProgramId();
+ if (recordedProgramId != null) {
+ mRecordedProgram = mDvrDataManger.getRecordedProgram(recordedProgramId);
+ }
SparseArrayObjectAdapter adapter =
new SparseArrayObjectAdapter(new ActionPresenterSelector());
Resources res = getResources();
@@ -82,6 +115,35 @@ public class CurrentRecordingDetailsFragment extends RecordingDetailsFragment {
res.getString(R.string.dvr_detail_stop_recording),
null,
res.getDrawable(R.drawable.lb_ic_stop)));
+ if (mConcurrentDvrPlaybackFlags.enabled()
+ && mRecordedProgram != null
+ && mRecordedProgram.isPartial()) {
+ 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)));
+ }
+ }
return adapter;
}
@@ -107,6 +169,13 @@ public class CurrentRecordingDetailsFragment extends RecordingDetailsFragment {
}
}
});
+ } else if (action.getId() == ACTION_RESUME_PLAYING) {
+ startPlayback(
+ mRecordedProgram,
+ mDvrWatchedPositionManager.getWatchedPosition(
+ mRecordedProgram.getId()));
+ } else if (action.getId() == ACTION_PLAY_FROM_BEGINNING) {
+ startPlayback(mRecordedProgram, TvInputManager.TIME_SHIFT_INVALID_TIME);
}
}
};
diff --git a/src/com/android/tv/dvr/ui/browse/DetailsContent.java b/src/com/android/tv/dvr/ui/browse/DetailsContent.java
index cba6293b..e179743c 100644
--- a/src/com/android/tv/dvr/ui/browse/DetailsContent.java
+++ b/src/com/android/tv/dvr/ui/browse/DetailsContent.java
@@ -22,6 +22,7 @@ import android.support.annotation.Nullable;
import android.text.TextUtils;
import com.android.tv.R;
import com.android.tv.TvSingletons;
+import com.android.tv.data.Program;
import com.android.tv.data.api.Channel;
import com.android.tv.dvr.data.RecordedProgram;
import com.android.tv.dvr.data.ScheduledRecording;
@@ -29,7 +30,7 @@ import com.android.tv.dvr.data.SeriesRecording;
import com.android.tv.dvr.ui.DvrUiHelper;
/** A class for details content. */
-class DetailsContent {
+public class DetailsContent {
/** Constant for invalid time. */
public static final long INVALID_TIME = -1;
@@ -40,6 +41,7 @@ class DetailsContent {
private String mLogoImageUri;
private String mBackgroundImageUri;
private boolean mUsingChannelLogo;
+ private boolean mShowErrorMessage;
static DetailsContent createFromRecordedProgram(
Context context, RecordedProgram recordedProgram) {
@@ -59,6 +61,23 @@ class DetailsContent {
.build(context);
}
+ public static DetailsContent createFromProgram(Context context, Program program) {
+ return new DetailsContent.Builder()
+ .setChannelId(program.getChannelId())
+ .setProgramTitle(program.getTitle())
+ .setSeasonNumber(program.getSeasonNumber())
+ .setEpisodeNumber(program.getEpisodeNumber())
+ .setStartTimeUtcMillis(program.getStartTimeUtcMillis())
+ .setEndTimeUtcMillis(program.getEndTimeUtcMillis())
+ .setDescription(
+ TextUtils.isEmpty(program.getLongDescription())
+ ? program.getDescription()
+ : program.getLongDescription())
+ .setPosterArtUri(program.getPosterArtUri())
+ .setThumbnailUri(program.getThumbnailUri())
+ .build(context);
+ }
+
static DetailsContent createFromSeriesRecording(
Context context, SeriesRecording seriesRecording) {
return new DetailsContent.Builder()
@@ -79,37 +98,9 @@ class DetailsContent {
TvSingletons.getSingletons(context)
.getChannelDataManager()
.getChannel(scheduledRecording.getChannelId());
- String description =
- !TextUtils.isEmpty(scheduledRecording.getProgramDescription())
- ? scheduledRecording.getProgramDescription()
- : scheduledRecording.getProgramLongDescription();
- if (TextUtils.isEmpty(description)) {
- description = channel != null ? channel.getDescription() : null;
- }
- return new DetailsContent.Builder()
- .setChannelId(scheduledRecording.getChannelId())
- .setProgramTitle(scheduledRecording.getProgramTitle())
- .setSeasonNumber(scheduledRecording.getSeasonNumber())
- .setEpisodeNumber(scheduledRecording.getEpisodeNumber())
- .setStartTimeUtcMillis(scheduledRecording.getStartTimeMs())
- .setEndTimeUtcMillis(scheduledRecording.getEndTimeMs())
- .setDescription(description)
- .setPosterArtUri(scheduledRecording.getProgramPosterArtUri())
- .setThumbnailUri(scheduledRecording.getProgramThumbnailUri())
- .build(context);
- }
-
- static DetailsContent createFromFailedScheduledRecording(
- Context context, ScheduledRecording scheduledRecording, String errMsg) {
- Channel channel =
- TvSingletons.getSingletons(context)
- .getChannelDataManager()
- .getChannel(scheduledRecording.getChannelId());
String description;
- if (scheduledRecording.getState() == ScheduledRecording.STATE_RECORDING_FAILED
- && errMsg != null) {
- description = errMsg
- + " (Error code: " + scheduledRecording.getFailedReason() + ")";
+ if (scheduledRecording.getState() == ScheduledRecording.STATE_RECORDING_FAILED) {
+ description = getErrorMessage(context, scheduledRecording);
} else {
description =
!TextUtils.isEmpty(scheduledRecording.getProgramDescription())
@@ -129,9 +120,39 @@ class DetailsContent {
.setDescription(description)
.setPosterArtUri(scheduledRecording.getProgramPosterArtUri())
.setThumbnailUri(scheduledRecording.getProgramThumbnailUri())
+ .setShowErrorMessage(
+ scheduledRecording.getState() == ScheduledRecording.STATE_RECORDING_FAILED)
.build(context);
}
+ private static String getErrorMessage(Context context, ScheduledRecording recording) {
+ int reason = recording.getFailedReason() == null
+ ? ScheduledRecording.FAILED_REASON_OTHER
+ : recording.getFailedReason();
+ switch (reason) {
+ case ScheduledRecording.FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED:
+ return context.getString(R.string.dvr_recording_failed_not_started);
+ case ScheduledRecording.FAILED_REASON_RESOURCE_BUSY:
+ return context.getString(R.string.dvr_recording_failed_resource_busy);
+ case ScheduledRecording.FAILED_REASON_INPUT_UNAVAILABLE:
+ return context.getString(
+ R.string.dvr_recording_failed_input_unavailable,
+ recording.getInputId());
+ case ScheduledRecording.FAILED_REASON_INPUT_DVR_UNSUPPORTED:
+ return context.getString(R.string.dvr_recording_failed_input_dvr_unsupported);
+ case ScheduledRecording.FAILED_REASON_INSUFFICIENT_SPACE:
+ return context.getString(R.string.dvr_recording_failed_insufficient_space);
+ case ScheduledRecording.FAILED_REASON_OTHER: // fall through
+ case ScheduledRecording.FAILED_REASON_NOT_FINISHED: // fall through
+ case ScheduledRecording.FAILED_REASON_SCHEDULER_STOPPED: // fall through
+ case ScheduledRecording.FAILED_REASON_INVALID_CHANNEL: // fall through
+ case ScheduledRecording.FAILED_REASON_MESSAGE_NOT_SENT: // fall through
+ case ScheduledRecording.FAILED_REASON_CONNECTION_FAILED: // fall through
+ default:
+ return context.getString(R.string.dvr_recording_failed_system_failure, reason);
+ }
+ }
+
private DetailsContent() {}
/** Returns title. */
@@ -169,6 +190,11 @@ class DetailsContent {
return mUsingChannelLogo;
}
+ /** Returns if the error message should be shown. */
+ public boolean shouldShowErrorMessage() {
+ return mShowErrorMessage;
+ }
+
/** Copies other details content. */
public void copyFrom(DetailsContent other) {
if (this == other) {
@@ -181,6 +207,7 @@ class DetailsContent {
mLogoImageUri = other.mLogoImageUri;
mBackgroundImageUri = other.mBackgroundImageUri;
mUsingChannelLogo = other.mUsingChannelLogo;
+ mShowErrorMessage = other.mShowErrorMessage;
}
/** A class for building details content. */
@@ -266,6 +293,11 @@ class DetailsContent {
return this;
}
+ private Builder setShowErrorMessage(boolean showErrorMessage) {
+ mDetailsContent.mShowErrorMessage = showErrorMessage;
+ return this;
+ }
+
private void createStyledTitle(Context context, Channel channel) {
CharSequence title =
DvrUiHelper.getStyledTitleWithEpisodeNumber(
diff --git a/src/com/android/tv/dvr/ui/browse/DetailsContentPresenter.java b/src/com/android/tv/dvr/ui/browse/DetailsContentPresenter.java
index aec8c411..6b5fd1fd 100644
--- a/src/com/android/tv/dvr/ui/browse/DetailsContentPresenter.java
+++ b/src/com/android/tv/dvr/ui/browse/DetailsContentPresenter.java
@@ -45,12 +45,13 @@ import com.android.tv.util.Utils;
* The latter class are re-used to provide a customized version of {@link
* android.support.v17.leanback.widget.DetailsOverviewRow}.
*/
-class DetailsContentPresenter extends Presenter {
+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 LinearLayout mErrorMessage;
final TextView mBody;
final TextView mReadMoreView;
final int mTitleMargin;
@@ -150,6 +151,8 @@ class DetailsContentPresenter extends Presenter {
});
mTitle = (TextView) view.findViewById(R.id.dvr_details_description_title);
mSubtitle = (TextView) view.findViewById(R.id.dvr_details_description_subtitle);
+ mErrorMessage =
+ (LinearLayout) view.findViewById(R.id.dvr_details_description_error_message);
mBody = (TextView) view.findViewById(R.id.dvr_details_description_body);
mDescriptionContainer =
(LinearLayout) view.findViewById(R.id.dvr_details_description_container);
@@ -321,6 +324,9 @@ class DetailsContentPresenter extends Presenter {
if (TextUtils.isEmpty(detailsContent.getDescription())) {
vh.mBody.setVisibility(View.GONE);
} else {
+ if (detailsContent.shouldShowErrorMessage()) {
+ vh.mErrorMessage.setVisibility(View.VISIBLE);
+ }
vh.mBody.setText(detailsContent.getDescription());
vh.mBody.setVisibility(View.VISIBLE);
vh.mBody.setLineSpacing(
diff --git a/src/com/android/tv/dvr/ui/browse/DetailsViewBackgroundHelper.java b/src/com/android/tv/dvr/ui/browse/DetailsViewBackgroundHelper.java
index 849360b8..4e41daee 100644
--- a/src/com/android/tv/dvr/ui/browse/DetailsViewBackgroundHelper.java
+++ b/src/com/android/tv/dvr/ui/browse/DetailsViewBackgroundHelper.java
@@ -24,7 +24,7 @@ import android.os.Handler;
import android.support.v17.leanback.app.BackgroundManager;
/** The Background Helper. */
-class DetailsViewBackgroundHelper {
+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;
diff --git a/src/com/android/tv/dvr/ui/browse/DvrBrowseActivity.java b/src/com/android/tv/dvr/ui/browse/DvrBrowseActivity.java
index 6cc1c7a1..5743ea5c 100644
--- a/src/com/android/tv/dvr/ui/browse/DvrBrowseActivity.java
+++ b/src/com/android/tv/dvr/ui/browse/DvrBrowseActivity.java
@@ -22,9 +22,15 @@ import android.media.tv.TvInputManager;
import android.os.Bundle;
import com.android.tv.R;
import com.android.tv.Starter;
+import com.android.tv.perf.PerformanceMonitorManagerFactory;
/** {@link android.app.Activity} for DVR UI. */
public class DvrBrowseActivity extends Activity {
+
+ {
+ PerformanceMonitorManagerFactory.create().getStartupMeasure().onActivityInit();
+ }
+
private DvrBrowseFragment mFragment;
@Override
diff --git a/src/com/android/tv/dvr/ui/browse/DvrBrowseFragment.java b/src/com/android/tv/dvr/ui/browse/DvrBrowseFragment.java
index 40b3a1f0..17ba1939 100644
--- a/src/com/android/tv/dvr/ui/browse/DvrBrowseFragment.java
+++ b/src/com/android/tv/dvr/ui/browse/DvrBrowseFragment.java
@@ -31,9 +31,7 @@ import android.support.v17.leanback.widget.TitleViewAdapter;
import android.util.Log;
import android.view.View;
import android.view.ViewTreeObserver.OnGlobalFocusChangeListener;
-
import com.android.tv.R;
-import com.android.tv.TvFeatures;
import com.android.tv.TvSingletons;
import com.android.tv.data.GenreItems;
import com.android.tv.dvr.DvrDataManager;
@@ -47,7 +45,7 @@ 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 com.google.common.collect.ImmutableList;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
@@ -66,7 +64,7 @@ public class DvrBrowseFragment extends BrowseFragment
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_RECENT_ITEM_COUNT = 4;
private static final int MAX_SCHEDULED_ITEM_COUNT = 4;
private boolean mShouldShowScheduleRow;
@@ -104,93 +102,84 @@ public class DvrBrowseFragment extends BrowseFragment
};
private final Comparator<Object> RECORDED_PROGRAM_COMPARATOR =
- new Comparator<Object>() {
- @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;
+ (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 0;
+ return -1;
}
+ } else if (rhs instanceof RecordedProgram) {
+ return 1;
+ } else {
+ return 0;
}
};
private static final Comparator<Object> SCHEDULE_COMPARATOR =
- new Comparator<Object>() {
- @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;
+ (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 0;
+ return -1;
}
+ } else if (rhs instanceof ScheduledRecording) {
+ return 1;
+ } else {
+ return 0;
}
};
static final Comparator<Object> RECENT_ROW_COMPARATOR =
- new Comparator<Object>() {
- @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
- .reversed()
- .compare((ScheduledRecording) lhs, (ScheduledRecording) rhs);
- } else if (rhs instanceof RecordedProgram) {
- ScheduledRecording scheduled = (ScheduledRecording) lhs;
- RecordedProgram recorded = (RecordedProgram) rhs;
- int compare =
- Long.compare(
- recorded.getStartTimeUtcMillis(),
- scheduled.getStartTimeMs());
- // recorded program first when the start times are the same
- return compare == 0 ? 1 : compare;
- } else {
- return -1;
- }
- } else if (lhs instanceof RecordedProgram) {
- if (rhs instanceof RecordedProgram) {
- return RecordedProgram.START_TIME_THEN_ID_COMPARATOR
- .reversed()
- .compare((RecordedProgram) lhs, (RecordedProgram) rhs);
- } else if (rhs instanceof ScheduledRecording) {
- RecordedProgram recorded = (RecordedProgram) lhs;
- ScheduledRecording scheduled = (ScheduledRecording) rhs;
- int compare =
- Long.compare(
- scheduled.getStartTimeMs(),
- recorded.getStartTimeUtcMillis());
- // recorded program first when the start times are the same
- return compare == 0 ? -1 : compare;
- } else {
- return -1;
- }
+ (Object lhs, Object rhs) -> {
+ if (lhs instanceof ScheduledRecording) {
+ if (rhs instanceof ScheduledRecording) {
+ return ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR
+ .reversed()
+ .compare((ScheduledRecording) lhs, (ScheduledRecording) rhs);
+ } else if (rhs instanceof RecordedProgram) {
+ ScheduledRecording scheduled = (ScheduledRecording) lhs;
+ RecordedProgram recorded = (RecordedProgram) rhs;
+ int compare =
+ Long.compare(
+ recorded.getStartTimeUtcMillis(),
+ scheduled.getStartTimeMs());
+ // recorded program first when the start times are the same
+ return compare == 0 ? 1 : compare;
+ } else {
+ return -1;
+ }
+ } else if (lhs instanceof RecordedProgram) {
+ if (rhs instanceof RecordedProgram) {
+ return RecordedProgram.START_TIME_THEN_ID_COMPARATOR
+ .reversed()
+ .compare((RecordedProgram) lhs, (RecordedProgram) rhs);
+ } else if (rhs instanceof ScheduledRecording) {
+ RecordedProgram recorded = (RecordedProgram) lhs;
+ ScheduledRecording scheduled = (ScheduledRecording) rhs;
+ int compare =
+ Long.compare(
+ scheduled.getStartTimeMs(),
+ recorded.getStartTimeUtcMillis());
+ // recorded program first when the start times are the same
+ return compare == 0 ? -1 : compare;
} else {
- return !(rhs instanceof RecordedProgram)
- && !(rhs instanceof ScheduledRecording)
- ? 0 : 1;
+ return -1;
}
+ } else {
+ return !(rhs instanceof RecordedProgram) && !(rhs instanceof ScheduledRecording)
+ ? 0
+ : 1;
}
};
@@ -207,13 +196,7 @@ public class DvrBrowseFragment extends BrowseFragment
}
};
- private final Runnable mUpdateRowsRunnable =
- new Runnable() {
- @Override
- public void run() {
- updateRows();
- }
- };
+ private final Runnable mUpdateRowsRunnable = this::updateRows;
@Override
public void onCreate(Bundle savedInstanceState) {
@@ -233,13 +216,10 @@ public class DvrBrowseFragment extends BrowseFragment
SeriesRecording.class, new SeriesRecordingPresenter(context))
.addClassPresenter(
FullScheduleCardHolder.class,
- new FullSchedulesCardPresenter(context));
+ new FullSchedulesCardPresenter(context))
+ .addClassPresenter(
+ DvrHistoryCardHolder.class, new DvrHistoryCardPresenter(context));
- if (TvFeatures.DVR_FAILED_LIST.isEnabled(context)) {
- mPresenterSelector.addClassPresenter(
- DvrHistoryCardHolder.class,
- new DvrHistoryCardPresenter(context));
- }
mGenreLabels = new ArrayList<>(Arrays.asList(GenreItems.getLabels(context)));
mGenreLabels.add(getString(R.string.dvr_main_others));
prepareUiElements();
@@ -310,7 +290,9 @@ public class DvrBrowseFragment extends BrowseFragment
@Override
public void onRecordedProgramsChanged(RecordedProgram... recordedPrograms) {
for (RecordedProgram recordedProgram : recordedPrograms) {
- handleRecordedProgramChanged(recordedProgram);
+ if (recordedProgram.isVisible()) {
+ handleRecordedProgramChanged(recordedProgram);
+ }
}
postUpdateRows();
}
@@ -340,6 +322,9 @@ public class DvrBrowseFragment extends BrowseFragment
public void onScheduledRecordingRemoved(ScheduledRecording... scheduledRecordings) {
for (ScheduledRecording scheduleRecording : scheduledRecordings) {
mScheduleAdapter.remove(scheduleRecording);
+ if (scheduleRecording.getState() == ScheduledRecording.STATE_RECORDING_FAILED) {
+ mRecentAdapter.remove(scheduleRecording);
+ }
}
}
@@ -351,6 +336,9 @@ public class DvrBrowseFragment extends BrowseFragment
} else {
mScheduleAdapter.removeWithId(scheduleRecording);
}
+ if (scheduleRecording.getState() == ScheduledRecording.STATE_RECORDING_FAILED) {
+ mRecentAdapter.change(scheduleRecording);
+ }
}
}
@@ -443,16 +431,17 @@ public class DvrBrowseFragment extends BrowseFragment
mScheduleAdapter.addExtraItem(FullScheduleCardHolder.FULL_SCHEDULE_CARD_HOLDER);
// Recorded Programs.
for (RecordedProgram recordedProgram : mDvrDataManager.getRecordedPrograms()) {
- handleRecordedProgramAdded(recordedProgram, false);
- }
- if (TvFeatures.DVR_FAILED_LIST.isEnabled(getContext())) {
- // only get failed recordings
- for (ScheduledRecording scheduledRecording
- : mDvrDataManager.getFailedScheduledRecordings()) {
- onScheduledRecordingAdded(scheduledRecording);
+ if (recordedProgram.isVisible()) {
+ handleRecordedProgramAdded(recordedProgram, false);
}
- mRecentAdapter.addExtraItem(DvrHistoryCardHolder.DVR_HISTORY_CARD_HOLDER);
}
+ // only get failed recordings
+ for (ScheduledRecording scheduledRecording :
+ mDvrDataManager.getFailedScheduledRecordings()) {
+ onScheduledRecordingAdded(scheduledRecording);
+ }
+ mRecentAdapter.addExtraItem(DvrHistoryCardHolder.DVR_HISTORY_CARD_HOLDER);
+
// Series Recordings. Series recordings should be added after recorded programs, because
// we build series recordings' latest program information while adding recorded
// programs.
@@ -592,9 +581,9 @@ public class DvrBrowseFragment extends BrowseFragment
}
}
- private List<RecordedProgramAdapter> getGenreAdapters(String[] genres) {
+ private List<RecordedProgramAdapter> getGenreAdapters(ImmutableList<String> genres) {
List<RecordedProgramAdapter> result = new ArrayList<>();
- if (genres == null || genres.length == 0) {
+ if (genres == null || genres.isEmpty()) {
result.add(mGenreAdapters[mGenreAdapters.length - 1]);
} else {
for (String genre : genres) {
@@ -642,8 +631,8 @@ public class DvrBrowseFragment extends BrowseFragment
private void updateRows() {
int visibleRowsCount = 1; // Schedule's Row will never be empty
- int recentRowMinSize = TvFeatures.DVR_FAILED_LIST.isEnabled(getContext()) ? 1 : 0;
- if (mRecentAdapter.size() <= recentRowMinSize) {
+ if (mRecentAdapter.size() <= 1) {
+ // remove the row if there is only the DVR history card
mRowsAdapter.remove(mRecentRow);
} else {
if (mRowsAdapter.indexOf(mRecentRow) < 0) {
@@ -673,6 +662,9 @@ public class DvrBrowseFragment extends BrowseFragment
}
}
}
+ if (getSelectedPosition() >= mRowsAdapter.size()) {
+ setSelectedPosition(mRecentAdapter.size() - 1);
+ }
}
private boolean needToShowScheduledRecording(ScheduledRecording recording) {
@@ -713,16 +705,13 @@ public class DvrBrowseFragment extends BrowseFragment
SeriesAdapter() {
super(
mPresenterSelector,
- new Comparator<SeriesRecording>() {
- @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);
+ (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);
});
}
diff --git a/src/com/android/tv/dvr/ui/browse/DvrDetailsActivity.java b/src/com/android/tv/dvr/ui/browse/DvrDetailsActivity.java
deleted file mode 100644
index 0336b319..00000000
--- a/src/com/android/tv/dvr/ui/browse/DvrDetailsActivity.java
+++ /dev/null
@@ -1,144 +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.browse;
-
-import android.app.Activity;
-import android.os.Bundle;
-import android.support.v17.leanback.app.DetailsFragment;
-import android.transition.Transition;
-import android.transition.Transition.TransitionListener;
-import android.view.View;
-import com.android.tv.R;
-import com.android.tv.Starter;
-import com.android.tv.dialog.PinDialogFragment;
-
-/** Activity to show details view in DVR. */
-public class DvrDetailsActivity extends Activity implements PinDialogFragment.OnPinCheckedListener {
- /** 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";
-
- /** Name of error message of a failed recording */
- public static final String EXTRA_FAILED_MESSAGE = "failed_message";
-
- /** 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;
-
- private PinDialogFragment.OnPinCheckedListener mOnPinCheckedListener;
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- Starter.start(this);
- 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);
- String failedMsg = getIntent().getStringExtra(EXTRA_FAILED_MESSAGE);
- 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);
- args.putString(EXTRA_FAILED_MESSAGE, failedMsg);
- 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();
- }
-
- // This is a workaround for the focus on O device
- addTransitionListener();
- }
-
- @Override
- public void onPinChecked(boolean checked, int type, String rating) {
- if (mOnPinCheckedListener != null) {
- mOnPinCheckedListener.onPinChecked(checked, type, rating);
- }
- }
-
- void setOnPinCheckListener(PinDialogFragment.OnPinCheckedListener listener) {
- mOnPinCheckedListener = listener;
- }
-
- private void addTransitionListener() {
- getWindow()
- .getSharedElementEnterTransition()
- .addListener(
- new TransitionListener() {
- @Override
- public void onTransitionStart(Transition transition) {
- // Do nothing
- }
-
- @Override
- public void onTransitionEnd(Transition transition) {
- View actions = findViewById(R.id.details_overview_actions);
- if (actions != null) {
- actions.requestFocus();
- }
- }
-
- @Override
- public void onTransitionCancel(Transition transition) {
- // Do nothing
-
- }
-
- @Override
- public void onTransitionPause(Transition transition) {
- // Do nothing
- }
-
- @Override
- public void onTransitionResume(Transition transition) {
- // Do nothing
- }
- });
- }
-}
diff --git a/src/com/android/tv/dvr/ui/browse/DvrDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/DvrDetailsFragment.java
index 8f4e4dab..f90981f0 100644
--- a/src/com/android/tv/dvr/ui/browse/DvrDetailsFragment.java
+++ b/src/com/android/tv/dvr/ui/browse/DvrDetailsFragment.java
@@ -47,8 +47,10 @@ import com.android.tv.dialog.PinDialogFragment.OnPinCheckedListener;
import com.android.tv.dvr.data.RecordedProgram;
import com.android.tv.dvr.ui.DvrUiHelper;
import com.android.tv.parental.ParentalControlSettings;
+import com.android.tv.ui.DetailsActivity;
import com.android.tv.util.ToastUtils;
import com.android.tv.util.images.ImageLoader;
+import com.google.common.collect.ImmutableList;
import java.io.File;
abstract class DvrDetailsFragment extends DetailsFragment {
@@ -89,7 +91,7 @@ abstract class DvrDetailsFragment extends DetailsFragment {
rowPresenter.setBackgroundColor(
getResources().getColor(R.color.common_tv_background, null));
rowPresenter.setSharedElementEnterTransition(
- getActivity(), DvrDetailsActivity.SHARED_ELEMENT_NAME);
+ getActivity(), DetailsActivity.SHARED_ELEMENT_NAME);
rowPresenter.setOnActionClickedListener(onCreateOnActionClickedListener());
mRowsAdapter = new ArrayObjectAdapter(onCreatePresenterSelector(rowPresenter));
setAdapter(mRowsAdapter);
@@ -221,7 +223,7 @@ abstract class DvrDetailsFragment extends DetailsFragment {
checkPinToPlay(recordedProgram, seekTimeMs);
return;
}
- TvContentRating[] ratings = recordedProgram.getContentRatings();
+ ImmutableList<TvContentRating> ratings = recordedProgram.getContentRatings();
TvContentRating blockRatings = parental.getBlockedRating(ratings);
if (blockRatings != null) {
checkPinToPlay(recordedProgram, seekTimeMs);
@@ -245,15 +247,14 @@ abstract class DvrDetailsFragment extends DetailsFragment {
}
private void checkPinToPlay(RecordedProgram recordedProgram, long seekTimeMs) {
- SoftPreconditions.checkState(getActivity() instanceof DvrDetailsActivity);
- if (getActivity() instanceof DvrDetailsActivity) {
- ((DvrDetailsActivity) getActivity())
+ SoftPreconditions.checkState(getActivity() instanceof DetailsActivity);
+ if (getActivity() instanceof DetailsActivity) {
+ ((DetailsActivity) getActivity())
.setOnPinCheckListener(
new OnPinCheckedListener() {
@Override
public void onPinChecked(boolean checked, int type, String rating) {
- ((DvrDetailsActivity) getActivity())
- .setOnPinCheckListener(null);
+ ((DetailsActivity) getActivity()).setOnPinCheckListener(null);
if (checked
&& type
== PinDialogFragment
diff --git a/src/com/android/tv/dvr/ui/browse/RecordedProgramDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/RecordedProgramDetailsFragment.java
index 47b1a198..bf963547 100644
--- a/src/com/android/tv/dvr/ui/browse/RecordedProgramDetailsFragment.java
+++ b/src/com/android/tv/dvr/ui/browse/RecordedProgramDetailsFragment.java
@@ -24,10 +24,13 @@ import android.support.v17.leanback.widget.OnActionClickedListener;
import android.support.v17.leanback.widget.SparseArrayObjectAdapter;
import com.android.tv.R;
import com.android.tv.TvSingletons;
+import com.android.tv.common.util.PermissionUtils;
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.ui.DvrUiHelper;
+import com.android.tv.ui.DetailsActivity;
/** {@link android.support.v17.leanback.app.DetailsFragment} for recorded program in DVR. */
public class RecordedProgramDetailsFragment extends DvrDetailsFragment
@@ -80,7 +83,7 @@ public class RecordedProgramDetailsFragment extends DvrDetailsFragment
@Override
protected boolean onLoadRecordingDetails(Bundle args) {
- long recordedProgramId = args.getLong(DvrDetailsActivity.RECORDING_ID);
+ long recordedProgramId = args.getLong(DetailsActivity.RECORDING_ID);
mRecordedProgram = mDvrDataManager.getRecordedProgram(recordedProgramId);
return mRecordedProgram != null;
}
@@ -138,15 +141,24 @@ public class RecordedProgramDetailsFragment extends DvrDetailsFragment
mDvrWatchedPositionManager.getWatchedPosition(
mRecordedProgram.getId()));
} else if (action.getId() == ACTION_DELETE_RECORDING) {
- DvrManager dvrManager =
- TvSingletons.getSingletons(getActivity()).getDvrManager();
- dvrManager.removeRecordedProgram(mRecordedProgram);
- getActivity().finish();
+ delete();
}
}
};
}
+ private void delete() {
+ if (!PermissionUtils.hasWriteExternalStorage(getContext())
+ && DvrManager.isFile(mRecordedProgram.getDataUri())
+ && !DvrManager.isFromBundledInput(mRecordedProgram)) {
+ DvrUiHelper.showWriteStoragePermissionRationaleDialog(getActivity());
+ } else {
+ DvrManager dvrManager = TvSingletons.getSingletons(getActivity()).getDvrManager();
+ dvrManager.removeRecordedProgram(mRecordedProgram, true);
+ getActivity().finish();
+ }
+ }
+
@Override
public void onRecordedProgramsAdded(RecordedProgram... recordedPrograms) {}
diff --git a/src/com/android/tv/dvr/ui/browse/RecordingCardView.java b/src/com/android/tv/dvr/ui/browse/RecordingCardView.java
index fe3c52d9..c83ceaf0 100644
--- a/src/com/android/tv/dvr/ui/browse/RecordingCardView.java
+++ b/src/com/android/tv/dvr/ui/browse/RecordingCardView.java
@@ -48,11 +48,10 @@ public class RecordingCardView extends BaseCardView {
private final int mImageWidth;
private final int mImageHeight;
private String mImageUri;
+ private final ImageView mContentIconView;
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;
@@ -94,8 +93,7 @@ public class RecordingCardView extends BaseCardView {
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);
+ mContentIconView = (ImageView) findViewById(R.id.content_icon);
mMajorContentView = (TextView) findViewById(R.id.content_major);
mMinorContentView = (TextView) findViewById(R.id.content_minor);
mTitleArea = (FrameLayout) findViewById(R.id.title_area);
@@ -184,6 +182,7 @@ public class RecordingCardView extends BaseCardView {
}
void setContent(CharSequence majorContent, CharSequence minorContent) {
+ mContentIconView.setVisibility(View.GONE);
if (!TextUtils.isEmpty(majorContent)) {
mMajorContentView.setText(majorContent);
mMajorContentView.setVisibility(View.VISIBLE);
@@ -198,6 +197,24 @@ public class RecordingCardView extends BaseCardView {
}
}
+ void setRecordingFailedContent(Context context) {
+ mContentIconView.setVisibility(View.VISIBLE);
+ mContentIconView.setImageResource(R.drawable.ic_error_outline_pink_24dp);
+ mMajorContentView.setText(context.getString(R.string.dvr_recording_failed_no_period));
+ mMajorContentView.setVisibility(View.VISIBLE);
+ mMajorContentView.setTextColor(
+ getResources().getColor(R.color.dvr_recording_failed_text_color, null));
+ }
+
+ void setRecordingConflictContent(Context context) {
+ mContentIconView.setVisibility(View.VISIBLE);
+ mContentIconView.setImageResource(R.drawable.ic_warning_yellow_24dp);
+ mMajorContentView.setText(context.getString(R.string.dvr_recording_conflict));
+ mMajorContentView.setVisibility(View.VISIBLE);
+ mMajorContentView.setTextColor(
+ getResources().getColor(R.color.dvr_recording_conflict_text_color, null));
+ }
+
/** Sets progress bar. If progress is {@code null}, hides progress bar. */
void setProgressBar(Integer progress) {
if (progress == null) {
@@ -245,19 +262,6 @@ public class RecordingCardView extends BaseCardView {
}
/**
- * Sets the affiliated icon of the card view, which will be displayed at the lower-right corner
- * of the poster.
- */
- public void setAffiliatedIcon(int imageResId) {
- if (imageResId > 0) {
- mAffiliatedIconContainer.setVisibility(View.VISIBLE);
- mAffiliatedIcon.setImageResource(imageResId);
- } else {
- mAffiliatedIconContainer.setVisibility(View.INVISIBLE);
- }
- }
-
- /**
* Sets the background image URI of the card view, which will be displayed as background when
* the view is clicked and shows its details fragment.
*/
diff --git a/src/com/android/tv/dvr/ui/browse/RecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/RecordingDetailsFragment.java
index aa2ccf75..243681c6 100644
--- a/src/com/android/tv/dvr/ui/browse/RecordingDetailsFragment.java
+++ b/src/com/android/tv/dvr/ui/browse/RecordingDetailsFragment.java
@@ -20,6 +20,7 @@ import android.os.Bundle;
import android.support.v17.leanback.app.DetailsFragment;
import com.android.tv.TvSingletons;
import com.android.tv.dvr.data.ScheduledRecording;
+import com.android.tv.ui.DetailsActivity;
/** {@link DetailsFragment} for recordings in DVR. */
abstract class RecordingDetailsFragment extends DvrDetailsFragment {
@@ -33,7 +34,7 @@ abstract class RecordingDetailsFragment extends DvrDetailsFragment {
@Override
protected boolean onLoadRecordingDetails(Bundle args) {
- long scheduledRecordingId = args.getLong(DvrDetailsActivity.RECORDING_ID);
+ long scheduledRecordingId = args.getLong(DetailsActivity.RECORDING_ID);
mRecording =
TvSingletons.getSingletons(getContext())
.getDvrDataManager()
diff --git a/src/com/android/tv/dvr/ui/browse/ScheduledRecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/ScheduledRecordingDetailsFragment.java
index 302b8318..f08bb12b 100644
--- a/src/com/android/tv/dvr/ui/browse/ScheduledRecordingDetailsFragment.java
+++ b/src/com/android/tv/dvr/ui/browse/ScheduledRecordingDetailsFragment.java
@@ -21,10 +21,12 @@ 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 com.android.tv.R;
import com.android.tv.TvSingletons;
import com.android.tv.dvr.DvrManager;
import com.android.tv.dvr.ui.DvrUiHelper;
+import com.android.tv.ui.DetailsActivity;
/** {@link RecordingDetailsFragment} for scheduled recording in DVR. */
public class ScheduledRecordingDetailsFragment extends RecordingDetailsFragment {
@@ -34,14 +36,12 @@ public class ScheduledRecordingDetailsFragment extends RecordingDetailsFragment
private DvrManager mDvrManager;
private Action mScheduleAction;
private boolean mHideViewSchedule;
- private String mFailedMessage;
@Override
public void onCreate(Bundle savedInstance) {
Bundle args = getArguments();
mDvrManager = TvSingletons.getSingletons(getContext()).getDvrManager();
- mHideViewSchedule = args.getBoolean(DvrDetailsActivity.HIDE_VIEW_SCHEDULE);
- mFailedMessage = args.getString(DvrDetailsActivity.EXTRA_FAILED_MESSAGE);
+ mHideViewSchedule = args.getBoolean(DetailsActivity.HIDE_VIEW_SCHEDULE);
super.onCreate(savedInstance);
}
@@ -54,17 +54,6 @@ public class ScheduledRecordingDetailsFragment extends RecordingDetailsFragment
}
@Override
- protected void onCreateInternal() {
- if (mFailedMessage == null) {
- super.onCreateInternal();
- return;
- }
- setDetailsOverviewRow(
- DetailsContent.createFromFailedScheduledRecording(
- getContext(), getScheduledRecording(), mFailedMessage));
- }
-
- @Override
protected SparseArrayObjectAdapter onCreateActionsAdapter() {
SparseArrayObjectAdapter adapter =
new SparseArrayObjectAdapter(new ActionPresenterSelector());
diff --git a/src/com/android/tv/dvr/ui/browse/ScheduledRecordingPresenter.java b/src/com/android/tv/dvr/ui/browse/ScheduledRecordingPresenter.java
index 8e028689..3d279354 100644
--- a/src/com/android/tv/dvr/ui/browse/ScheduledRecordingPresenter.java
+++ b/src/com/android/tv/dvr/ui/browse/ScheduledRecordingPresenter.java
@@ -119,21 +119,17 @@ class ScheduledRecordingPresenter extends DvrItemPresenter<ScheduledRecording> {
DetailsContent details = DetailsContent.createFromScheduledRecording(mContext, recording);
cardView.setTitle(details.getTitle());
cardView.setImageUri(details.getLogoImageUri(), details.isUsingChannelLogo());
- if (mDvrManager.isConflicting(recording)) {
- cardView.setAffiliatedIcon(R.drawable.ic_warning_white_32dp);
- } else if (recording.getState() == ScheduledRecording.STATE_RECORDING_FAILED) {
- cardView.setAffiliatedIcon(R.drawable.ic_error_white_48dp);
+ if (recording.getState() == ScheduledRecording.STATE_RECORDING_FAILED) {
+ cardView.setRecordingFailedContent(mContext);
+ } else if (mDvrManager.isConflicting(recording)) {
+ cardView.setRecordingConflictContent(mContext);
} else {
- cardView.setAffiliatedIcon(0);
+ cardView.setContent(generateMajorContent(recording), null);
}
- cardView.setContent(generateMajorContent(recording), null);
cardView.setDetailBackgroundImageUri(details.getBackgroundImageUri());
}
private String generateMajorContent(ScheduledRecording recording) {
- if (recording.getState() == ScheduledRecording.STATE_RECORDING_FAILED) {
- return mContext.getString(R.string.dvr_recording_failed);
- }
int dateDifference =
Utils.computeDateDifference(System.currentTimeMillis(), recording.getStartTimeMs());
if (dateDifference <= 0) {
diff --git a/src/com/android/tv/dvr/ui/browse/SeriesRecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/SeriesRecordingDetailsFragment.java
index 2cd191a7..9104ef10 100644
--- a/src/com/android/tv/dvr/ui/browse/SeriesRecordingDetailsFragment.java
+++ b/src/com/android/tv/dvr/ui/browse/SeriesRecordingDetailsFragment.java
@@ -20,6 +20,7 @@ import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.media.tv.TvInputManager;
import android.os.Bundle;
+import android.support.annotation.Nullable;
import android.support.v17.leanback.app.DetailsFragment;
import android.support.v17.leanback.widget.Action;
import android.support.v17.leanback.widget.ArrayObjectAdapter;
@@ -41,6 +42,7 @@ 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 com.android.tv.ui.DetailsActivity;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
@@ -135,7 +137,7 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment
@Override
protected boolean onLoadRecordingDetails(Bundle args) {
- long recordId = args.getLong(DvrDetailsActivity.RECORDING_ID);
+ long recordId = args.getLong(DetailsActivity.RECORDING_ID);
mSeries =
TvSingletons.getSingletons(getActivity())
.getDvrDataManager()
@@ -215,6 +217,7 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment
}
/** The programs are sorted by season number and episode number. */
+ @Nullable
private RecordedProgram getRecommendProgram(List<RecordedProgram> programs) {
for (int i = programs.size() - 1; i >= 0; i--) {
RecordedProgram program = programs.get(i);
@@ -289,7 +292,8 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment
}
}
}
- if (recordedProgram.getId() == mRecommendRecordedProgram.getId()) {
+ if (mRecommendRecordedProgram != null
+ && recordedProgram.getId() == mRecommendRecordedProgram.getId()) {
updateWatchAction();
}
}
@@ -339,14 +343,7 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment
new ListRow(
header,
new SeasonRowAdapter(
- selector,
- new Comparator<RecordedProgram>() {
- @Override
- public int compare(RecordedProgram lhs, RecordedProgram rhs) {
- return BaseProgram.EPISODE_COMPARATOR.compare(lhs, rhs);
- }
- },
- seasonNumber));
+ selector, BaseProgram.EPISODE_COMPARATOR::compare, seasonNumber));
getRowsAdapter().add(position, row);
return row;
}
diff --git a/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java b/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java
index 38d3d582..11680a0d 100644
--- a/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java
+++ b/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java
@@ -37,7 +37,6 @@ import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;
import com.android.tv.R;
-import com.android.tv.TvFeatures;
import com.android.tv.TvSingletons;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.data.api.Channel;
@@ -90,18 +89,19 @@ class ScheduleRowPresenter extends RowPresenter {
private ScheduleRowPresenter mPresenter;
@ScheduleRowAction private int[] mActions;
private boolean mLtr;
- private LinearLayout mInfoContainer;
+ private final LinearLayout mInfoContainer;
// The first action is on the right of the second action.
- private RelativeLayout mSecondActionContainer;
- private RelativeLayout mFirstActionContainer;
- private View mSelectorView;
- private TextView mTimeView;
- private TextView mProgramTitleView;
- private TextView mInfoSeparatorView;
- private TextView mChannelNameView;
- private TextView mConflictInfoView;
- private ImageView mSecondActionView;
- private ImageView mFirstActionView;
+ private final RelativeLayout mSecondActionContainer;
+ private final RelativeLayout mFirstActionContainer;
+ private final View mSelectorView;
+ private final TextView mTimeView;
+ private final TextView mProgramTitleView;
+ private final TextView mInfoSeparatorView;
+ private final TextView mChannelNameView;
+ private final ImageView mExtraInfoIcon;
+ private final TextView mExtraInfoView;
+ private final ImageView mSecondActionView;
+ private final ImageView mFirstActionView;
private Runnable mPendingAnimationRunnable;
@@ -117,14 +117,11 @@ class ScheduleRowPresenter extends RowPresenter {
@Override
public void onFocusChange(View view, boolean focused) {
view.post(
- new Runnable() {
- @Override
- public void run() {
- if (view.isFocused()) {
- mPresenter.mLastFocusedViewId = view.getId();
- }
- updateSelector();
+ () -> {
+ if (view.isFocused()) {
+ mPresenter.mLastFocusedViewId = view.getId();
}
+ updateSelector();
});
}
};
@@ -146,7 +143,8 @@ class ScheduleRowPresenter extends RowPresenter {
mProgramTitleView = (TextView) view.findViewById(R.id.program_title);
mInfoSeparatorView = (TextView) view.findViewById(R.id.info_separator);
mChannelNameView = (TextView) view.findViewById(R.id.channel_name);
- mConflictInfoView = (TextView) view.findViewById(R.id.conflict_info);
+ mExtraInfoIcon = (ImageView) view.findViewById(R.id.extra_info_icon);
+ mExtraInfoView = (TextView) view.findViewById(R.id.extra_info);
Resources res = view.getResources();
mSelectorTranslationDelta =
res.getDimensionPixelSize(R.dimen.dvr_schedules_item_section_margin)
@@ -311,7 +309,7 @@ class ScheduleRowPresenter extends RowPresenter {
mInfoContainer
.getResources()
.getColor(R.color.dvr_schedules_item_info_grey, null));
- mConflictInfoView.setTextColor(
+ mExtraInfoView.setTextColor(
mInfoContainer
.getResources()
.getColor(R.color.dvr_schedules_item_info_grey, null));
@@ -327,7 +325,7 @@ class ScheduleRowPresenter extends RowPresenter {
mInfoContainer.getResources().getColor(R.color.dvr_schedules_item_info, null));
mChannelNameView.setTextColor(
mInfoContainer.getResources().getColor(R.color.dvr_schedules_item_info, null));
- mConflictInfoView.setTextColor(
+ mExtraInfoView.setTextColor(
mInfoContainer.getResources().getColor(R.color.dvr_schedules_item_info, null));
}
}
@@ -426,39 +424,76 @@ class ScheduleRowPresenter extends RowPresenter {
}
}
ScheduledRecording schedule = row.getSchedule();
- if (mDvrManager.isConflicting(schedule)
- || (TvFeatures.DVR_FAILED_LIST.isEnabled(getContext())
- && schedule != null
- && schedule.getState() == ScheduledRecording.STATE_RECORDING_FAILED)) {
- String conflictInfo;
- if (TvFeatures.DVR_FAILED_LIST.isEnabled(getContext())
- && schedule != null
- && schedule.getState() == ScheduledRecording.STATE_RECORDING_FAILED) {
- // TODO(b/72638385): show real error messages
- // TODO(b/72638385): use a better name for ConflictInfoXXX
- conflictInfo = "Failed";
- if (schedule.getFailedReason() != null) {
- conflictInfo += " (Error code: " + schedule.getFailedReason() + ")";
- }
+ viewHolder.mExtraInfoIcon.setVisibility(View.GONE);
+ if (mDvrManager.isConflicting(schedule) || isFailedRecording(schedule)) {
+ String extraInfo;
+ if (isFailedRecording(schedule)) {
+ extraInfo =
+ mContext.getString(R.string.dvr_recording_failed_short)
+ + " "
+ + getErrorMessage(schedule);
+ viewHolder.mExtraInfoIcon.setVisibility(View.VISIBLE);
} else if (mDvrScheduleManager.isPartiallyConflicting(row.getSchedule())) {
- conflictInfo = mTunerConflictWillBePartiallyRecordedInfo;
+ extraInfo = mTunerConflictWillBePartiallyRecordedInfo;
} else {
- conflictInfo = mTunerConflictWillNotBeRecordedInfo;
+ extraInfo = mTunerConflictWillNotBeRecordedInfo;
}
- viewHolder.mConflictInfoView.setText(conflictInfo);
- viewHolder.mConflictInfoView.setVisibility(View.VISIBLE);
+ viewHolder.mExtraInfoView.setText(extraInfo);
+ viewHolder.mExtraInfoView.setVisibility(View.VISIBLE);
} else {
- viewHolder.mConflictInfoView.setVisibility(View.GONE);
+ viewHolder.mExtraInfoView.setVisibility(View.GONE);
}
if (shouldBeGrayedOut(row)) {
viewHolder.greyOutInfo();
} else {
viewHolder.whiteBackInfo();
}
+ if (isFailedRecording(schedule)) {
+ viewHolder.mExtraInfoView.setTextColor(
+ viewHolder
+ .mInfoContainer
+ .getResources()
+ .getColor(R.color.dvr_recording_failed_text_color, null));
+ }
viewHolder.mInfoContainer.setFocusable(isInfoClickable(row));
updateActionContainer(viewHolder, viewHolder.isSelected());
}
+ private boolean isFailedRecording(ScheduledRecording scheduledRecording) {
+ return scheduledRecording != null
+ && scheduledRecording.getState() == ScheduledRecording.STATE_RECORDING_FAILED;
+ }
+
+ private String getErrorMessage(ScheduledRecording recording) {
+ int reason =
+ recording.getFailedReason() == null
+ ? ScheduledRecording.FAILED_REASON_OTHER
+ : recording.getFailedReason();
+ switch (reason) {
+ case ScheduledRecording.FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED:
+ return mContext.getString(R.string.dvr_recording_failed_not_started_short);
+ case ScheduledRecording.FAILED_REASON_RESOURCE_BUSY:
+ return mContext.getString(R.string.dvr_recording_failed_resource_busy_short);
+ case ScheduledRecording.FAILED_REASON_INPUT_UNAVAILABLE:
+ return mContext.getString(
+ R.string.dvr_recording_failed_input_unavailable_short,
+ recording.getInputId());
+ case ScheduledRecording.FAILED_REASON_INPUT_DVR_UNSUPPORTED:
+ return mContext.getString(
+ R.string.dvr_recording_failed_input_dvr_unsupported_short);
+ case ScheduledRecording.FAILED_REASON_INSUFFICIENT_SPACE:
+ return mContext.getString(R.string.dvr_recording_failed_insufficient_space_short);
+ case ScheduledRecording.FAILED_REASON_OTHER: // fall through
+ case ScheduledRecording.FAILED_REASON_NOT_FINISHED: // fall through
+ case ScheduledRecording.FAILED_REASON_SCHEDULER_STOPPED: // fall through
+ case ScheduledRecording.FAILED_REASON_INVALID_CHANNEL: // fall through
+ case ScheduledRecording.FAILED_REASON_MESSAGE_NOT_SENT: // fall through
+ case ScheduledRecording.FAILED_REASON_CONNECTION_FAILED: // fall through
+ default:
+ return mContext.getString(R.string.dvr_recording_failed_system_failure, reason);
+ }
+ }
+
private int getImageForAction(@ScheduleRowAction int action) {
switch (action) {
case ACTION_START_RECORDING:
@@ -512,7 +547,8 @@ class ScheduleRowPresenter extends RowPresenter {
return schedule != null
&& (schedule.isNotStarted()
|| schedule.isInProgress()
- || schedule.isFinished());
+ || schedule.isFinished()
+ || schedule.isFailed());
}
/** Called when the button in a row is clicked. */
@@ -702,23 +738,17 @@ class ScheduleRowPresenter extends RowPresenter {
prepareShowActionView(viewHolder.mSecondActionContainer);
prepareShowActionView(viewHolder.mFirstActionContainer);
viewHolder.mPendingAnimationRunnable =
- new Runnable() {
- @Override
- public void run() {
- showActionView(viewHolder.mSecondActionContainer);
- showActionView(viewHolder.mFirstActionContainer);
- }
+ () -> {
+ showActionView(viewHolder.mSecondActionContainer);
+ showActionView(viewHolder.mFirstActionContainer);
};
break;
case 1:
prepareShowActionView(viewHolder.mFirstActionContainer);
viewHolder.mPendingAnimationRunnable =
- new Runnable() {
- @Override
- public void run() {
- hideActionView(viewHolder.mSecondActionContainer, View.GONE);
- showActionView(viewHolder.mFirstActionContainer);
- }
+ () -> {
+ hideActionView(viewHolder.mSecondActionContainer, View.GONE);
+ showActionView(viewHolder.mFirstActionContainer);
};
if (mLastFocusedViewId == R.id.action_second_container) {
mLastFocusedViewId = R.id.info_container;
@@ -727,12 +757,9 @@ class ScheduleRowPresenter extends RowPresenter {
case 0:
default:
viewHolder.mPendingAnimationRunnable =
- new Runnable() {
- @Override
- public void run() {
- hideActionView(viewHolder.mSecondActionContainer, View.GONE);
- hideActionView(viewHolder.mFirstActionContainer, View.GONE);
- }
+ () -> {
+ hideActionView(viewHolder.mSecondActionContainer, View.GONE);
+ hideActionView(viewHolder.mFirstActionContainer, View.GONE);
};
mLastFocusedViewId = R.id.info_container;
SoftPreconditions.checkState(
diff --git a/src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java b/src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java
index eb01aba2..28a44bf3 100644
--- a/src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java
+++ b/src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java
@@ -211,13 +211,7 @@ abstract class SchedulesHeaderRowPresenter extends RowPresenter {
new View.OnFocusChangeListener() {
@Override
public void onFocusChange(View view, boolean focused) {
- view.post(
- new Runnable() {
- @Override
- public void run() {
- updateSelector(view);
- }
- });
+ view.post(() -> updateSelector(view));
}
};
mSeriesSettingsButton.setOnFocusChangeListener(onFocusChangeListener);
diff --git a/src/com/android/tv/dvr/ui/playback/DvrPlaybackActivity.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackActivity.java
index b8b19adc..f24ad2c0 100644
--- a/src/com/android/tv/dvr/ui/playback/DvrPlaybackActivity.java
+++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackActivity.java
@@ -74,8 +74,10 @@ public class DvrPlaybackActivity extends Activity implements OnPinCheckedListene
private Intent createProgramIntent(Intent intent) {
if (Intent.ACTION_VIEW.equals(intent.getAction())) {
Uri uri = intent.getData();
- long recordedProgramId = ContentUris.parseId(uri);
- intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_ID, recordedProgramId);
+ if (uri != null) {
+ long recordedProgramId = ContentUris.parseId(uri);
+ intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_ID, recordedProgramId);
+ }
}
return intent;
}
diff --git a/src/com/android/tv/dvr/ui/playback/DvrPlaybackControlHelper.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackControlHelper.java
index 59c90d11..791d26bb 100644
--- a/src/com/android/tv/dvr/ui/playback/DvrPlaybackControlHelper.java
+++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackControlHelper.java
@@ -39,6 +39,7 @@ import android.text.TextUtils;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
+import android.view.ViewGroup;
import com.android.tv.R;
import com.android.tv.util.TimeShiftUtils;
import java.util.ArrayList;
@@ -53,10 +54,13 @@ class DvrPlaybackControlHelper extends PlaybackControlGlue {
private static final boolean DEBUG = false;
private static final int AUDIO_ACTION_ID = 1001;
+ private static final long INVALID_TIME = -1;
private int mPlaybackState = PlaybackState.STATE_NONE;
private int mPlaybackSpeedLevel;
private int mPlaybackSpeedId;
+ private long mProgramStartTimeMs = INVALID_TIME;
+ private boolean mEnableBuffering = false;
private boolean mReadyToControl;
private final DvrPlaybackOverlayFragment mFragment;
@@ -67,6 +71,8 @@ class DvrPlaybackControlHelper extends PlaybackControlGlue {
private final MultiAction mClosedCaptioningAction;
private final MultiAction mMultiAudioAction;
private ArrayObjectAdapter mSecondaryActionsAdapter;
+ private PlaybackControlsRow mPlaybackControlsRow;
+ @Nullable private View mPlayPauseButton;
DvrPlaybackControlHelper(Activity activity, DvrPlaybackOverlayFragment overlayFragment) {
super(activity, new int[TimeShiftUtils.MAX_SPEED_LEVEL + 1]);
@@ -79,13 +85,18 @@ class DvrPlaybackControlHelper extends PlaybackControlGlue {
.getDimensionPixelOffset(R.dimen.dvr_playback_controls_extra_padding_top);
mClosedCaptioningAction = new ClosedCaptioningAction(activity);
mMultiAudioAction = new MultiAudioAction(activity);
+ mProgramStartTimeMs = overlayFragment.getProgramStartTimeMs();
+ if (mProgramStartTimeMs != INVALID_TIME) {
+ mEnableBuffering = true;
+ }
createControlsRowPresenter();
}
void createControlsRow() {
- PlaybackControlsRow controlsRow = new PlaybackControlsRow(this);
- setControlsRow(controlsRow);
- mSecondaryActionsAdapter = (ArrayObjectAdapter) controlsRow.getSecondaryActionsAdapter();
+ mPlaybackControlsRow = new PlaybackControlsRow(this);
+ setControlsRow(mPlaybackControlsRow);
+ mSecondaryActionsAdapter =
+ (ArrayObjectAdapter) mPlaybackControlsRow.getSecondaryActionsAdapter();
}
private void createControlsRowPresenter() {
@@ -118,6 +129,8 @@ class DvrPlaybackControlHelper extends PlaybackControlGlue {
protected void onBindRowViewHolder(RowPresenter.ViewHolder vh, Object item) {
super.onBindRowViewHolder(vh, item);
vh.setOnKeyListener(DvrPlaybackControlHelper.this);
+ ViewGroup controlBar = (ViewGroup) vh.view.findViewById(R.id.control_bar);
+ mPlayPauseButton = controlBar.getChildAt(1);
}
@Override
@@ -265,6 +278,13 @@ class DvrPlaybackControlHelper extends PlaybackControlGlue {
getHost().notifyPlaybackRowChanged();
}
+ /** Update the focus to play pause button. */
+ public void onPlaybackResume() {
+ if (mPlayPauseButton != null) {
+ mPlayPauseButton.requestFocus();
+ }
+ }
+
@Nullable
Boolean hasSecondaryRow() {
if (mSecondaryActionsAdapter == null) {
@@ -292,6 +312,15 @@ class DvrPlaybackControlHelper extends PlaybackControlGlue {
mTransportControls.pause();
}
+ @Override
+ public void updateProgress() {
+ if (mEnableBuffering) {
+ super.updateProgress();
+ long bufferedTimeMs = System.currentTimeMillis() - mProgramStartTimeMs;
+ mPlaybackControlsRow.setBufferedPosition(bufferedTimeMs);
+ }
+ }
+
/** Notifies closed caption being enabled/disabled to update related UI. */
void onSubtitleTrackStateChanged(boolean enabled) {
mClosedCaptioningAction.setIndex(
diff --git a/src/com/android/tv/dvr/ui/playback/DvrPlaybackMediaSessionHelper.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackMediaSessionHelper.java
index bef036eb..81abb8e4 100644
--- a/src/com/android/tv/dvr/ui/playback/DvrPlaybackMediaSessionHelper.java
+++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackMediaSessionHelper.java
@@ -39,9 +39,6 @@ import com.android.tv.util.Utils;
import com.android.tv.util.images.ImageLoader;
class DvrPlaybackMediaSessionHelper {
- private static final String TAG = "DvrPlaybackMediaSessionHelper";
- private static final boolean DEBUG = false;
-
private int mNowPlayingCardWidth;
private int mNowPlayingCardHeight;
private int mSpeedLevel;
@@ -73,6 +70,9 @@ class DvrPlaybackMediaSessionHelper {
@Override
public void onPlaybackPositionChanged(long positionMs) {
updateMediaSessionPlaybackState();
+ if (getProgram().isPartial()) {
+ overlayFragment.updateProgress();
+ }
if (mDvrPlayer.isPlaybackPrepared()) {
mDvrWatchedPositionManager.setWatchedPosition(
mDvrPlayer.getProgram().getId(), positionMs);
@@ -94,6 +94,11 @@ class DvrPlaybackMediaSessionHelper {
mActivity.startActivity(intent);
}
}
+
+ @Override
+ public void onPlaybackResume() {
+ overlayFragment.onPlaybackResume();
+ }
});
initializeMediaSession(mediaSessionTag);
}
diff --git a/src/com/android/tv/dvr/ui/playback/DvrPlaybackOverlayFragment.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackOverlayFragment.java
index d3374cfa..1059e852 100644
--- a/src/com/android/tv/dvr/ui/playback/DvrPlaybackOverlayFragment.java
+++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackOverlayFragment.java
@@ -25,7 +25,6 @@ import android.media.session.PlaybackState;
import android.media.tv.TvContentRating;
import android.media.tv.TvInputManager;
import android.media.tv.TvTrackInfo;
-import android.media.tv.TvView;
import android.os.Bundle;
import android.support.v17.leanback.app.PlaybackFragment;
import android.support.v17.leanback.app.PlaybackFragmentGlueHost;
@@ -52,7 +51,7 @@ import com.android.tv.dvr.data.SeriesRecording;
import com.android.tv.dvr.ui.SortedArrayAdapter;
import com.android.tv.dvr.ui.browse.DvrListRowPresenter;
import com.android.tv.dvr.ui.browse.RecordingCardView;
-import com.android.tv.parental.ContentRatingsManager;
+import com.android.tv.ui.AppLayerTvView;
import com.android.tv.util.TvSettings;
import com.android.tv.util.TvTrackInfoUtils;
import com.android.tv.util.Utils;
@@ -66,6 +65,7 @@ public class DvrPlaybackOverlayFragment extends PlaybackFragment {
private static final String MEDIA_SESSION_TAG = "com.android.tv.dvr.mediasession";
private static final float DISPLAY_ASPECT_RATIO_EPSILON = 0.01f;
+ private static final long INVALID_TIME = -1;
// mProgram is only used to store program from intent. Don't use it elsewhere.
private RecordedProgram mProgram;
@@ -76,8 +76,7 @@ public class DvrPlaybackOverlayFragment extends PlaybackFragment {
private SortedArrayAdapter<BaseProgram> mRelatedRecordingsRowAdapter;
private DvrPlaybackCardPresenter mRelatedRecordingCardPresenter;
private DvrDataManager mDvrDataManager;
- private ContentRatingsManager mContentRatingsManager;
- private TvView mTvView;
+ private AppLayerTvView mTvView;
private View mBlockScreenView;
private ListRow mRelatedRecordingsRow;
private int mVerticalPaddingBase;
@@ -117,10 +116,6 @@ public class DvrPlaybackOverlayFragment extends PlaybackFragment {
.getDimensionPixelOffset(
R.dimen.dvr_playback_overlay_padding_top_no_secondary_row);
mDvrDataManager = TvSingletons.getSingletons(getActivity()).getDvrDataManager();
- mContentRatingsManager =
- TvSingletons.getSingletons(getContext())
- .getTvInputManagerHelper()
- .getContentRatingsManager();
if (!mDvrDataManager.isRecordedProgramLoadFinished()) {
mDvrDataManager.addRecordedProgramLoadFinishedListener(
new DvrDataManager.OnRecordedProgramLoadFinishedListener() {
@@ -157,9 +152,9 @@ public class DvrPlaybackOverlayFragment extends PlaybackFragment {
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
- mTvView = (TvView) getActivity().findViewById(R.id.dvr_tv_view);
+ mTvView = getActivity().findViewById(R.id.dvr_tv_view);
mBlockScreenView = getActivity().findViewById(R.id.block_screen);
- mDvrPlayer = new DvrPlayer(mTvView);
+ mDvrPlayer = new DvrPlayer(mTvView, getActivity());
mMediaSessionHelper =
new DvrPlaybackMediaSessionHelper(
getActivity(), MEDIA_SESSION_TAG, mDvrPlayer, this);
@@ -279,6 +274,7 @@ public class DvrPlaybackOverlayFragment extends PlaybackFragment {
mPlaybackControlHelper.unregisterCallback();
mMediaSessionHelper.release();
mRelatedRecordingCardPresenter.unbindAllViewHolders();
+ mDvrPlayer.release();
super.onDestroy();
}
@@ -503,6 +499,20 @@ public class DvrPlaybackOverlayFragment extends PlaybackFragment {
}
}
+ public void onPlaybackResume() {
+ mPlaybackControlHelper.onPlaybackResume();
+ }
+
+ public long getProgramStartTimeMs() {
+ return (mProgram != null && mProgram.isPartial())
+ ? mProgram.getStartTimeUtcMillis()
+ : INVALID_TIME;
+ }
+
+ public void updateProgress() {
+ mPlaybackControlHelper.updateProgress();
+ }
+
private class RelatedRecordingsAdapter extends SortedArrayAdapter<BaseProgram> {
RelatedRecordingsAdapter(DvrPlaybackCardPresenter presenter) {
super(new SinglePresenterSelector(presenter), BaseProgram.EPISODE_COMPARATOR);
diff --git a/src/com/android/tv/dvr/ui/playback/DvrPlayer.java b/src/com/android/tv/dvr/ui/playback/DvrPlayer.java
index 85bb31b2..d14646b8 100644
--- a/src/com/android/tv/dvr/ui/playback/DvrPlayer.java
+++ b/src/com/android/tv/dvr/ui/playback/DvrPlayer.java
@@ -16,6 +16,7 @@
package com.android.tv.dvr.ui.playback;
+import android.content.Context;
import android.media.PlaybackParams;
import android.media.session.PlaybackState;
import android.media.tv.TvContentRating;
@@ -24,12 +25,16 @@ import android.media.tv.TvTrackInfo;
import android.media.tv.TvView;
import android.text.TextUtils;
import android.util.Log;
+import com.android.tv.common.compat.TvViewCompat.TvInputCallbackCompat;
+import com.android.tv.dvr.DvrTvView;
import com.android.tv.dvr.data.RecordedProgram;
+import com.android.tv.ui.AppLayerTvView;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
-class DvrPlayer {
+/** Player for recorded programs. */
+public class DvrPlayer {
private static final String TAG = "DvrPlayer";
private static final boolean DEBUG = false;
@@ -40,10 +45,11 @@ class DvrPlayer {
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 static final long FORWARD_POSITION_MARGIN_MS = TimeUnit.SECONDS.toMillis(5);
private RecordedProgram mProgram;
private long mInitialSeekPositionMs;
- private final TvView mTvView;
+ private final DvrTvView mTvView;
private DvrPlayerCallback mCallback;
private OnAspectRatioChangedListener mOnAspectRatioChangedListener;
private OnContentBlockedListener mOnContentBlockedListener;
@@ -63,6 +69,7 @@ class DvrPlayer {
private long mStartPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME;
private boolean mTimeShiftPlayAvailable;
+ /** Callback of DVR player. */
public static class DvrPlayerCallback {
/**
* Called when the playback position is changed. The normal updating frequency is around 1
@@ -74,8 +81,11 @@ class DvrPlayer {
public void onPlaybackStateChanged(int playbackState, int playbackSpeed) {}
/** Called when the playback toward the end. */
public void onPlaybackEnded() {}
+ /** Called when the playback is resumed to live position. */
+ public void onPlaybackResume() {}
}
+ /** Listener for aspect ratio changed events. */
public interface OnAspectRatioChangedListener {
/**
* Called when the Video's aspect ratio is changed.
@@ -86,27 +96,32 @@ class DvrPlayer {
void onAspectRatioChanged(float videoAspectRatio);
}
+ /** Listener for content blocked events. */
public interface OnContentBlockedListener {
/** Called when the Video's aspect ratio is changed. */
void onContentBlocked(TvContentRating rating);
}
+ /** Listener for tracks availability changed events */
public interface OnTracksAvailabilityChangedListener {
/** Called when the Video's subtitle or audio tracks are changed. */
void onTracksAvailabilityChanged(boolean hasClosedCaption, boolean hasMultiAudio);
}
+ /** Listener for track selected events */
public interface OnTrackSelectedListener {
/** Called when certain subtitle or audio track is selected. */
void onTrackSelected(String selectedTrackId);
}
- public DvrPlayer(TvView tvView) {
- mTvView = tvView;
+ /** Constructor of DvrPlayer. */
+ public DvrPlayer(AppLayerTvView tvView, Context context) {
+ mTvView = new DvrTvView(context, tvView, this);
mTvView.setCaptionEnabled(true);
mPlaybackParams.setSpeed(1.0f);
setTvViewCallbacks();
setCallback(null);
+ mTvView.init();
}
/**
@@ -333,7 +348,8 @@ class DvrPlayer {
/** Returns the audio tracks of the current playback. */
public ArrayList<TvTrackInfo> getAudioTracks() {
- return new ArrayList<>(mTvView.getTracks(TvTrackInfo.TYPE_AUDIO));
+ List<TvTrackInfo> tracks = mTvView.getTracks(TvTrackInfo.TYPE_AUDIO);
+ return tracks == null ? new ArrayList<>() : new ArrayList<>(tracks);
}
/** Returns the ID of the selected track of the given type. */
@@ -352,6 +368,10 @@ class DvrPlayer {
&& mPlaybackState != PlaybackState.STATE_CONNECTING;
}
+ public void release() {
+ mTvView.release();
+ }
+
/**
* Selects the given track.
*
@@ -426,9 +446,16 @@ class DvrPlayer {
resumeToWatchedPositionIfNeeded();
}
timeMs -= mStartPositionMs;
- if (mPlaybackState == PlaybackState.STATE_REWINDING
- && timeMs <= REWIND_POSITION_MARGIN_MS) {
+ long bufferedTimeMs =
+ System.currentTimeMillis()
+ - mProgram.getStartTimeUtcMillis()
+ - FORWARD_POSITION_MARGIN_MS;
+ if ((mPlaybackState == PlaybackState.STATE_REWINDING
+ && timeMs <= REWIND_POSITION_MARGIN_MS)
+ || (mPlaybackState == PlaybackState.STATE_FAST_FORWARDING
+ && timeMs > bufferedTimeMs)) {
play();
+ mCallback.onPlaybackResume();
} else {
mTimeShiftCurrentPositionMs = getRealSeekPosition(timeMs, 0);
mCallback.onPlaybackPositionChanged(mTimeShiftCurrentPositionMs);
@@ -440,7 +467,7 @@ class DvrPlayer {
}
});
mTvView.setCallback(
- new TvView.TvInputCallback() {
+ new TvInputCallbackCompat() {
@Override
public void onTimeShiftStatusChanged(String inputId, int status) {
if (DEBUG) Log.d(TAG, "onTimeShiftStatusChanged:" + status);
diff --git a/src/com/android/tv/features/PartnerFeatures.java b/src/com/android/tv/features/PartnerFeatures.java
new file mode 100644
index 00000000..6d680b7b
--- /dev/null
+++ b/src/com/android/tv/features/PartnerFeatures.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.features;
+
+import android.content.Context;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import com.android.tv.common.feature.Feature;
+import com.google.android.tv.partner.support.PartnerCustomizations;
+
+/** Features backed by {@link PartnerCustomizations}. */
+@SuppressWarnings("AndroidApiChecker") // TODO(b/32513850) remove when error prone is updated
+public final class PartnerFeatures {
+
+ public static final Feature TVPROVIDER_ALLOWS_SYSTEM_INSERTS_TO_PROGRAM_TABLE =
+ new PartnerFeature(
+ PartnerCustomizations.TVPROVIDER_ALLOWS_SYSTEM_INSERTS_TO_PROGRAM_TABLE);
+
+ public static final Feature TURN_OFF_EMBEDDED_TUNER =
+ new PartnerFeature(PartnerCustomizations.TURN_OFF_EMBEDDED_TUNER);
+
+ public static final Feature TVPROVIDER_ALLOWS_COLUMN_CREATION =
+ new PartnerFeature(PartnerCustomizations.TVPROVIDER_ALLOWS_COLUMN_CREATION);
+
+ private static class PartnerFeature implements Feature {
+
+ private final String property;
+
+ public PartnerFeature(String property) {
+ this.property = property;
+ }
+
+ @Override
+ public boolean isEnabled(Context context) {
+ if (VERSION.SDK_INT >= VERSION_CODES.N) {
+ PartnerCustomizations partnerCustomizations = new PartnerCustomizations(context);
+ return partnerCustomizations.getBooleanResource(context, property).orElse(false);
+ }
+ return false;
+ }
+ }
+
+ private PartnerFeatures() {}
+}
diff --git a/src/com/android/tv/TvFeatures.java b/src/com/android/tv/features/TvFeatures.java
index d2cf76e7..208d53f6 100644
--- a/src/com/android/tv/TvFeatures.java
+++ b/src/com/android/tv/features/TvFeatures.java
@@ -14,13 +14,15 @@
* limitations under the License
*/
-package com.android.tv;
+package com.android.tv.features;
-import static com.android.tv.common.feature.EngOnlyFeature.ENG_ONLY_FEATURE;
-import static com.android.tv.common.feature.FeatureUtils.AND;
+import static com.android.tv.common.feature.BuildTypeFeature.ASOP_FEATURE;
+import static com.android.tv.common.feature.BuildTypeFeature.ENG_ONLY_FEATURE;
import static com.android.tv.common.feature.FeatureUtils.OFF;
import static com.android.tv.common.feature.FeatureUtils.ON;
-import static com.android.tv.common.feature.FeatureUtils.OR;
+import static com.android.tv.common.feature.FeatureUtils.and;
+import static com.android.tv.common.feature.FeatureUtils.not;
+import static com.android.tv.common.feature.FeatureUtils.or;
import android.content.Context;
import android.content.pm.PackageManager;
@@ -31,14 +33,14 @@ import com.android.tv.common.feature.CommonFeatures;
import com.android.tv.common.feature.ExperimentFeature;
import com.android.tv.common.feature.Feature;
import com.android.tv.common.feature.FeatureUtils;
-import com.android.tv.common.feature.GServiceFeature;
+import com.android.tv.common.feature.FlagFeature;
import com.android.tv.common.feature.PropertyFeature;
import com.android.tv.common.feature.Sdk;
import com.android.tv.common.feature.TestableFeature;
+import com.android.tv.common.flags.has.HasUiFlags;
+import com.android.tv.common.singletons.HasSingletons;
import com.android.tv.common.util.PermissionUtils;
-import com.google.android.tv.partner.support.PartnerCustomizations;
-
/**
* List of {@link Feature} for the Live TV App.
*
@@ -46,33 +48,48 @@ import com.google.android.tv.partner.support.PartnerCustomizations;
*/
public final class TvFeatures extends CommonFeatures {
+ /** When enabled store network affiliation information to TV provider */
+ public static final Feature STORE_NETWORK_AFFILIATION = ENG_ONLY_FEATURE;
+
/** When enabled use system setting for turning on analytics. */
public static final Feature ANALYTICS_OPT_IN =
ExperimentFeature.from(Experiments.ENABLE_ANALYTICS_VIA_CHECKBOX);
- /** When enabled shows a list of failed recordings */
- public static final Feature DVR_FAILED_LIST = ENG_ONLY_FEATURE;
/**
* Analytics that include sensitive information such as channel or program identifiers.
*
* <p>See <a href="http://b/22062676">b/22062676</a>
*/
- public static final Feature ANALYTICS_V2 = AND(ON, ANALYTICS_OPT_IN);
+ public static final Feature ANALYTICS_V2 = and(ON, ANALYTICS_OPT_IN);
+
+ private static final Feature TV_PROVIDER_ALLOWS_INSERT_TO_PROGRAM_TABLE =
+ or(Sdk.AT_LEAST_O, PartnerFeatures.TVPROVIDER_ALLOWS_SYSTEM_INSERTS_TO_PROGRAM_TABLE);
+
+ /**
+ * Enable cloud EPG for third parties.
+ *
+ * @see <a href="http://go/cloud-epg-3p-proposal">go/cloud-epg-3p-proposal</a>
+ */
+ // TODO verify customization for N
+ public static final TestableFeature CLOUD_EPG_FOR_3RD_PARTY =
+ TestableFeature.createTestableFeature(
+ and(
+ not(ASOP_FEATURE),
+ // TODO(b/66696290): use newer version of robolectric.
+ or(
+ TV_PROVIDER_ALLOWS_INSERT_TO_PROGRAM_TABLE,
+ FeatureUtils.ROBOLECTRIC)));
- public static final Feature EPG_SEARCH =
- PropertyFeature.create("feature_tv_use_epg_search", false);
+ // TODO(b/76149661): Fix EPG search or remove it
+ public static final Feature EPG_SEARCH = OFF;
- private static final String GSERVICE_KEY_UNHIDE = "live_channels_unhide";
/** A flag which indicates that LC app is unhidden even when there is no input. */
public static final Feature UNHIDE =
- OR(
- new GServiceFeature(GSERVICE_KEY_UNHIDE, false),
- new Feature() {
- @Override
- public boolean isEnabled(Context context) {
- // If LC app runs as non-system app, we unhide the app.
- return !PermissionUtils.hasAccessAllEpg(context);
- }
- });
+ or(
+ FlagFeature.from(
+ context -> HasSingletons.get(HasUiFlags.class, context),
+ input -> input.getUiFlags().uhideLauncher()),
+ // If LC app runs as non-system app, we unhide the app.
+ not(PermissionUtils::hasAccessAllEpg));
public static final Feature PICTURE_IN_PICTURE =
new Feature() {
diff --git a/src/com/android/tv/guide/ProgramGuide.java b/src/com/android/tv/guide/ProgramGuide.java
index 5b53f904..bc1b11b6 100644
--- a/src/com/android/tv/guide/ProgramGuide.java
+++ b/src/com/android/tv/guide/ProgramGuide.java
@@ -47,7 +47,7 @@ import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeL
import com.android.tv.ChannelTuner;
import com.android.tv.MainActivity;
import com.android.tv.R;
-import com.android.tv.TvFeatures;
+import com.android.tv.TvSingletons;
import com.android.tv.analytics.Tracker;
import com.android.tv.common.WeakHandler;
import com.android.tv.common.util.DurationTimer;
@@ -56,11 +56,16 @@ import com.android.tv.data.GenreItems;
import com.android.tv.data.ProgramDataManager;
import com.android.tv.dvr.DvrDataManager;
import com.android.tv.dvr.DvrScheduleManager;
+import com.android.tv.features.TvFeatures;
+import com.android.tv.perf.EventNames;
+import com.android.tv.perf.PerformanceMonitor;
+import com.android.tv.perf.TimerEvent;
import com.android.tv.ui.HardwareLayerAnimatorListenerAdapter;
import com.android.tv.ui.ViewUtils;
import com.android.tv.ui.hideable.AutoHideScheduler;
import com.android.tv.util.TvInputManagerHelper;
import com.android.tv.util.Utils;
+import com.android.tv.common.flags.BackendKnobsFlags;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
@@ -150,6 +155,9 @@ public class ProgramGuide
private final ProgramManagerListener mProgramManagerListener = new ProgramManagerListener();
+ private final PerformanceMonitor mPerformanceMonitor;
+ private TimerEvent mTimerEvent;
+
private final Runnable mUpdateTimeIndicator =
new Runnable() {
@Override
@@ -175,13 +183,17 @@ public class ProgramGuide
Runnable preShowRunnable,
Runnable postHideRunnable) {
mActivity = activity;
+ TvSingletons singletons = TvSingletons.getSingletons(mActivity);
+ mPerformanceMonitor = singletons.getPerformanceMonitor();
+ BackendKnobsFlags backendKnobsFlags = singletons.getBackendKnobs();
mProgramManager =
new ProgramManager(
tvInputManagerHelper,
channelDataManager,
programDataManager,
dvrDataManager,
- dvrScheduleManager);
+ dvrScheduleManager,
+ backendKnobsFlags);
mChannelTuner = channelTuner;
mTracker = tracker;
mPreShowRunnable = preShowRunnable;
@@ -316,12 +328,43 @@ public class ProgramGuide
mGrid.setItemAlignmentOffset(0);
mGrid.setItemAlignmentOffsetPercent(ProgramGrid.ITEM_ALIGN_OFFSET_PERCENT_DISABLED);
+ mGrid.addOnScrollListener(
+ new RecyclerView.OnScrollListener() {
+ @Override
+ public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
+ if (DEBUG) {
+ Log.d(TAG, "ProgramGrid onScrollStateChanged. newState=" + newState);
+ }
+ if (newState == RecyclerView.SCROLL_STATE_SETTLING) {
+ mPerformanceMonitor.startJankRecorder(
+ EventNames.PROGRAM_GUIDE_SCROLL_VERTICALLY);
+ } else if (newState == RecyclerView.SCROLL_STATE_IDLE) {
+ mPerformanceMonitor.stopJankRecorder(
+ EventNames.PROGRAM_GUIDE_SCROLL_VERTICALLY);
+ }
+ }
+ });
+
RecyclerView.OnScrollListener onScrollListener =
new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
onHorizontalScrolled(dx);
}
+
+ @Override
+ public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
+ if (DEBUG) {
+ Log.d(TAG, "TimelineRow onScrollStateChanged. newState=" + newState);
+ }
+ if (newState == RecyclerView.SCROLL_STATE_SETTLING) {
+ mPerformanceMonitor.startJankRecorder(
+ EventNames.PROGRAM_GUIDE_SCROLL_HORIZONTALLY);
+ } else if (newState == RecyclerView.SCROLL_STATE_IDLE) {
+ mPerformanceMonitor.stopJankRecorder(
+ EventNames.PROGRAM_GUIDE_SCROLL_HORIZONTALLY);
+ }
+ }
};
mTimelineRow.addOnScrollListener(onScrollListener);
@@ -332,6 +375,18 @@ public class ProgramGuide
R.animator.program_guide_side_panel_enter_full,
0,
R.animator.program_guide_table_enter_full);
+ mShowAnimatorFull.addListener(
+ new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (mTimerEvent != null) {
+ mPerformanceMonitor.stopTimer(
+ mTimerEvent, EventNames.PROGRAM_GUIDE_SHOW);
+ mTimerEvent = null;
+ }
+ mPerformanceMonitor.stopJankRecorder(EventNames.PROGRAM_GUIDE_SHOW);
+ }
+ });
mShowAnimatorPartial =
createAnimator(
@@ -345,6 +400,16 @@ public class ProgramGuide
mSidePanelGridView.setVisibility(View.VISIBLE);
mSidePanelGridView.setAlpha(1.0f);
}
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (mTimerEvent != null) {
+ mPerformanceMonitor.stopTimer(
+ mTimerEvent, EventNames.PROGRAM_GUIDE_SHOW);
+ mTimerEvent = null;
+ }
+ mPerformanceMonitor.stopJankRecorder(EventNames.PROGRAM_GUIDE_SHOW);
+ }
});
mHideAnimatorFull =
@@ -355,6 +420,11 @@ public class ProgramGuide
mHideAnimatorFull.addListener(
new AnimatorListenerAdapter() {
@Override
+ public void onAnimationStart(Animator animation) {
+ mPerformanceMonitor.recordMemory(EventNames.MEMORY_ON_PROGRAM_GUIDE_CLOSE);
+ }
+
+ @Override
public void onAnimationEnd(Animator animation) {
mContainer.setVisibility(View.GONE);
}
@@ -367,6 +437,11 @@ public class ProgramGuide
mHideAnimatorPartial.addListener(
new AnimatorListenerAdapter() {
@Override
+ public void onAnimationStart(Animator animation) {
+ mPerformanceMonitor.recordMemory(EventNames.MEMORY_ON_PROGRAM_GUIDE_CLOSE);
+ }
+
+ @Override
public void onAnimationEnd(Animator animation) {
mContainer.setVisibility(View.GONE);
}
@@ -447,6 +522,8 @@ public class ProgramGuide
if (mContainer.getVisibility() == View.VISIBLE) {
return;
}
+ mTimerEvent = mPerformanceMonitor.startTimer();
+ mPerformanceMonitor.startJankRecorder(EventNames.PROGRAM_GUIDE_SHOW);
mTracker.sendShowEpg();
mTracker.sendScreenView(SCREEN_NAME);
if (mPreShowRunnable != null) {
@@ -643,6 +720,11 @@ public class ProgramGuide
return mGrid;
}
+ /** Returns if Accessibility is enabled. */
+ boolean isAccessibilityEnabled() {
+ return mAccessibilityManager.isEnabled();
+ }
+
/** Gets {@link VerticalGridView} for "genre select" side panel. */
VerticalGridView getSidePanel() {
return mSidePanelGridView;
@@ -711,9 +793,7 @@ public class ProgramGuide
}
private void startFull() {
- if (!mShowGuidePartial || mAccessibilityManager.isEnabled()) {
- // If accessibility service is enabled, focus cannot be moved to side panel due to it's
- // hidden. Therefore, we don't hide side panel when accessibility service is enabled.
+ if (!mShowGuidePartial) {
return;
}
mShowGuidePartial = false;
@@ -806,13 +886,7 @@ public class ProgramGuide
detailView.setVisibility(View.VISIBLE);
final ProgramRow programRow = (ProgramRow) row.findViewById(R.id.row);
- programRow.post(
- new Runnable() {
- @Override
- public void run() {
- programRow.focusCurrentProgram();
- }
- });
+ programRow.post(programRow::focusCurrentProgram);
} else {
animateRowChange(mSelectedRow, row);
}
@@ -935,6 +1009,7 @@ public class ProgramGuide
private static final int UNKNOWN = 0;
private static final int SIDE_PANEL = 1;
private static final int PROGRAM_TABLE = 2;
+ private static final int CHANNEL_COLUMN = 3;
@Override
public void onGlobalFocusChanged(View oldFocus, View newFocus) {
@@ -948,6 +1023,10 @@ public class ProgramGuide
startFull();
} else if (fromLocation == PROGRAM_TABLE && toLocation == SIDE_PANEL) {
startPartial();
+ } else if (fromLocation == CHANNEL_COLUMN && toLocation == PROGRAM_TABLE) {
+ startFull();
+ } else if (fromLocation == PROGRAM_TABLE && toLocation == CHANNEL_COLUMN) {
+ startPartial();
}
}
@@ -959,7 +1038,11 @@ public class ProgramGuide
if (obj == mSidePanel) {
return SIDE_PANEL;
} else if (obj == mGrid) {
- return PROGRAM_TABLE;
+ if (view instanceof ProgramItemView) {
+ return PROGRAM_TABLE;
+ } else {
+ return CHANNEL_COLUMN;
+ }
}
}
return UNKNOWN;
diff --git a/src/com/android/tv/guide/ProgramItemView.java b/src/com/android/tv/guide/ProgramItemView.java
index 9f379e43..a46beab7 100644
--- a/src/com/android/tv/guide/ProgramItemView.java
+++ b/src/com/android/tv/guide/ProgramItemView.java
@@ -103,12 +103,9 @@ public class ProgramItemView extends TextView {
tvActivity.getChannelDataManager().getChannel(entry.channelId);
if (entry.isCurrentProgram()) {
view.postDelayed(
- new Runnable() {
- @Override
- public void run() {
- tvActivity.tuneToChannel(channel);
- tvActivity.hideOverlaysForTune();
- }
+ () -> {
+ tvActivity.tuneToChannel(channel);
+ tvActivity.hideOverlaysForTune();
},
entry.getWidth() > ((ProgramItemView) view).mMaxWidthForRipple
? 0
@@ -125,13 +122,9 @@ public class ProgramItemView extends TextView {
DvrUiHelper.checkStorageStatusAndShowErrorMessage(
tvActivity,
channel.getInputId(),
- new Runnable() {
- @Override
- public void run() {
+ () ->
DvrUiHelper.requestRecordingFutureProgram(
- tvActivity, entry.program, false);
- }
- });
+ tvActivity, entry.program, false));
} else {
dvrManager.removeScheduledRecording(entry.scheduledRecording);
String msg =
@@ -378,7 +371,7 @@ public class ProgramItemView extends TextView {
int iconResId = 0;
if (isEntryWideEnough() && mTableEntry.scheduledRecording != null) {
if (mDvrManager.isConflicting(mTableEntry.scheduledRecording)) {
- iconResId = R.drawable.ic_warning_white_18dp;
+ iconResId = R.drawable.quantum_ic_warning_white_18;
} else {
switch (mTableEntry.scheduledRecording.getState()) {
case ScheduledRecording.STATE_RECORDING_NOT_STARTED:
@@ -405,20 +398,22 @@ public class ProgramItemView extends TextView {
if (channel != null) {
description = channel.getDisplayNumber() + " " + description;
}
- description +=
- " "
- + Utils.getDurationString(
- getContext(),
- mClock,
- mTableEntry.entryStartUtcMillis,
- mTableEntry.entryEndUtcMillis,
- true);
Program program = mTableEntry.program;
if (program != null) {
+ description += " " + program.getDurationString(getContext());
String episodeDescription = program.getEpisodeContentDescription(getContext());
if (!TextUtils.isEmpty(episodeDescription)) {
description += " " + episodeDescription;
}
+ } else {
+ description +=
+ " "
+ + Utils.getDurationString(
+ getContext(),
+ mClock,
+ mTableEntry.entryStartUtcMillis,
+ mTableEntry.entryEndUtcMillis,
+ true);
}
if (mTableEntry.scheduledRecording != null) {
if (mDvrManager.isConflicting(mTableEntry.scheduledRecording)) {
diff --git a/src/com/android/tv/guide/ProgramManager.java b/src/com/android/tv/guide/ProgramManager.java
index 3f20a837..3a5a4a02 100644
--- a/src/com/android/tv/guide/ProgramManager.java
+++ b/src/com/android/tv/guide/ProgramManager.java
@@ -32,6 +32,7 @@ import com.android.tv.dvr.DvrScheduleManager.OnConflictStateChangeListener;
import com.android.tv.dvr.data.ScheduledRecording;
import com.android.tv.util.TvInputManagerHelper;
import com.android.tv.util.Utils;
+import com.android.tv.common.flags.BackendKnobsFlags;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@@ -59,6 +60,7 @@ public class ProgramManager {
private final ProgramDataManager mProgramDataManager;
private final DvrDataManager mDvrDataManager; // Only set if DVR is enabled
private final DvrScheduleManager mDvrScheduleManager;
+ private final BackendKnobsFlags mBackendKnobsFlags;
private long mStartUtcMillis;
private long mEndUtcMillis;
@@ -114,12 +116,26 @@ public class ProgramManager {
}
};
- private final ProgramDataManager.Listener mProgramDataManagerListener =
- new ProgramDataManager.Listener() {
+ private final ProgramDataManager.Callback mProgramDataManagerCallback =
+ new ProgramDataManager.Callback() {
@Override
public void onProgramUpdated() {
updateTableEntries(true);
}
+
+ @Override
+ public void onSingleChannelUpdated(long channelId) {
+ boolean parentalControlsEnabled =
+ mTvInputManagerHelper
+ .getParentalControlSettings()
+ .isParentalControlsEnabled();
+ // Inline the updating of the mChannelIdEntriesMap here so we can only call
+ // getParentalControlSettings once.
+ List<TableEntry> entries =
+ createProgramEntries(channelId, parentalControlsEnabled);
+ mChannelIdEntriesMap.put(channelId, entries);
+ notifyTableEntriesUpdated();
+ }
};
private final DvrDataManager.ScheduledRecordingListener mScheduledRecordingListener =
@@ -199,19 +215,21 @@ public class ProgramManager {
ChannelDataManager channelDataManager,
ProgramDataManager programDataManager,
@Nullable DvrDataManager dvrDataManager,
- @Nullable DvrScheduleManager dvrScheduleManager) {
+ @Nullable DvrScheduleManager dvrScheduleManager,
+ BackendKnobsFlags backendKnobsFlags) {
mTvInputManagerHelper = tvInputManagerHelper;
mChannelDataManager = channelDataManager;
mProgramDataManager = programDataManager;
mDvrDataManager = dvrDataManager;
mDvrScheduleManager = dvrScheduleManager;
+ mBackendKnobsFlags = backendKnobsFlags;
}
void programGuideVisibilityChanged(boolean visible) {
mProgramDataManager.setPauseProgramUpdate(visible);
if (visible) {
mChannelDataManager.addListener(mChannelDataManagerListener);
- mProgramDataManager.addListener(mProgramDataManagerListener);
+ mProgramDataManager.addCallback(mProgramDataManagerCallback);
if (mDvrDataManager != null) {
if (!mDvrDataManager.isDvrScheduleLoadFinished()) {
mDvrDataManager.addDvrScheduleLoadFinishedListener(mDvrLoadedListener);
@@ -224,7 +242,7 @@ public class ProgramManager {
}
} else {
mChannelDataManager.removeListener(mChannelDataManagerListener);
- mProgramDataManager.removeListener(mProgramDataManagerListener);
+ mProgramDataManager.removeCallback(mProgramDataManagerCallback);
if (mDvrDataManager != null) {
mDvrDataManager.removeDvrScheduleLoadFinishedListener(mDvrLoadedListener);
mDvrDataManager.removeScheduledRecordingListener(mScheduledRecordingListener);
@@ -233,6 +251,7 @@ public class ProgramManager {
mDvrScheduleManager.removeOnConflictStateChangeListener(
mOnConflictStateChangeListener);
}
+ mChannelIdEntriesMap.clear();
}
}
@@ -309,8 +328,8 @@ public class ProgramManager {
long fromUtcMillis = mFromUtcMillis + timeMillisToScroll;
long toUtcMillis = mToUtcMillis + timeMillisToScroll;
if (fromUtcMillis < mStartUtcMillis) {
- fromUtcMillis = mStartUtcMillis;
toUtcMillis += mStartUtcMillis - fromUtcMillis;
+ fromUtcMillis = mStartUtcMillis;
}
if (toUtcMillis > mEndUtcMillis) {
fromUtcMillis -= toUtcMillis - mEndUtcMillis;
@@ -345,10 +364,12 @@ public class ProgramManager {
/** Returns the program index of the program at {@code time} or -1 if not found. */
int getProgramIndexAtTime(long channelId, long time) {
List<TableEntry> entries = mChannelIdEntriesMap.get(channelId);
- for (int i = 0; i < entries.size(); ++i) {
- TableEntry entry = entries.get(i);
- if (entry.entryStartUtcMillis <= time && time < entry.entryEndUtcMillis) {
- return i;
+ if (entries != null) {
+ for (int i = 0; i < entries.size(); ++i) {
+ TableEntry entry = entries.get(i);
+ if (entry.entryStartUtcMillis <= time && time < entry.entryEndUtcMillis) {
+ return i;
+ }
}
}
return -1;
@@ -401,7 +422,7 @@ public class ProgramManager {
* given {@code channelId}.
*/
int getTableEntryCount(long channelId) {
- return mChannelIdEntriesMap.get(channelId).size();
+ return mChannelIdEntriesMap.isEmpty() ? 0 : mChannelIdEntriesMap.get(channelId).size();
}
/**
@@ -410,6 +431,9 @@ public class ProgramManager {
* (e.g., whose channelId is INVALID_ID), when it corresponds to a gap between programs.
*/
TableEntry getTableEntry(long channelId, int index) {
+ if (mBackendKnobsFlags.enablePartialProgramFetch()) {
+ mProgramDataManager.prefetchChannel(channelId);
+ }
return mChannelIdEntriesMap.get(channelId).get(index);
}
@@ -437,6 +461,14 @@ public class ProgramManager {
buildGenreFilters();
}
+ /** Sets the channel list for testing */
+ void setChannels(List<Channel> channels) {
+ mChannels = new ArrayList<>(channels);
+ mSelectedGenreId = GenreItems.ID_ALL_CHANNELS;
+ mFilteredChannels = mChannels;
+ buildGenreFilters();
+ }
+
private void updateTableEntries(boolean clear) {
updateTableEntriesWithoutNotification(clear);
notifyTableEntriesUpdated();
@@ -544,6 +576,9 @@ public class ProgramManager {
@Nullable
private TableEntry getTableEntry(long channelId, long entryId) {
+ if (mChannelIdEntriesMap.isEmpty()) {
+ return null;
+ }
List<TableEntry> entries = mChannelIdEntriesMap.get(channelId);
if (entries != null) {
for (TableEntry entry : entries) {
diff --git a/src/com/android/tv/guide/ProgramRow.java b/src/com/android/tv/guide/ProgramRow.java
index 83175bb6..3317c15f 100644
--- a/src/com/android/tv/guide/ProgramRow.java
+++ b/src/com/android/tv/guide/ProgramRow.java
@@ -72,6 +72,9 @@ public class ProgramRow extends TimelineGridView {
public ProgramRow(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
+ ProgramRowAccessibilityDelegate rowAccessibilityDelegate =
+ new ProgramRowAccessibilityDelegate(this);
+ this.setAccessibilityDelegateCompat(rowAccessibilityDelegate);
}
/** Registers a listener focus events occurring on children to the {@code ProgramRow}. */
@@ -126,13 +129,26 @@ public class ProgramRow extends TimelineGridView {
: direction == View.FOCUS_LEFT;
}
+ // When Accessibility is enabled, this API will keep next node visible
+ void focusSearchAccessibility(View focused, int direction) {
+ TableEntry focusedEntry = ((ProgramItemView) focused).getTableEntry();
+ long toMillis = mProgramManager.getToUtcMillis();
+
+ if (isDirectionEnd(direction) || direction == View.FOCUS_FORWARD) {
+ if (focusedEntry.entryEndUtcMillis >= toMillis) {
+ scrollByTime(focusedEntry.entryEndUtcMillis - toMillis + HALF_HOUR_MILLIS);
+ }
+ }
+ }
+
@Override
public View focusSearch(View focused, int direction) {
TableEntry focusedEntry = ((ProgramItemView) focused).getTableEntry();
long fromMillis = mProgramManager.getFromUtcMillis();
long toMillis = mProgramManager.getToUtcMillis();
- if (isDirectionStart(direction) || direction == View.FOCUS_BACKWARD) {
+ if (!mProgramGuide.isAccessibilityEnabled()
+ && (isDirectionStart(direction) || direction == View.FOCUS_BACKWARD)) {
if (focusedEntry.entryStartUtcMillis < fromMillis) {
// The current entry starts outside of the view; Align or scroll to the left.
scrollByTime(
@@ -162,7 +178,9 @@ public class ProgramRow extends TimelineGridView {
TableEntry targetEntry = ((ProgramItemView) target).getTableEntry();
if (isDirectionStart(direction) || direction == View.FOCUS_BACKWARD) {
- if (targetEntry.entryStartUtcMillis < fromMillis
+ if (mProgramGuide.isAccessibilityEnabled()) {
+ scrollByTime(targetEntry.entryStartUtcMillis - fromMillis);
+ } else if (targetEntry.entryStartUtcMillis < fromMillis
&& targetEntry.entryEndUtcMillis < fromMillis + HALF_HOUR_MILLIS) {
// The target entry starts outside the view; Align or scroll to the left.
scrollByTime(
diff --git a/src/com/android/tv/guide/ProgramRowAccessibilityDelegate.java b/src/com/android/tv/guide/ProgramRowAccessibilityDelegate.java
new file mode 100644
index 00000000..5e498be4
--- /dev/null
+++ b/src/com/android/tv/guide/ProgramRowAccessibilityDelegate.java
@@ -0,0 +1,64 @@
+/*
+ * 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.guide;
+
+import android.os.Bundle;
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.RecyclerViewAccessibilityDelegate;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+/** AccessibilityDelegate for {@link ProgramRow} */
+class ProgramRowAccessibilityDelegate extends RecyclerViewAccessibilityDelegate {
+ private final ItemDelegate mItemDelegate;
+
+ ProgramRowAccessibilityDelegate(RecyclerView recyclerView) {
+ super(recyclerView);
+
+ mItemDelegate =
+ new ItemDelegate(this) {
+ @Override
+ public boolean performAccessibilityAction(View host, int action, Bundle args) {
+ // Prevent Accessibility service to move the Program Row elements
+ // Ignoring Accessibility action above Set Text
+ // (accessibilityActionShowOnScreen)
+ if (action > AccessibilityNodeInfo.ACTION_SET_TEXT) {
+ return false;
+ }
+
+ return super.performAccessibilityAction(host, action, args);
+ }
+ };
+ }
+
+ @Override
+ public ItemDelegate getItemDelegate() {
+ return mItemDelegate;
+ }
+
+ @Override
+ public boolean onRequestSendAccessibilityEvent(
+ ViewGroup host, View child, AccessibilityEvent event) {
+ // Forcing the next item to be visible for scrolling in forward direction
+ if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_FOCUSED) {
+ ((ProgramRow) host).focusSearchAccessibility(child, View.FOCUS_FORWARD);
+ }
+ return super.onRequestSendAccessibilityEvent(host, child, event);
+ }
+}
diff --git a/src/com/android/tv/guide/ProgramTableAdapter.java b/src/com/android/tv/guide/ProgramTableAdapter.java
index 6e7485ac..7576bf50 100644
--- a/src/com/android/tv/guide/ProgramTableAdapter.java
+++ b/src/com/android/tv/guide/ProgramTableAdapter.java
@@ -110,6 +110,8 @@ class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapter.Progr
private final int mDvrPaddingStartWithTrack;
private final int mDvrPaddingStartWithOutTrack;
+ private RecyclerView mRecyclerView;
+
ProgramTableAdapter(Context context, ProgramGuide programGuide) {
mContext = context;
mAccessibilityManager =
@@ -198,7 +200,15 @@ class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapter.Progr
mProgramManager.addTableEntriesUpdatedListener(listAdapter);
mProgramListAdapters.add(listAdapter);
}
- notifyDataSetChanged();
+ if (mRecyclerView != null && mRecyclerView.isComputingLayout()) {
+ // it means that RecyclerView is in a lockdown state and any attempt to update adapter
+ // contents will result in an exception because adapter contents cannot be changed while
+ // RecyclerView is trying to compute the layout
+ // postpone the change using a Handler
+ mHandler.post(this::notifyDataSetChanged);
+ } else {
+ notifyDataSetChanged();
+ }
}
@Override
@@ -238,8 +248,22 @@ class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapter.Progr
int channelIndex = mProgramManager.getChannelIndex(tableEntry.channelId);
int pos = mProgramManager.getProgramIdIndex(tableEntry.channelId, tableEntry.getId());
if (DEBUG) Log.d(TAG, "update(" + channelIndex + ", " + pos + ")");
- mProgramListAdapters.get(channelIndex).notifyItemChanged(pos, tableEntry);
- notifyItemChanged(channelIndex, true);
+ if (channelIndex >= 0 && channelIndex < mProgramListAdapters.size()) {
+ mProgramListAdapters.get(channelIndex).notifyItemChanged(pos, tableEntry);
+ notifyItemChanged(channelIndex, true);
+ }
+ }
+
+ @Override
+ public void onAttachedToRecyclerView(RecyclerView recyclerView) {
+ mRecyclerView = recyclerView;
+ super.onAttachedToRecyclerView(recyclerView);
+ }
+
+ @Override
+ public void onDetachedFromRecyclerView(RecyclerView recyclerView) {
+ super.onDetachedFromRecyclerView(recyclerView);
+ mRecyclerView = null;
}
class ProgramRowViewHolder extends RecyclerView.ViewHolder
@@ -260,13 +284,7 @@ class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapter.Progr
}
}
};
- private final Runnable mUpdateDetailViewRunnable =
- new Runnable() {
- @Override
- public void run() {
- updateDetailView();
- }
- };
+ private final Runnable mUpdateDetailViewRunnable = this::updateDetailView;
private final RecyclerView.OnScrollListener mOnScrollListener =
new RecyclerView.OnScrollListener() {
@@ -420,12 +438,14 @@ class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapter.Progr
mChannelNumberView.setText(displayNumber);
mChannelNumberView.setVisibility(View.VISIBLE);
}
+
+ boolean isChannelLocked = isChannelLocked(channel);
mChannelNumberView.setTextColor(
- isChannelLocked(channel) ? mChannelBlockedTextColor : mChannelTextColor);
+ isChannelLocked ? mChannelBlockedTextColor : mChannelTextColor);
mChannelLogoView.setImageBitmap(null);
mChannelLogoView.setVisibility(View.GONE);
- if (isChannelLocked(channel)) {
+ if (isChannelLocked) {
mChannelNameView.setVisibility(View.GONE);
mChannelBlockView.setVisibility(View.VISIBLE);
} else {
@@ -573,13 +593,7 @@ class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapter.Progr
mTitleView.setText(text);
}
- updateTextView(
- mTimeView,
- Utils.getDurationString(
- context,
- program.getStartTimeUtcMillis(),
- program.getEndTimeUtcMillis(),
- false));
+ updateTextView(mTimeView, program.getDurationString(context));
boolean trackMetaDataVisible =
updateTextView(
diff --git a/src/com/android/tv/menu/ChannelsRowAdapter.java b/src/com/android/tv/menu/ChannelsRowAdapter.java
index 8536ef1f..4a9e4765 100644
--- a/src/com/android/tv/menu/ChannelsRowAdapter.java
+++ b/src/com/android/tv/menu/ChannelsRowAdapter.java
@@ -20,6 +20,9 @@ import android.content.Context;
import android.content.Intent;
import android.media.tv.TvInputInfo;
import android.view.View;
+import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener;
+import com.android.tv.ChannelChanger;
import com.android.tv.R;
import com.android.tv.TvSingletons;
import com.android.tv.analytics.Tracker;
@@ -34,9 +37,8 @@ import java.util.ArrayList;
import java.util.List;
/** An adapter of the Channels row. */
-public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter<ChannelsRowItem> {
- // There are four special cards: guide, setup, dvr, applink.
- private static final int SIZE_OF_VIEW_TYPE = 5;
+public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter<ChannelsRowItem>
+ implements AccessibilityStateChangeListener {
private final Context mContext;
private final Tracker mTracker;
@@ -44,58 +46,9 @@ public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter<Channels
private final DvrDataManager mDvrDataManager;
private final int mMaxCount;
private final int mMinCount;
+ private final ChannelChanger mChannelChanger;
- private final View.OnClickListener mGuideOnClickListener =
- new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- mTracker.sendMenuClicked(R.string.channels_item_program_guide);
- getMainActivity().getOverlayManager().showProgramGuide();
- }
- };
-
- private final View.OnClickListener mSetupOnClickListener =
- new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- mTracker.sendMenuClicked(R.string.channels_item_setup);
- getMainActivity().getOverlayManager().showSetupFragment();
- }
- };
-
- private final View.OnClickListener mDvrOnClickListener =
- new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- mTracker.sendMenuClicked(R.string.channels_item_dvr);
- getMainActivity().getOverlayManager().showDvrManager();
- }
- };
-
- private final View.OnClickListener mAppLinkOnClickListener =
- new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- mTracker.sendMenuClicked(R.string.channels_item_app_link);
- Intent intent = ((AppLinkCardView) view).getIntent();
- if (intent != null) {
- getMainActivity().startActivitySafe(intent);
- }
- }
- };
-
- private final View.OnClickListener mChannelOnClickListener =
- new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- // Always send the label "Channels" because the channel ID or name or number
- // might be
- // sensitive.
- mTracker.sendMenuClicked(R.string.menu_title_channels);
- getMainActivity().tuneToChannel((Channel) view.getTag());
- getMainActivity().hideOverlaysForTune();
- }
- };
+ private boolean mShowChannelUpDown;
public ChannelsRowAdapter(
Context context, Recommender recommender, int minCount, int maxCount) {
@@ -112,6 +65,11 @@ public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter<Channels
mMinCount = minCount;
mMaxCount = maxCount;
setHasStableIds(true);
+ mChannelChanger = (ChannelChanger) (context);
+ AccessibilityManager accessibilityManager =
+ context.getSystemService(AccessibilityManager.class);
+ mShowChannelUpDown = accessibilityManager.isEnabled();
+ accessibilityManager.addAccessibilityStateChangeListener(this);
}
@Override
@@ -133,18 +91,22 @@ public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter<Channels
public void onBindViewHolder(MyViewHolder viewHolder, int position) {
int viewType = getItemViewType(position);
if (viewType == R.layout.menu_card_guide) {
- viewHolder.itemView.setOnClickListener(mGuideOnClickListener);
+ viewHolder.itemView.setOnClickListener(this::onGuideClicked);
+ } else if (viewType == R.layout.menu_card_up) {
+ viewHolder.itemView.setOnClickListener(this::onChannelUpClicked);
+ } else if (viewType == R.layout.menu_card_down) {
+ viewHolder.itemView.setOnClickListener(this::onChannelDownClicked);
} else if (viewType == R.layout.menu_card_setup) {
- viewHolder.itemView.setOnClickListener(mSetupOnClickListener);
+ viewHolder.itemView.setOnClickListener(this::onSetupClicked);
} else if (viewType == R.layout.menu_card_app_link) {
- viewHolder.itemView.setOnClickListener(mAppLinkOnClickListener);
+ viewHolder.itemView.setOnClickListener(this::onAppLinkClicked);
} else if (viewType == R.layout.menu_card_dvr) {
- viewHolder.itemView.setOnClickListener(mDvrOnClickListener);
+ viewHolder.itemView.setOnClickListener(this::onDvrClicked);
SimpleCardView view = (SimpleCardView) viewHolder.itemView;
view.setText(R.string.channels_item_dvr);
} else {
viewHolder.itemView.setTag(getItemList().get(position).getChannel());
- viewHolder.itemView.setOnClickListener(mChannelOnClickListener);
+ viewHolder.itemView.setOnClickListener(this::onChannelClicked);
}
super.onBindViewHolder(viewHolder, position);
}
@@ -158,9 +120,53 @@ public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter<Channels
}
}
+ private void onGuideClicked(View unused) {
+ mTracker.sendMenuClicked(R.string.channels_item_program_guide);
+ getMainActivity().getOverlayManager().showProgramGuide();
+ }
+
+ private void onChannelDownClicked(View unused) {
+ mChannelChanger.channelDown();
+ }
+
+ private void onChannelUpClicked(View unused) {
+ mChannelChanger.channelUp();
+ }
+
+ private void onSetupClicked(View unused) {
+ mTracker.sendMenuClicked(R.string.channels_item_setup);
+ getMainActivity().getOverlayManager().showSetupFragment();
+ }
+
+ private void onDvrClicked(View unused) {
+ mTracker.sendMenuClicked(R.string.channels_item_dvr);
+ getMainActivity().getOverlayManager().showDvrManager();
+ }
+
+ private void onAppLinkClicked(View view) {
+ mTracker.sendMenuClicked(R.string.channels_item_app_link);
+ Intent intent = ((AppLinkCardView) view).getIntent();
+ if (intent != null) {
+ getMainActivity().startActivitySafe(intent);
+ }
+ }
+
+ private void onChannelClicked(View view) {
+ // Always send the label "Channels" because the channel ID or name or number might be
+ // sensitive.
+ mTracker.sendMenuClicked(R.string.menu_title_channels);
+ getMainActivity().tuneToChannel((Channel) view.getTag());
+ getMainActivity().hideOverlaysForTune();
+ }
+
private void createItems() {
List<ChannelsRowItem> items = new ArrayList<>();
items.add(ChannelsRowItem.GUIDE_ITEM);
+ if (mShowChannelUpDown) {
+ items.add(ChannelsRowItem.UP_ITEM);
+ items.add(ChannelsRowItem.DOWN_ITEM);
+ }
+
if (needToShowSetupItem()) {
items.add(ChannelsRowItem.SETUP_ITEM);
}
@@ -183,6 +189,12 @@ public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter<Channels
// The current index of the item list to iterate. It starts from 1 because the first item
// (GUIDE) is always visible and not updated.
int currentIndex = 1;
+ if (updateItem(mShowChannelUpDown, ChannelsRowItem.UP_ITEM, currentIndex)) {
+ ++currentIndex;
+ }
+ if (updateItem(mShowChannelUpDown, ChannelsRowItem.DOWN_ITEM, currentIndex)) {
+ ++currentIndex;
+ }
if (updateItem(needToShowSetupItem(), ChannelsRowItem.SETUP_ITEM, currentIndex)) {
++currentIndex;
}
@@ -298,4 +310,10 @@ public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter<Channels
channelList.add(channel);
return true;
}
+
+ @Override
+ public void onAccessibilityStateChanged(boolean enabled) {
+ mShowChannelUpDown = enabled;
+ update();
+ }
}
diff --git a/src/com/android/tv/menu/ChannelsRowItem.java b/src/com/android/tv/menu/ChannelsRowItem.java
index 608bb36e..12976ef2 100644
--- a/src/com/android/tv/menu/ChannelsRowItem.java
+++ b/src/com/android/tv/menu/ChannelsRowItem.java
@@ -30,6 +30,10 @@ public class ChannelsRowItem {
public static final int DVR_ITEM_ID = -3;
/** The item ID for app link item */
public static final int APP_LINK_ITEM_ID = -4;
+ /** The item ID for channel up item */
+ public static final int UP_ID = -5;
+ /** The item ID for app link item */
+ public static final int DOWN_ID = -6;
/** The item which represents the guide. */
public static final ChannelsRowItem GUIDE_ITEM =
@@ -44,6 +48,12 @@ public class ChannelsRowItem {
public static final ChannelsRowItem APP_LINK_ITEM =
new ChannelsRowItem(APP_LINK_ITEM_ID, R.layout.menu_card_app_link);
+ /** The item which represents the channel up. */
+ public static final ChannelsRowItem UP_ITEM = new ChannelsRowItem(UP_ID, R.layout.menu_card_up);
+ /** The item which represents the channel down. */
+ public static final ChannelsRowItem DOWN_ITEM =
+ new ChannelsRowItem(DOWN_ID, R.layout.menu_card_down);
+
private final long mItemId;
@NonNull private Channel mChannel;
private final int mLayoutId;
diff --git a/src/com/android/tv/menu/Menu.java b/src/com/android/tv/menu/Menu.java
index 19a93dbc..6bdbf87b 100644
--- a/src/com/android/tv/menu/Menu.java
+++ b/src/com/android/tv/menu/Menu.java
@@ -213,12 +213,9 @@ public class Menu implements AccessibilityStateChangeListener {
rowIdToSelect,
mAnimationDisabledForTest
? null
- : new Runnable() {
- @Override
- public void run() {
- if (isActive()) {
- mShowAnimator.start();
- }
+ : () -> {
+ if (isActive()) {
+ mShowAnimator.start();
}
});
scheduleHide();
diff --git a/src/com/android/tv/menu/MenuAction.java b/src/com/android/tv/menu/MenuAction.java
index 52372535..8c180cae 100644
--- a/src/com/android/tv/menu/MenuAction.java
+++ b/src/com/android/tv/menu/MenuAction.java
@@ -50,12 +50,12 @@ public class MenuAction {
new MenuAction(
R.string.options_item_more_channels,
TvOptionsManager.OPTION_MORE_CHANNELS,
- R.drawable.ic_store);
+ R.drawable.ic_app_store);
public static final MenuAction DEV_ACTION =
new MenuAction(
R.string.options_item_developer,
TvOptionsManager.OPTION_DEVELOPER,
- R.drawable.ic_developer_mode_tv_white_48dp);
+ R.drawable.quantum_ic_developer_mode_tv_white_48);
public static final MenuAction SETTINGS_ACTION =
new MenuAction(
R.string.options_item_settings,
diff --git a/src/com/android/tv/menu/OptionsRowAdapter.java b/src/com/android/tv/menu/OptionsRowAdapter.java
index ceffe861..4e69a601 100644
--- a/src/com/android/tv/menu/OptionsRowAdapter.java
+++ b/src/com/android/tv/menu/OptionsRowAdapter.java
@@ -37,17 +37,14 @@ public abstract class OptionsRowAdapter extends ItemListRowView.ItemListAdapter<
public void onClick(View view) {
final MenuAction action = (MenuAction) view.getTag();
view.post(
- new Runnable() {
- @Override
- public void run() {
- int resId = action.getActionNameResId();
- if (resId == 0) {
- mTracker.sendMenuClicked(CUSTOM_ACTION_LABEL);
- } else {
- mTracker.sendMenuClicked(resId);
- }
- executeAction(action.getType());
+ () -> {
+ int resId = action.getActionNameResId();
+ if (resId == 0) {
+ mTracker.sendMenuClicked(CUSTOM_ACTION_LABEL);
+ } else {
+ mTracker.sendMenuClicked(resId);
}
+ executeAction(action.getType());
});
}
};
diff --git a/src/com/android/tv/menu/PlayControlsRowView.java b/src/com/android/tv/menu/PlayControlsRowView.java
index 496d1969..0ce74ae1 100644
--- a/src/com/android/tv/menu/PlayControlsRowView.java
+++ b/src/com/android/tv/menu/PlayControlsRowView.java
@@ -185,13 +185,10 @@ public class PlayControlsRowView extends MenuRowView {
R.drawable.lb_ic_skip_previous,
R.string.play_controls_description_skip_previous,
null,
- new Runnable() {
- @Override
- public void run() {
- if (mTimeShiftManager.isAvailable()) {
- mTimeShiftManager.jumpToPrevious();
- updateControls(true);
- }
+ () -> {
+ if (mTimeShiftManager.isAvailable()) {
+ mTimeShiftManager.jumpToPrevious();
+ updateControls(true);
}
});
initializeButton(
@@ -199,13 +196,10 @@ public class PlayControlsRowView extends MenuRowView {
R.drawable.lb_ic_fast_rewind,
R.string.play_controls_description_fast_rewind,
null,
- new Runnable() {
- @Override
- public void run() {
- if (mTimeShiftManager.isAvailable()) {
- mTimeShiftManager.rewind();
- updateButtons();
- }
+ () -> {
+ if (mTimeShiftManager.isAvailable()) {
+ mTimeShiftManager.rewind();
+ updateButtons();
}
});
initializeButton(
@@ -213,13 +207,10 @@ public class PlayControlsRowView extends MenuRowView {
R.drawable.lb_ic_play,
R.string.play_controls_description_play_pause,
null,
- new Runnable() {
- @Override
- public void run() {
- if (mTimeShiftManager.isAvailable()) {
- mTimeShiftManager.togglePlayPause();
- updateButtons();
- }
+ () -> {
+ if (mTimeShiftManager.isAvailable()) {
+ mTimeShiftManager.togglePlayPause();
+ updateButtons();
}
});
initializeButton(
@@ -227,13 +218,10 @@ public class PlayControlsRowView extends MenuRowView {
R.drawable.lb_ic_fast_forward,
R.string.play_controls_description_fast_forward,
null,
- new Runnable() {
- @Override
- public void run() {
- if (mTimeShiftManager.isAvailable()) {
- mTimeShiftManager.fastForward();
- updateButtons();
- }
+ () -> {
+ if (mTimeShiftManager.isAvailable()) {
+ mTimeShiftManager.fastForward();
+ updateButtons();
}
});
initializeButton(
@@ -241,13 +229,10 @@ public class PlayControlsRowView extends MenuRowView {
R.drawable.lb_ic_skip_next,
R.string.play_controls_description_skip_next,
null,
- new Runnable() {
- @Override
- public void run() {
- if (mTimeShiftManager.isAvailable()) {
- mTimeShiftManager.jumpToNext();
- updateControls(true);
- }
+ () -> {
+ if (mTimeShiftManager.isAvailable()) {
+ mTimeShiftManager.jumpToNext();
+ updateControls(true);
}
});
int color =
@@ -257,12 +242,7 @@ public class PlayControlsRowView extends MenuRowView {
R.drawable.ic_record_start,
R.string.channels_item_record_start,
color,
- new Runnable() {
- @Override
- public void run() {
- onRecordButtonClicked();
- }
- });
+ this::onRecordButtonClicked);
}
private boolean isCurrentChannelRecording() {
@@ -296,13 +276,9 @@ public class PlayControlsRowView extends MenuRowView {
DvrUiHelper.checkStorageStatusAndShowErrorMessage(
mMainActivity,
currentChannel.getInputId(),
- new Runnable() {
- @Override
- public void run() {
+ () ->
DvrUiHelper.requestRecordingCurrentProgram(
- mMainActivity, currentChannel, program, true);
- }
- });
+ mMainActivity, currentChannel, program, true));
}
} else if (currentChannel != null) {
DvrUiHelper.showStopRecordingDialog(
@@ -490,15 +466,12 @@ public class PlayControlsRowView extends MenuRowView {
// After the focus is actually changed, hideRippleAnimation should run
// to reflect the result of the focus change. To be sure, hideRippleAnimation is posted.
post(
- new Runnable() {
- @Override
- public void run() {
- mJumpPreviousButton.hideRippleAnimation();
- mRewindButton.hideRippleAnimation();
- mPlayPauseButton.hideRippleAnimation();
- mFastForwardButton.hideRippleAnimation();
- mJumpNextButton.hideRippleAnimation();
- }
+ () -> {
+ mJumpPreviousButton.hideRippleAnimation();
+ mRewindButton.hideRippleAnimation();
+ mPlayPauseButton.hideRippleAnimation();
+ mFastForwardButton.hideRippleAnimation();
+ mJumpNextButton.hideRippleAnimation();
});
}
diff --git a/src/com/android/tv/menu/TvOptionsRowAdapter.java b/src/com/android/tv/menu/TvOptionsRowAdapter.java
index 55affb59..fe52b25e 100644
--- a/src/com/android/tv/menu/TvOptionsRowAdapter.java
+++ b/src/com/android/tv/menu/TvOptionsRowAdapter.java
@@ -18,12 +18,11 @@ package com.android.tv.menu;
import android.content.Context;
import android.media.tv.TvTrackInfo;
-import android.support.annotation.VisibleForTesting;
-import com.android.tv.TvFeatures;
import com.android.tv.TvOptionsManager;
import com.android.tv.common.customization.CustomAction;
import com.android.tv.common.util.CommonUtils;
import com.android.tv.data.DisplayMode;
+import com.android.tv.features.TvFeatures;
import com.android.tv.ui.TvViewUiManager;
import com.android.tv.ui.sidepanel.ClosedCaptionFragment;
import com.android.tv.ui.sidepanel.DeveloperOptionFragment;
@@ -78,7 +77,6 @@ public class TvOptionsRowAdapter extends CustomizableOptionsRowAdapter {
}
}
- @VisibleForTesting
private boolean updateClosedCaptionAction() {
return updateActionDescription(MenuAction.SELECT_CLOSED_CAPTION_ACTION);
}
diff --git a/src/com/android/tv/modules/TvApplicationModule.java b/src/com/android/tv/modules/TvApplicationModule.java
new file mode 100644
index 00000000..45383ae1
--- /dev/null
+++ b/src/com/android/tv/modules/TvApplicationModule.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.modules;
+
+import android.content.Context;
+import com.android.tv.MainActivity;
+import com.android.tv.TvApplication;
+import com.android.tv.common.concurrent.NamedThreadFactory;
+import com.android.tv.common.dagger.ApplicationModule;
+import com.android.tv.common.dagger.annotations.ApplicationContext;
+import com.android.tv.onboarding.OnboardingActivity;
+import com.android.tv.util.AsyncDbTask;
+import com.android.tv.util.TvInputManagerHelper;
+import dagger.Module;
+import dagger.Provides;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import javax.inject.Singleton;
+
+/** Dagger module for {@link TvApplication}. */
+@Module(
+ includes = {
+ ApplicationModule.class,
+ TvSingletonsModule.class,
+ MainActivity.Module.class,
+ OnboardingActivity.Module.class
+ })
+public class TvApplicationModule {
+ private static final NamedThreadFactory THREAD_FACTORY = new NamedThreadFactory("tv-app-db");
+
+ @Provides
+ @AsyncDbTask.DbExecutor
+ @Singleton
+ Executor providesDbExecutor() {
+ return Executors.newSingleThreadExecutor(THREAD_FACTORY);
+ }
+
+ @Provides
+ @Singleton
+ TvInputManagerHelper providesTvInputManagerHelper(@ApplicationContext Context context) {
+ TvInputManagerHelper tvInputManagerHelper = new TvInputManagerHelper(context);
+ tvInputManagerHelper.start();
+ return tvInputManagerHelper;
+ }
+}
diff --git a/src/com/android/tv/modules/TvSingletonsModule.java b/src/com/android/tv/modules/TvSingletonsModule.java
new file mode 100644
index 00000000..f998c08b
--- /dev/null
+++ b/src/com/android/tv/modules/TvSingletonsModule.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.tv.modules;
+
+import com.android.tv.TvSingletons;
+import com.android.tv.data.ChannelDataManager;
+import com.android.tv.data.ProgramDataManager;
+import dagger.Module;
+import dagger.Provides;
+
+/**
+ * Provides bindings for items provided by {@link TvSingletons}.
+ *
+ * <p>Use this module to inject items directly instead of using {@code TvSingletons}.
+ */
+@Module
+@SuppressWarnings("deprecation")
+public class TvSingletonsModule {
+ private final TvSingletons mTvSingletons;
+
+ public TvSingletonsModule(TvSingletons mTvSingletons) {
+ this.mTvSingletons = mTvSingletons;
+ }
+
+ @Provides
+ ChannelDataManager providesChannelDataManager() {
+ return mTvSingletons.getChannelDataManager();
+ }
+
+ @Provides
+ ProgramDataManager providesProgramDataManager() {
+ return mTvSingletons.getProgramDataManager();
+ }
+}
diff --git a/src/com/android/tv/onboarding/OnboardingActivity.java b/src/com/android/tv/onboarding/OnboardingActivity.java
index a1cf9de1..776ae664 100644
--- a/src/com/android/tv/onboarding/OnboardingActivity.java
+++ b/src/com/android/tv/onboarding/OnboardingActivity.java
@@ -37,6 +37,9 @@ import com.android.tv.data.ChannelDataManager;
import com.android.tv.util.OnboardingUtils;
import com.android.tv.util.SetupUtils;
import com.android.tv.util.TvInputManagerHelper;
+import dagger.android.AndroidInjection;
+import dagger.android.ContributesAndroidInjector;
+import javax.inject.Inject;
public class OnboardingActivity extends SetupActivity {
private static final String KEY_INTENT_AFTER_COMPLETION = "key_intent_after_completion";
@@ -47,9 +50,9 @@ public class OnboardingActivity extends SetupActivity {
private static final int REQUEST_CODE_START_SETUP_ACTIVITY = 1;
- private ChannelDataManager mChannelDataManager;
+ @Inject ChannelDataManager mChannelDataManager;
private TvInputManagerHelper mInputManager;
- private SetupUtils mSetupUtils;
+ @Inject SetupUtils mSetupUtils;
private final ChannelDataManager.Listener mChannelListener =
new ChannelDataManager.Listener() {
@Override
@@ -80,12 +83,11 @@ public class OnboardingActivity extends SetupActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
+ AndroidInjection.inject(this);
super.onCreate(savedInstanceState);
TvSingletons singletons = TvSingletons.getSingletons(this);
mInputManager = singletons.getTvInputManagerHelper();
- mSetupUtils = singletons.getSetupUtils();
if (PermissionUtils.hasAccessAllEpg(this) || PermissionUtils.hasReadTvListings(this)) {
- mChannelDataManager = singletons.getChannelDataManager();
// Make the channels of the new inputs which have been setup outside Live TV
// browsable.
if (mChannelDataManager.isDbLoadFinished()) {
@@ -148,13 +150,7 @@ public class OnboardingActivity extends SetupActivity {
private void showMerchantCollection() {
executeActionWithDelay(
- new Runnable() {
- @Override
- public void run() {
- startActivity(OnboardingUtils.ONLINE_STORE_INTENT);
- }
- },
- SHOW_RIPPLE_DURATION_MS);
+ () -> startActivity(OnboardingUtils.ONLINE_STORE_INTENT), SHOW_RIPPLE_DURATION_MS);
}
@Override
@@ -228,4 +224,11 @@ public class OnboardingActivity extends SetupActivity {
}
return false;
}
+
+ /** Exports {@link OnboardingActivity} for Dagger codegen to create the appropriate injector. */
+ @dagger.Module
+ public abstract static class Module {
+ @ContributesAndroidInjector
+ abstract OnboardingActivity contributeOnboardingActivityInjector();
+ }
}
diff --git a/src/com/android/tv/onboarding/SetupSourcesFragment.java b/src/com/android/tv/onboarding/SetupSourcesFragment.java
index f032f622..3566c9c3 100644
--- a/src/com/android/tv/onboarding/SetupSourcesFragment.java
+++ b/src/com/android/tv/onboarding/SetupSourcesFragment.java
@@ -197,9 +197,13 @@ public class SetupSourcesFragment extends SetupMultiPaneFragment {
mChannelDataManager.addListener(mChannelDataManagerListener);
super.onCreate(savedInstanceState);
mParentFragment = (SetupSourcesFragment) getParentFragment();
- singletons
- .getTunerInputController()
- .executeNetworkTunerDiscoveryAsyncTask(getContext());
+ if (singletons.getBuiltInTunerManager().isPresent()) {
+ singletons
+ .getBuiltInTunerManager()
+ .get()
+ .getTunerInputController()
+ .executeNetworkTunerDiscoveryAsyncTask(getContext());
+ }
}
@Override
@@ -332,7 +336,7 @@ public class SetupSourcesFragment extends SetupMultiPaneFragment {
.id(ACTION_ONLINE_STORE)
.title(getString(R.string.setup_store_action_title))
.description(getString(R.string.setup_store_action_description))
- .icon(R.drawable.ic_store)
+ .icon(R.drawable.ic_app_store)
.build());
if (newPosition != -1) {
diff --git a/src/com/android/tv/parental/ContentRatingSystem.java b/src/com/android/tv/parental/ContentRatingSystem.java
index 600aaca1..d85dd50e 100644
--- a/src/com/android/tv/parental/ContentRatingSystem.java
+++ b/src/com/android/tv/parental/ContentRatingSystem.java
@@ -31,13 +31,10 @@ public class ContentRatingSystem {
* A comparator that implements the display order of a group of content rating systems.
*/
public static final Comparator<ContentRatingSystem> DISPLAY_NAME_COMPARATOR =
- new Comparator<ContentRatingSystem>() {
- @Override
- public int compare(ContentRatingSystem s1, ContentRatingSystem s2) {
- String name1 = s1.getDisplayName();
- String name2 = s2.getDisplayName();
- return name1.compareTo(name2);
- }
+ (ContentRatingSystem s1, ContentRatingSystem s2) -> {
+ String name1 = s1.getDisplayName();
+ String name2 = s2.getDisplayName();
+ return name1.compareTo(name2);
};
private static final String DELIMITER = "/";
diff --git a/src/com/android/tv/parental/ParentalControlSettings.java b/src/com/android/tv/parental/ParentalControlSettings.java
index db1f0a4d..b41b160e 100644
--- a/src/com/android/tv/parental/ParentalControlSettings.java
+++ b/src/com/android/tv/parental/ParentalControlSettings.java
@@ -24,6 +24,7 @@ import com.android.tv.parental.ContentRatingSystem.Rating;
import com.android.tv.parental.ContentRatingSystem.SubRating;
import com.android.tv.util.TvSettings;
import com.android.tv.util.TvSettings.ContentRatingLevel;
+import com.google.common.collect.ImmutableList;
import java.util.HashSet;
import java.util.Set;
@@ -160,6 +161,26 @@ public class ParentalControlSettings {
}
/**
+ * Checks whether any of given ratings is blocked and returns the first blocked rating.
+ *
+ * @param ratings The array of ratings to check
+ * @return The {@link TvContentRating} that is blocked.
+ */
+ public TvContentRating getBlockedRating(ImmutableList<TvContentRating> ratings) {
+ if (ratings == null || ratings.isEmpty()) {
+ return mTvInputManager.isRatingBlocked(TvContentRating.UNRATED)
+ ? TvContentRating.UNRATED
+ : null;
+ }
+ for (TvContentRating rating : ratings) {
+ if (mTvInputManager.isRatingBlocked(rating)) {
+ return rating;
+ }
+ }
+ return null;
+ }
+
+ /**
* Sets the blocked status of a given content rating.
*
* <p>Note that a call to this method automatically changes the current rating level to {@code
@@ -178,34 +199,14 @@ public class ParentalControlSettings {
/**
* Checks whether any of given ratings is blocked.
*
- * @param ratings The array of ratings to check
+ * @param ratings The list of ratings to check
* @return {@code true} if a rating is blocked, {@code false} otherwise.
*/
- public boolean isRatingBlocked(TvContentRating[] ratings) {
+ public boolean isRatingBlocked(ImmutableList<TvContentRating> ratings) {
return getBlockedRating(ratings) != null;
}
/**
- * Checks whether any of given ratings is blocked and returns the first blocked rating.
- *
- * @param ratings The array of ratings to check
- * @return The {@link TvContentRating} that is blocked.
- */
- public TvContentRating getBlockedRating(TvContentRating[] ratings) {
- if (ratings == null || ratings.length <= 0) {
- return mTvInputManager.isRatingBlocked(TvContentRating.UNRATED)
- ? TvContentRating.UNRATED
- : null;
- }
- for (TvContentRating rating : ratings) {
- if (mTvInputManager.isRatingBlocked(rating)) {
- return rating;
- }
- }
- return null;
- }
-
- /**
* Checks whether a given rating is blocked by the user or not.
*
* @param contentRatingSystem The content rating system where the given rating belongs.
diff --git a/src/com/android/tv/perf/EventNames.java b/src/com/android/tv/perf/EventNames.java
index 54745f3b..4d21d6d8 100644
--- a/src/com/android/tv/perf/EventNames.java
+++ b/src/com/android/tv/perf/EventNames.java
@@ -25,31 +25,39 @@ import java.lang.annotation.Retention;
* Constants for performance event names.
*
* <p>Only constants are used to insure no PII is sent.
- *
+
*/
public final class EventNames {
@Retention(SOURCE)
@StringDef({
- APPLICATION_ONCREATE,
FETCH_EPG_TASK,
- MAIN_ACTIVITY_ONCREATE,
- MAIN_ACTIVITY_ONSTART,
- MAIN_ACTIVITY_ONRESUME,
- ON_DEVICE_SEARCH
+ ON_DEVICE_SEARCH,
+ PROGRAM_GUIDE_SHOW,
+ PROGRAM_DATA_MANAGER_PROGRAMS_PREFETCH_TASK_DO_IN_BACKGROUND,
+ PROGRAM_GUIDE_SHOW_FROM_EMPTY_CACHE,
+ PROGRAM_GUIDE_SCROLL_HORIZONTALLY,
+ PROGRAM_GUIDE_SCROLL_VERTICALLY,
+ MEMORY_ON_PROGRAM_GUIDE_CLOSE
})
public @interface EventName {}
- public static final String APPLICATION_ONCREATE = "Application.onCreate";
public static final String FETCH_EPG_TASK = "FetchEpgTask";
- public static final String MAIN_ACTIVITY_ONCREATE = "MainActivity.onCreate";
- public static final String MAIN_ACTIVITY_ONSTART = "MainActivity.onStart";
- public static final String MAIN_ACTIVITY_ONRESUME = "MainActivity.onResume";
/**
* Event name for query running time of on-device search in {@link
* com.android.tv.search.LocalSearchProvider}.
*/
public static final String ON_DEVICE_SEARCH = "OnDeviceSearch";
+ public static final String PROGRAM_GUIDE_SHOW = "ProgramGuide.show";
+ public static final String PROGRAM_DATA_MANAGER_PROGRAMS_PREFETCH_TASK_DO_IN_BACKGROUND =
+ "ProgramDataManager.ProgramsPrefetchTask.doInBackground";
+ public static final String PROGRAM_GUIDE_SHOW_FROM_EMPTY_CACHE =
+ "ProgramGuide.show.fromEmptyCache";
+ public static final String PROGRAM_GUIDE_SCROLL_HORIZONTALLY =
+ "ProgramGuide.scroll.horizontally";
+ public static final String PROGRAM_GUIDE_SCROLL_VERTICALLY = "ProgramGuide.scroll.vertically";
+ public static final String MEMORY_ON_PROGRAM_GUIDE_CLOSE = "ProgramGuide.memory.close";
+
private EventNames() {}
}
diff --git a/src/com/android/tv/perf/PerformanceMonitor.java b/src/com/android/tv/perf/PerformanceMonitor.java
index 111aa851..b1ae759d 100644
--- a/src/com/android/tv/perf/PerformanceMonitor.java
+++ b/src/com/android/tv/perf/PerformanceMonitor.java
@@ -19,6 +19,7 @@ package com.android.tv.perf;
import static com.android.tv.perf.EventNames.EventName;
import android.content.Context;
+import com.google.errorprone.annotations.CompileTimeConstant;
/** Measures Performance. */
public interface PerformanceMonitor {
@@ -34,7 +35,7 @@ public interface PerformanceMonitor {
*
* @param eventName to record
*/
- void recordMemory(@EventName String eventName);
+ void recordMemory(@EventName @CompileTimeConstant String eventName);
/**
* Starts a timer for a global event to allow measuring the event's latency across activities If
@@ -42,7 +43,7 @@ public interface PerformanceMonitor {
*
* @param eventName for which the timer starts
*/
- void startGlobalTimer(@EventName String eventName);
+ void startGlobalTimer(@EventName @CompileTimeConstant String eventName);
/**
* Stops a cross activities timer for a specific eventName and records the timer duration. If no
@@ -50,7 +51,7 @@ public interface PerformanceMonitor {
*
* @param eventName for which the timer stops
*/
- void stopGlobalTimer(@EventName String eventName);
+ void stopGlobalTimer(@EventName @CompileTimeConstant String eventName);
/**
* Starts a timer to record latency of a specific scenario or event. Use this method to track
@@ -69,7 +70,7 @@ public interface PerformanceMonitor {
* @param event that needs to be stopped
* @param eventName for which the timer stops. This must be constant with no PII.
*/
- void stopTimer(TimerEvent event, @EventName String eventName);
+ void stopTimer(TimerEvent event, @EventName @CompileTimeConstant String eventName);
/**
* Starts recording jank for a specific scenario or event.
@@ -79,14 +80,14 @@ public interface PerformanceMonitor {
*
* @param eventName of the event for which tracking is started
*/
- void startJankRecorder(@EventName String eventName);
+ void startJankRecorder(@EventName @CompileTimeConstant String eventName);
/**
* Stops recording jank for a specific event and records the jank event.
*
* @param eventName of the event that needs to be stopped
*/
- void stopJankRecorder(@EventName String eventName);
+ void stopJankRecorder(@EventName @CompileTimeConstant String eventName);
/**
* Starts activity to display PerformanceMonitor events recorded in local database for debug
diff --git a/src/com/android/tv/perf/PerformanceMonitorManager.java b/src/com/android/tv/perf/PerformanceMonitorManager.java
new file mode 100644
index 00000000..db6667d1
--- /dev/null
+++ b/src/com/android/tv/perf/PerformanceMonitorManager.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.perf;
+
+import android.app.Application;
+
+/** Manages the initialization of Performance Monitoring. */
+public interface PerformanceMonitorManager {
+
+ /**
+ * Initializes the {@link com.android.tv.perf.PerformanceMonitor}.
+ *
+ * <p>This should only be called once.
+ */
+ PerformanceMonitor initialize(Application app);
+
+ /**
+ * Returns a lightweight object to help measure both cold and warm startup latency.
+ *
+ * <p>This method is idempotent and lightweight. It can be called multiple times and does not
+ * need to be cached.
+ */
+ StartupMeasure getStartupMeasure();
+}
diff --git a/src/com/android/tv/perf/PerformanceMonitorManagerFactory.java b/src/com/android/tv/perf/PerformanceMonitorManagerFactory.java
new file mode 100644
index 00000000..fe3ea14b
--- /dev/null
+++ b/src/com/android/tv/perf/PerformanceMonitorManagerFactory.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.perf;
+
+import com.android.tv.perf.stub.StubPerformanceMonitorManager;
+import javax.inject.Inject;
+
+public final class PerformanceMonitorManagerFactory {
+ private static final PerformanceMonitorManagerFactory INSTANCE =
+ new PerformanceMonitorManagerFactory();
+
+ @Inject
+ public PerformanceMonitorManagerFactory() {}
+
+ public static PerformanceMonitorManager create() {
+ return INSTANCE.get();
+ }
+
+ public PerformanceMonitorManager get() {
+ return new StubPerformanceMonitorManager();
+ }
+}
diff --git a/src/com/android/tv/perf/StartupMeasure.java b/src/com/android/tv/perf/StartupMeasure.java
new file mode 100644
index 00000000..5cf183ca
--- /dev/null
+++ b/src/com/android/tv/perf/StartupMeasure.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.perf;
+
+import android.app.Activity;
+import android.app.Application;
+
+/**
+ * Measures App startup. This interface is lightweight to help measure both cold and warm startup
+ * latency. Implementations must not throw any Exception.
+ */
+public interface StartupMeasure {
+
+ /** To be be placed as the first static block in the app's Application class. */
+ void onAppClassLoaded();
+
+ /**
+ * To be placed in your {@link Application#onCreate} to let Performance Monitoring know when
+ * this happen.
+ */
+ void onAppCreate(Application application);
+
+ /**
+ * To be placed in an initialization block of your {@link Activity} to let Performance
+ * Monitoring know when this activity is instantiated. Please note that this initialization
+ * block should be before other initialization blocks (if any) in your activity class.
+ */
+ void onActivityInit();
+}
diff --git a/src/com/android/tv/perf/StubPerformanceMonitor.java b/src/com/android/tv/perf/stub/StubPerformanceMonitor.java
index 3742a2a7..80c2f6c5 100644
--- a/src/com/android/tv/perf/StubPerformanceMonitor.java
+++ b/src/com/android/tv/perf/stub/StubPerformanceMonitor.java
@@ -14,20 +14,17 @@
* limitations under the License.
*/
-package com.android.tv.perf;
+package com.android.tv.perf.stub;
-import android.app.Application;
import android.content.Context;
+import com.android.tv.perf.PerformanceMonitor;
+import com.android.tv.perf.TimerEvent;
/** Do nothing implementation of {@link PerformanceMonitor}. */
public final class StubPerformanceMonitor implements PerformanceMonitor {
private static final TimerEvent TIMER_EVENT = new TimerEvent() {};
- public static PerformanceMonitor initialize(Application app) {
- return new StubPerformanceMonitor();
- }
-
@Override
public void startMemoryMonitor() {}
diff --git a/src/com/android/tv/perf/stub/StubPerformanceMonitorManager.java b/src/com/android/tv/perf/stub/StubPerformanceMonitorManager.java
new file mode 100644
index 00000000..0c268155
--- /dev/null
+++ b/src/com/android/tv/perf/stub/StubPerformanceMonitorManager.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.perf.stub;
+
+import android.app.Application;
+import com.android.tv.perf.PerformanceMonitor;
+import com.android.tv.perf.PerformanceMonitorManager;
+import com.android.tv.perf.StartupMeasure;
+
+/** Manages a stub implementation of Performance Monitoring. */
+public class StubPerformanceMonitorManager implements PerformanceMonitorManager {
+
+ @Override
+ public PerformanceMonitor initialize(Application app) {
+ return new StubPerformanceMonitor();
+ }
+
+ @Override
+ public StartupMeasure getStartupMeasure() {
+ return new StubStartupMeasure();
+ }
+}
diff --git a/src/com/android/tv/perf/stub/StubStartupMeasure.java b/src/com/android/tv/perf/stub/StubStartupMeasure.java
new file mode 100644
index 00000000..d4412261
--- /dev/null
+++ b/src/com/android/tv/perf/stub/StubStartupMeasure.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.perf.stub;
+
+import android.app.Application;
+import com.android.tv.perf.StartupMeasure;
+
+/** Stub implementation of {@link StartupMeasure} */
+public class StubStartupMeasure implements StartupMeasure {
+
+ @Override
+ public void onAppClassLoaded() {}
+
+ @Override
+ public void onAppCreate(Application application) {}
+
+ @Override
+ public void onActivityInit() {}
+}
diff --git a/src/com/android/tv/receiver/BootCompletedReceiver.java b/src/com/android/tv/receiver/BootCompletedReceiver.java
index d8528bb5..0eb03bec 100644
--- a/src/com/android/tv/receiver/BootCompletedReceiver.java
+++ b/src/com/android/tv/receiver/BootCompletedReceiver.java
@@ -25,10 +25,10 @@ import android.os.Build;
import android.util.Log;
import com.android.tv.Starter;
import com.android.tv.TvActivity;
-import com.android.tv.TvFeatures;
import com.android.tv.TvSingletons;
import com.android.tv.dvr.recorder.DvrRecordingService;
import com.android.tv.dvr.recorder.RecordingScheduler;
+import com.android.tv.features.TvFeatures;
import com.android.tv.recommendation.ChannelPreviewUpdater;
import com.android.tv.recommendation.NotificationService;
import com.android.tv.util.OnboardingUtils;
@@ -70,7 +70,7 @@ public class BootCompletedReceiver extends BroadcastReceiver {
// Grant permission to already set up packages after the system has finished booting.
SetupUtils.grantEpgPermissionToSetUpPackages(context);
- if (TvFeatures.UNHIDE.isEnabled(context)) {
+ if (TvFeatures.UNHIDE.isEnabled(context.getApplicationContext())) {
if (OnboardingUtils.isFirstBoot(context)) {
// Enable the application if this is the first "unhide" feature is enabled just in
// case when the app has been disabled before.
diff --git a/src/com/android/tv/receiver/PackageIntentsReceiver.java b/src/com/android/tv/receiver/PackageIntentsReceiver.java
index 07f5d6be..5bc6d724 100644
--- a/src/com/android/tv/receiver/PackageIntentsReceiver.java
+++ b/src/com/android/tv/receiver/PackageIntentsReceiver.java
@@ -22,8 +22,8 @@ import android.content.Intent;
import android.net.Uri;
import android.util.Log;
import com.android.tv.Starter;
-import com.android.tv.TvFeatures;
import com.android.tv.TvSingletons;
+import com.android.tv.features.TvFeatures;
import com.android.tv.util.Partner;
import com.google.android.tv.partner.support.EpgContract;
diff --git a/src/com/android/tv/recommendation/ChannelPreviewUpdater.java b/src/com/android/tv/recommendation/ChannelPreviewUpdater.java
index 410b8252..2590a337 100644
--- a/src/com/android/tv/recommendation/ChannelPreviewUpdater.java
+++ b/src/com/android/tv/recommendation/ChannelPreviewUpdater.java
@@ -25,9 +25,9 @@ import android.content.Context;
import android.os.AsyncTask;
import android.os.Build;
import android.support.annotation.RequiresApi;
-import android.support.media.tv.TvContractCompat;
import android.text.TextUtils;
import android.util.Log;
+import androidx.tvprovider.media.tv.TvContractCompat;
import com.android.tv.Starter;
import com.android.tv.TvSingletons;
import com.android.tv.data.PreviewDataManager;
@@ -169,18 +169,23 @@ public class ChannelPreviewUpdater {
@Override
protected Set<Program> doInBackground(Void... params) {
Set<Program> programs = new HashSet<>();
- List<Channel> channels = new ArrayList<>(mRecommender.recommendChannels());
- for (Channel channel : channels) {
- if (channel.isPhysicalTunerChannel()) {
- final Program program = Utils.getCurrentProgram(mContext, channel.getId());
- if (program != null
- && isChannelRecommendationApplicable(channel, program)) {
- programs.add(program);
- if (programs.size() >= RECOMMENDATION_COUNT) {
- break;
+ try {
+ List<Channel> channels = new ArrayList<>(mRecommender.recommendChannels());
+ for (Channel channel : channels) {
+ if (channel.isPhysicalTunerChannel()) {
+ final Program program =
+ Utils.getCurrentProgram(mContext, channel.getId());
+ if (program != null
+ && isChannelRecommendationApplicable(channel, program)) {
+ programs.add(program);
+ if (programs.size() >= RECOMMENDATION_COUNT) {
+ break;
+ }
}
}
}
+ } catch (Exception e) {
+ Log.w(TAG, "Can't update preview data", e);
}
return programs;
}
@@ -241,6 +246,17 @@ public class ChannelPreviewUpdater {
}
}
});
+ } else if (mJobService != null && mJobParams != null) {
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "Preview channel not created because there is only "
+ + programs.size()
+ + " programs");
+ }
+ mJobService.jobFinished(mJobParams, false);
+ mJobService = null;
+ mJobParams = null;
}
} else {
updatePreviewProgramsForPreviewChannel(
diff --git a/src/com/android/tv/recommendation/RecommendationDataManager.java b/src/com/android/tv/recommendation/RecommendationDataManager.java
index 649920fb..fc20031c 100644
--- a/src/com/android/tv/recommendation/RecommendationDataManager.java
+++ b/src/com/android/tv/recommendation/RecommendationDataManager.java
@@ -33,6 +33,7 @@ import android.support.annotation.MainThread;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.WorkerThread;
+import android.util.Log;
import com.android.tv.TvSingletons;
import com.android.tv.common.WeakHandler;
import com.android.tv.common.util.PermissionUtils;
@@ -52,6 +53,7 @@ import java.util.concurrent.ConcurrentHashMap;
/** Manages teh data need to make recommendations. */
public class RecommendationDataManager implements WatchedHistoryManager.Listener {
+ private static final String TAG = "RecommendationDataManag";
private static final int MSG_START = 1000;
private static final int MSG_STOP = 1001;
private static final int MSG_UPDATE_CHANNELS = 1002;
@@ -187,13 +189,7 @@ public class RecommendationDataManager implements WatchedHistoryManager.Listener
mMainHandler = new RecommendationMainHandler(Looper.getMainLooper(), this);
mContentObserver = new RecommendationContentObserver(mHandler);
mChannelDataManager = TvSingletons.getSingletons(mContext).getChannelDataManager();
- runOnMainThread(
- new Runnable() {
- @Override
- public void run() {
- start();
- }
- });
+ runOnMainThread(this::start);
}
/**
@@ -202,13 +198,10 @@ public class RecommendationDataManager implements WatchedHistoryManager.Listener
*/
public void release(@NonNull final Listener listener) {
runOnMainThread(
- new Runnable() {
- @Override
- public void run() {
- removeListener(listener);
- if (mListeners.size() == 0) {
- stop();
- }
+ () -> {
+ removeListener(listener);
+ if (mListeners.size() == 0) {
+ stop();
}
});
}
@@ -257,13 +250,7 @@ public class RecommendationDataManager implements WatchedHistoryManager.Listener
}
private void addListener(Listener listener) {
- runOnMainThread(
- new Runnable() {
- @Override
- public void run() {
- mListeners.add(listener);
- }
- });
+ runOnMainThread(() -> mListeners.add(listener));
}
@MainThread
@@ -347,18 +334,18 @@ public class RecommendationDataManager implements WatchedHistoryManager.Listener
history.add(createWatchedProgramFromWatchedProgramCursor(cursor));
} while (cursor.moveToPrevious());
}
+ } catch (Exception e) {
+ Log.e(TAG, "Error trying to load watch history from " + uri, e);
+ return;
}
for (WatchedProgram watchedProgram : history) {
final ChannelRecord channelRecord =
updateChannelRecordFromWatchedProgram(watchedProgram);
if (mChannelRecordMapLoaded && channelRecord != null) {
runOnMainThread(
- new Runnable() {
- @Override
- public void run() {
- for (Listener l : mListeners) {
- l.onNewWatchLog(channelRecord);
- }
+ () -> {
+ for (Listener l : mListeners) {
+ l.onNewWatchLog(channelRecord);
}
});
}
@@ -397,12 +384,9 @@ public class RecommendationDataManager implements WatchedHistoryManager.Listener
convertFromWatchedHistoryManagerRecords(watchedRecord));
if (mChannelRecordMapLoaded && channelRecord != null) {
runOnMainThread(
- new Runnable() {
- @Override
- public void run() {
- for (Listener l : mListeners) {
- l.onNewWatchLog(channelRecord);
- }
+ () -> {
+ for (Listener l : mListeners) {
+ l.onNewWatchLog(channelRecord);
}
});
}
@@ -441,24 +425,18 @@ public class RecommendationDataManager implements WatchedHistoryManager.Listener
private void onNotifyChannelRecordMapLoaded() {
mChannelRecordMapLoaded = true;
runOnMainThread(
- new Runnable() {
- @Override
- public void run() {
- for (Listener l : mListeners) {
- l.onChannelRecordLoaded();
- }
+ () -> {
+ for (Listener l : mListeners) {
+ l.onChannelRecordLoaded();
}
});
}
private void onNotifyChannelRecordMapChanged() {
runOnMainThread(
- new Runnable() {
- @Override
- public void run() {
- for (Listener l : mListeners) {
- l.onChannelRecordChanged();
- }
+ () -> {
+ for (Listener l : mListeners) {
+ l.onChannelRecordChanged();
}
});
}
diff --git a/src/com/android/tv/search/AutoValue_LocalSearchProvider_SearchResult.java b/src/com/android/tv/search/AutoValue_LocalSearchProvider_SearchResult.java
deleted file mode 100644
index 528096dd..00000000
--- a/src/com/android/tv/search/AutoValue_LocalSearchProvider_SearchResult.java
+++ /dev/null
@@ -1,363 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-
-
-package com.android.tv.search;
-
-import android.support.annotation.Nullable;
-
-/**
- * Hand copy of generated Autovalue class.
- *
- * TODO get autovalue working
- */
-
-final class AutoValue_LocalSearchProvider_SearchResult extends LocalSearchProvider.SearchResult {
-
- private final long channelId;
- private final String channelNumber;
- private final String title;
- private final String description;
- private final String imageUri;
- private final String intentAction;
- private final String intentData;
- private final String intentExtraData;
- private final String contentType;
- private final boolean isLive;
- private final int videoWidth;
- private final int videoHeight;
- private final long duration;
- private final int progressPercentage;
-
- private AutoValue_LocalSearchProvider_SearchResult(
- long channelId,
- @Nullable String channelNumber,
- @Nullable String title,
- @Nullable String description,
- @Nullable String imageUri,
- @Nullable String intentAction,
- @Nullable String intentData,
- @Nullable String intentExtraData,
- @Nullable String contentType,
- boolean isLive,
- int videoWidth,
- int videoHeight,
- long duration,
- int progressPercentage) {
- this.channelId = channelId;
- this.channelNumber = channelNumber;
- this.title = title;
- this.description = description;
- this.imageUri = imageUri;
- this.intentAction = intentAction;
- this.intentData = intentData;
- this.intentExtraData = intentExtraData;
- this.contentType = contentType;
- this.isLive = isLive;
- this.videoWidth = videoWidth;
- this.videoHeight = videoHeight;
- this.duration = duration;
- this.progressPercentage = progressPercentage;
- }
-
- @Override
- long getChannelId() {
- return channelId;
- }
-
- @Nullable
- @Override
- String getChannelNumber() {
- return channelNumber;
- }
-
- @Nullable
- @Override
- String getTitle() {
- return title;
- }
-
- @Nullable
- @Override
- String getDescription() {
- return description;
- }
-
- @Nullable
- @Override
- String getImageUri() {
- return imageUri;
- }
-
- @Nullable
- @Override
- String getIntentAction() {
- return intentAction;
- }
-
- @Nullable
- @Override
- String getIntentData() {
- return intentData;
- }
-
- @Nullable
- @Override
- String getIntentExtraData() {
- return intentExtraData;
- }
-
- @Nullable
- @Override
- String getContentType() {
- return contentType;
- }
-
- @Override
- boolean getIsLive() {
- return isLive;
- }
-
- @Override
- int getVideoWidth() {
- return videoWidth;
- }
-
- @Override
- int getVideoHeight() {
- return videoHeight;
- }
-
- @Override
- long getDuration() {
- return duration;
- }
-
- @Override
- int getProgressPercentage() {
- return progressPercentage;
- }
-
- @Override
- public String toString() {
- return "SearchResult{"
- + "channelId=" + channelId + ", "
- + "channelNumber=" + channelNumber + ", "
- + "title=" + title + ", "
- + "description=" + description + ", "
- + "imageUri=" + imageUri + ", "
- + "intentAction=" + intentAction + ", "
- + "intentData=" + intentData + ", "
- + "intentExtraData=" + intentExtraData + ", "
- + "contentType=" + contentType + ", "
- + "isLive=" + isLive + ", "
- + "videoWidth=" + videoWidth + ", "
- + "videoHeight=" + videoHeight + ", "
- + "duration=" + duration + ", "
- + "progressPercentage=" + progressPercentage
- + "}";
- }
-
- @Override
- public boolean equals(Object o) {
- if (o == this) {
- return true;
- }
- if (o instanceof LocalSearchProvider.SearchResult) {
- LocalSearchProvider.SearchResult that = (LocalSearchProvider.SearchResult) o;
- return (this.channelId == that.getChannelId())
- && ((this.channelNumber == null) ? (that.getChannelNumber() == null) : this.channelNumber.equals(that.getChannelNumber()))
- && ((this.title == null) ? (that.getTitle() == null) : this.title.equals(that.getTitle()))
- && ((this.description == null) ? (that.getDescription() == null) : this.description.equals(that.getDescription()))
- && ((this.imageUri == null) ? (that.getImageUri() == null) : this.imageUri.equals(that.getImageUri()))
- && ((this.intentAction == null) ? (that.getIntentAction() == null) : this.intentAction.equals(that.getIntentAction()))
- && ((this.intentData == null) ? (that.getIntentData() == null) : this.intentData.equals(that.getIntentData()))
- && ((this.intentExtraData == null) ? (that.getIntentExtraData() == null) : this.intentExtraData.equals(that.getIntentExtraData()))
- && ((this.contentType == null) ? (that.getContentType() == null) : this.contentType.equals(that.getContentType()))
- && (this.isLive == that.getIsLive())
- && (this.videoWidth == that.getVideoWidth())
- && (this.videoHeight == that.getVideoHeight())
- && (this.duration == that.getDuration())
- && (this.progressPercentage == that.getProgressPercentage());
- }
- return false;
- }
-
- @Override
- public int hashCode() {
- int h$ = 1;
- h$ *= 1000003;
- h$ ^= (int) ((channelId >>> 32) ^ channelId);
- h$ *= 1000003;
- h$ ^= (channelNumber == null) ? 0 : channelNumber.hashCode();
- h$ *= 1000003;
- h$ ^= (title == null) ? 0 : title.hashCode();
- h$ *= 1000003;
- h$ ^= (description == null) ? 0 : description.hashCode();
- h$ *= 1000003;
- h$ ^= (imageUri == null) ? 0 : imageUri.hashCode();
- h$ *= 1000003;
- h$ ^= (intentAction == null) ? 0 : intentAction.hashCode();
- h$ *= 1000003;
- h$ ^= (intentData == null) ? 0 : intentData.hashCode();
- h$ *= 1000003;
- h$ ^= (intentExtraData == null) ? 0 : intentExtraData.hashCode();
- h$ *= 1000003;
- h$ ^= (contentType == null) ? 0 : contentType.hashCode();
- h$ *= 1000003;
- h$ ^= isLive ? 1231 : 1237;
- h$ *= 1000003;
- h$ ^= videoWidth;
- h$ *= 1000003;
- h$ ^= videoHeight;
- h$ *= 1000003;
- h$ ^= (int) ((duration >>> 32) ^ duration);
- h$ *= 1000003;
- h$ ^= progressPercentage;
- return h$;
- }
-
- static final class Builder extends LocalSearchProvider.SearchResult.Builder {
- private Long channelId;
- private String channelNumber;
- private String title;
- private String description;
- private String imageUri;
- private String intentAction;
- private String intentData;
- private String intentExtraData;
- private String contentType;
- private Boolean isLive;
- private Integer videoWidth;
- private Integer videoHeight;
- private Long duration;
- private Integer progressPercentage;
- Builder() {
- }
- @Override
- LocalSearchProvider.SearchResult.Builder setChannelId(long channelId) {
- this.channelId = channelId;
- return this;
- }
- @Override
- LocalSearchProvider.SearchResult.Builder setChannelNumber(@Nullable String channelNumber) {
- this.channelNumber = channelNumber;
- return this;
- }
- @Override
- LocalSearchProvider.SearchResult.Builder setTitle(@Nullable String title) {
- this.title = title;
- return this;
- }
- @Override
- LocalSearchProvider.SearchResult.Builder setDescription(@Nullable String description) {
- this.description = description;
- return this;
- }
- @Override
- LocalSearchProvider.SearchResult.Builder setImageUri(@Nullable String imageUri) {
- this.imageUri = imageUri;
- return this;
- }
- @Override
- LocalSearchProvider.SearchResult.Builder setIntentAction(@Nullable String intentAction) {
- this.intentAction = intentAction;
- return this;
- }
- @Override
- LocalSearchProvider.SearchResult.Builder setIntentData(@Nullable String intentData) {
- this.intentData = intentData;
- return this;
- }
- @Override
- LocalSearchProvider.SearchResult.Builder setIntentExtraData(@Nullable String intentExtraData) {
- this.intentExtraData = intentExtraData;
- return this;
- }
- @Override
- LocalSearchProvider.SearchResult.Builder setContentType(@Nullable String contentType) {
- this.contentType = contentType;
- return this;
- }
- @Override
- LocalSearchProvider.SearchResult.Builder setIsLive(boolean isLive) {
- this.isLive = isLive;
- return this;
- }
- @Override
- LocalSearchProvider.SearchResult.Builder setVideoWidth(int videoWidth) {
- this.videoWidth = videoWidth;
- return this;
- }
- @Override
- LocalSearchProvider.SearchResult.Builder setVideoHeight(int videoHeight) {
- this.videoHeight = videoHeight;
- return this;
- }
- @Override
- LocalSearchProvider.SearchResult.Builder setDuration(long duration) {
- this.duration = duration;
- return this;
- }
- @Override
- LocalSearchProvider.SearchResult.Builder setProgressPercentage(int progressPercentage) {
- this.progressPercentage = progressPercentage;
- return this;
- }
- @Override
- LocalSearchProvider.SearchResult build() {
- String missing = "";
- if (this.channelId == null) {
- missing += " channelId";
- }
- if (this.isLive == null) {
- missing += " isLive";
- }
- if (this.videoWidth == null) {
- missing += " videoWidth";
- }
- if (this.videoHeight == null) {
- missing += " videoHeight";
- }
- if (this.duration == null) {
- missing += " duration";
- }
- if (this.progressPercentage == null) {
- missing += " progressPercentage";
- }
- if (!missing.isEmpty()) {
- throw new IllegalStateException("Missing required properties:" + missing);
- }
- return new AutoValue_LocalSearchProvider_SearchResult(
- this.channelId,
- this.channelNumber,
- this.title,
- this.description,
- this.imageUri,
- this.intentAction,
- this.intentData,
- this.intentExtraData,
- this.contentType,
- this.isLive,
- this.videoWidth,
- this.videoHeight,
- this.duration,
- this.progressPercentage);
- }
- }
-
-}
diff --git a/src/com/android/tv/search/DataManagerSearch.java b/src/com/android/tv/search/DataManagerSearch.java
index 82fb5016..a649c0ac 100644
--- a/src/com/android/tv/search/DataManagerSearch.java
+++ b/src/com/android/tv/search/DataManagerSearch.java
@@ -34,12 +34,12 @@ import com.android.tv.data.api.Channel;
import com.android.tv.search.LocalSearchProvider.SearchResult;
import com.android.tv.util.MainThreadExecutor;
import com.android.tv.util.Utils;
+import com.google.common.collect.ImmutableList;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
-import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
@@ -68,13 +68,7 @@ public class DataManagerSearch implements SearchInterface {
public List<SearchResult> search(final String query, final int limit, final int action) {
Future<List<SearchResult>> future =
MainThreadExecutor.getInstance()
- .submit(
- new Callable<List<SearchResult>>() {
- @Override
- public List<SearchResult> call() throws Exception {
- return searchFromDataManagers(query, limit, action);
- }
- });
+ .submit(() -> searchFromDataManagers(query, limit, action));
try {
return future.get();
@@ -255,7 +249,7 @@ public class DataManagerSearch implements SearchInterface {
result.setIntentData(buildIntentData(channelId));
result.setContentType(Programs.CONTENT_ITEM_TYPE);
result.setIsLive(true);
- result.setProgressPercentage(LocalSearchProvider.PROGRESS_PERCENTAGE_HIDE);
+ result.setProgressPercentage(SearchInterface.PROGRESS_PERCENTAGE_HIDE);
} else {
result.setTitle(program.getTitle());
result.setDescription(
@@ -299,7 +293,7 @@ public class DataManagerSearch implements SearchInterface {
private int getProgressPercentage(long startUtcMillis, long endUtcMillis) {
long current = System.currentTimeMillis();
if (startUtcMillis > current || endUtcMillis <= current) {
- return LocalSearchProvider.PROGRESS_PERCENTAGE_HIDE;
+ return SearchInterface.PROGRESS_PERCENTAGE_HIDE;
}
return (int) (100 * (current - startUtcMillis) / (endUtcMillis - startUtcMillis));
}
@@ -308,10 +302,8 @@ public class DataManagerSearch implements SearchInterface {
return TvContract.buildChannelUri(channelId).toString();
}
- private boolean isRatingBlocked(TvContentRating[] ratings) {
- if (ratings == null
- || ratings.length == 0
- || !mTvInputManager.isParentalControlsEnabled()) {
+ private boolean isRatingBlocked(ImmutableList<TvContentRating> ratings) {
+ if (ratings == null || ratings.isEmpty() || !mTvInputManager.isParentalControlsEnabled()) {
return false;
}
for (TvContentRating rating : ratings) {
diff --git a/src/com/android/tv/search/LocalSearchProvider.java b/src/com/android/tv/search/LocalSearchProvider.java
index 97e7f229..5652c986 100644
--- a/src/com/android/tv/search/LocalSearchProvider.java
+++ b/src/com/android/tv/search/LocalSearchProvider.java
@@ -37,6 +37,7 @@ import com.android.tv.perf.EventNames;
import com.android.tv.perf.PerformanceMonitor;
import com.android.tv.perf.TimerEvent;
import com.android.tv.util.TvUriMatcher;
+import com.google.auto.value.AutoValue;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -48,8 +49,6 @@ public class LocalSearchProvider extends ContentProvider {
/** The authority for LocalSearchProvider. */
public static final String AUTHORITY = CommonConstants.BASE_PACKAGE + ".search";
- public static final int PROGRESS_PERCENTAGE_HIDE = -1;
-
// TODO: Remove this once added to the SearchManager.
private static final String SUGGEST_COLUMN_PROGRESS_BAR_PERCENTAGE = "progress_bar_percentage";
@@ -223,7 +222,7 @@ public class LocalSearchProvider extends ContentProvider {
}
/** A placeholder to a search result. */
- // TODO(b/72052568): Get autovalue to work in aosp master
+ @AutoValue
public abstract static class SearchResult {
public static Builder builder() {
// primitive fields cannot be nullable. Set to default;
@@ -236,7 +235,7 @@ public class LocalSearchProvider extends ContentProvider {
.setProgressPercentage(0);
}
- // TODO(b/72052568): Get autovalue to work in aosp master
+ @AutoValue.Builder
abstract static class Builder {
abstract Builder setChannelId(long value);
diff --git a/src/com/android/tv/search/ProgramGuideSearchFragment.java b/src/com/android/tv/search/ProgramGuideSearchFragment.java
index cb26252b..6c94bd33 100644
--- a/src/com/android/tv/search/ProgramGuideSearchFragment.java
+++ b/src/com/android/tv/search/ProgramGuideSearchFragment.java
@@ -84,7 +84,7 @@ public class ProgramGuideSearchFragment extends SearchFragment {
createImageLoaderCallback(cardView));
} else {
cardView.setMainImage(
- mMainActivity.getDrawable(R.drawable.ic_live_channels_96x96));
+ mMainActivity.getDrawable(R.drawable.ic_tv_app_96x96));
}
}
@@ -171,7 +171,7 @@ public class ProgramGuideSearchFragment extends SearchFragment {
View v = super.onCreateView(inflater, container, savedInstanceState);
v.setBackgroundResource(R.color.program_guide_scrim);
- setBadgeDrawable(mMainActivity.getDrawable(R.drawable.ic_live_channels_96x96));
+ setBadgeDrawable(mMainActivity.getDrawable(R.drawable.ic_tv_app_96x96));
setSearchResultProvider(mSearchResultProvider);
setOnItemViewClickedListener(mItemClickedListener);
return v;
diff --git a/src/com/android/tv/search/SearchInterface.java b/src/com/android/tv/search/SearchInterface.java
index 4866ee84..d16270ed 100644
--- a/src/com/android/tv/search/SearchInterface.java
+++ b/src/com/android/tv/search/SearchInterface.java
@@ -26,6 +26,7 @@ public interface SearchInterface {
int ACTION_TYPE_SWITCH_CHANNEL = 2;
int ACTION_TYPE_SWITCH_INPUT = 3;
int ACTION_TYPE_END = 3;
+ int PROGRESS_PERCENTAGE_HIDE = -1;
/**
* Search channels, inputs, or programs. This assumes that parental control settings will not be
diff --git a/src/com/android/tv/search/TvProviderSearch.java b/src/com/android/tv/search/TvProviderSearch.java
index 92197f2d..8a1f51f9 100644
--- a/src/com/android/tv/search/TvProviderSearch.java
+++ b/src/com/android/tv/search/TvProviderSearch.java
@@ -36,6 +36,7 @@ import com.android.tv.common.TvContentRatingCache;
import com.android.tv.common.util.PermissionUtils;
import com.android.tv.search.LocalSearchProvider.SearchResult;
import com.android.tv.util.Utils;
+import com.google.common.collect.ImmutableList;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
@@ -270,7 +271,7 @@ public class TvProviderSearch implements SearchInterface {
result.setIntentData(buildIntentData(id));
result.setContentType(Programs.CONTENT_ITEM_TYPE);
result.setIsLive(true);
- result.setProgressPercentage(LocalSearchProvider.PROGRESS_PERCENTAGE_HIDE);
+ result.setProgressPercentage(SearchInterface.PROGRESS_PERCENTAGE_HIDE);
searchResults.add(result.build());
@@ -343,7 +344,7 @@ public class TvProviderSearch implements SearchInterface {
private int getProgressPercentage(long startUtcMillis, long endUtcMillis) {
long current = System.currentTimeMillis();
if (startUtcMillis > current || endUtcMillis <= current) {
- return LocalSearchProvider.PROGRESS_PERCENTAGE_HIDE;
+ return SearchInterface.PROGRESS_PERCENTAGE_HIDE;
}
return (int) (100 * (current - startUtcMillis) / (endUtcMillis - startUtcMillis));
}
@@ -481,7 +482,7 @@ public class TvProviderSearch implements SearchInterface {
if (TextUtils.isEmpty(ratings) || !mTvInputManager.isParentalControlsEnabled()) {
return false;
}
- TvContentRating[] ratingArray = mTvContentRatingCache.getRatings(ratings);
+ ImmutableList<TvContentRating> ratingArray = mTvContentRatingCache.getRatings(ratings);
if (ratingArray != null) {
for (TvContentRating r : ratingArray) {
if (mTvInputManager.isRatingBlocked(r)) {
diff --git a/src/com/android/tv/setup/SystemSetupActivity.java b/src/com/android/tv/setup/SystemSetupActivity.java
index c6b10e52..b2160b3a 100644
--- a/src/com/android/tv/setup/SystemSetupActivity.java
+++ b/src/com/android/tv/setup/SystemSetupActivity.java
@@ -64,13 +64,7 @@ public class SystemSetupActivity extends SetupActivity {
private void showMerchantCollection() {
executeActionWithDelay(
- new Runnable() {
- @Override
- public void run() {
- startActivity(OnboardingUtils.ONLINE_STORE_INTENT);
- }
- },
- SHOW_RIPPLE_DURATION_MS);
+ () -> startActivity(OnboardingUtils.ONLINE_STORE_INTENT), SHOW_RIPPLE_DURATION_MS);
}
@Override
diff --git a/src/com/android/tv/tuner/TunerInputController.java b/src/com/android/tv/tuner/TunerInputController.java
deleted file mode 100644
index 02611bbf..00000000
--- a/src/com/android/tv/tuner/TunerInputController.java
+++ /dev/null
@@ -1,556 +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;
-
-import android.app.AlarmManager;
-import android.app.Notification;
-import android.app.NotificationChannel;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
-import android.content.BroadcastReceiver;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.content.pm.PackageManager;
-import android.content.pm.PackageManager.NameNotFoundException;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.hardware.usb.UsbDevice;
-import android.hardware.usb.UsbManager;
-import android.net.ConnectivityManager;
-import android.net.NetworkInfo;
-import android.net.Uri;
-import android.os.AsyncTask;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.Message;
-import android.os.SystemClock;
-import android.preference.PreferenceManager;
-import android.support.annotation.NonNull;
-import android.text.TextUtils;
-import android.util.Log;
-import android.widget.Toast;
-import com.android.tv.R;
-import com.android.tv.Starter;
-import com.android.tv.TvApplication;
-import com.android.tv.TvSingletons;
-import com.android.tv.common.BuildConfig;
-import com.android.tv.common.util.SystemPropertiesProxy;
-
-
-import com.android.tv.tuner.setup.BaseTunerSetupActivity;
-import com.android.tv.tuner.util.TunerInputInfoUtils;
-import java.text.ParseException;
-import java.text.SimpleDateFormat;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Controls the package visibility of {@link BaseTunerTvInputService}.
- *
- * <p>Listens to broadcast intent for {@link Intent#ACTION_BOOT_COMPLETED}, {@code
- * UsbManager.ACTION_USB_DEVICE_ATTACHED}, and {@code UsbManager.ACTION_USB_DEVICE_ATTACHED} to
- * update the connection status of the supported USB TV tuners.
- */
-public class TunerInputController {
- private static final boolean DEBUG = false;
- private static final String TAG = "TunerInputController";
- private static final String PREFERENCE_IS_NETWORK_TUNER_ATTACHED = "network_tuner";
- private static final String SECURITY_PATCH_LEVEL_KEY = "ro.build.version.security_patch";
- private static final String SECURITY_PATCH_LEVEL_FORMAT = "yyyy-MM-dd";
- private static final String PLAY_STORE_LINK_TEMPLATE = "market://details?id=%s";
-
- /** Action of {@link Intent} to check network connection repeatedly when it is necessary. */
- private static final String CHECKING_NETWORK_TUNER_STATUS =
- "com.android.tv.action.CHECKING_NETWORK_TUNER_STATUS";
-
- private static final String EXTRA_CHECKING_DURATION =
- "com.android.tv.action.extra.CHECKING_DURATION";
- private static final String EXTRA_DEVICE_IP = "com.android.tv.action.extra.DEVICE_IP";
-
- private static final long INITIAL_CHECKING_DURATION_MS = TimeUnit.SECONDS.toMillis(10);
- private static final long MAXIMUM_CHECKING_DURATION_MS = TimeUnit.MINUTES.toMillis(10);
- private static final String NOTIFICATION_CHANNEL_ID = "tuner_discovery_notification";
-
- // TODO: Load settings from XML file
- private static final TunerDevice[] TUNER_DEVICES = {
- new TunerDevice(0x2040, 0xb123, null), // WinTV-HVR-955Q
- new TunerDevice(0x07ca, 0x0837, null), // AverTV Volar Hybrid Q
- // WinTV-dualHD (bulk) will be supported after 2017 April security patch.
- new TunerDevice(0x2040, 0x826d, "2017-04-01"), // WinTV-dualHD (bulk)
- new TunerDevice(0x2040, 0x0264, null),
- };
-
- private static final int MSG_ENABLE_INPUT_SERVICE = 1000;
- private static final long DVB_DRIVER_CHECK_DELAY_MS = 300;
-
- private final ComponentName usbTunerComponent;
- private final ComponentName networkTunerComponent;
- private final ComponentName builtInTunerComponent;
- private final Map<TunerDevice, ComponentName> mTunerServiceMapping = new HashMap<>();
-
- private final Map<ComponentName, String> mTunerApplicationNames = new HashMap<>();
- private final Map<ComponentName, String> mNotificationMessages = new HashMap<>();
- private final Map<ComponentName, Bitmap> mNotificationLargeIcons = new HashMap<>();
-
- private final CheckDvbDeviceHandler mHandler = new CheckDvbDeviceHandler(this);
-
- public TunerInputController(ComponentName embeddedTuner) {
- usbTunerComponent = embeddedTuner;
- networkTunerComponent = usbTunerComponent;
- builtInTunerComponent = usbTunerComponent;
- for (TunerDevice device : TUNER_DEVICES) {
- mTunerServiceMapping.put(device, usbTunerComponent);
- }
- }
-
- /** Checks status of USB devices to see if there are available USB tuners connected. */
- public void onCheckingUsbTunerStatus(Context context, String action) {
- onCheckingUsbTunerStatus(context, action, mHandler);
- }
-
- private void onCheckingUsbTunerStatus(
- Context context, String action, @NonNull CheckDvbDeviceHandler handler) {
- Set<TunerDevice> connectedUsbTuners = getConnectedUsbTuners(context);
- handler.removeMessages(MSG_ENABLE_INPUT_SERVICE);
- if (!connectedUsbTuners.isEmpty()) {
- // Need to check if DVB driver is accessible. Since the driver creation
- // could be happen after the USB event, delay the checking by
- // DVB_DRIVER_CHECK_DELAY_MS.
- handler.sendMessageDelayed(
- handler.obtainMessage(MSG_ENABLE_INPUT_SERVICE, context),
- DVB_DRIVER_CHECK_DELAY_MS);
- } else {
- handleTunerStatusChanged(
- context,
- false,
- connectedUsbTuners,
- TextUtils.equals(action, UsbManager.ACTION_USB_DEVICE_DETACHED)
- ? TunerHal.TUNER_TYPE_USB
- : null);
- }
- }
-
- private void onNetworkTunerChanged(Context context, boolean enabled) {
- SharedPreferences sharedPreferences =
- PreferenceManager.getDefaultSharedPreferences(context);
- if (sharedPreferences.contains(PREFERENCE_IS_NETWORK_TUNER_ATTACHED)
- && sharedPreferences.getBoolean(PREFERENCE_IS_NETWORK_TUNER_ATTACHED, false)
- == enabled) {
- // the status is not changed
- return;
- }
- if (enabled) {
- sharedPreferences.edit().putBoolean(PREFERENCE_IS_NETWORK_TUNER_ATTACHED, true).apply();
- } else {
- sharedPreferences
- .edit()
- .putBoolean(PREFERENCE_IS_NETWORK_TUNER_ATTACHED, false)
- .apply();
- }
- // Network tuner detection is initiated by UI. So the app should not
- // be killed.
- handleTunerStatusChanged(
- context, true, getConnectedUsbTuners(context), TunerHal.TUNER_TYPE_NETWORK);
- }
-
- /**
- * See if any USB tuner hardware is attached in the system.
- *
- * @param context {@link Context} instance
- * @return {@code true} if any tuner device we support is plugged in
- */
- private Set<TunerDevice> getConnectedUsbTuners(Context context) {
- UsbManager manager = (UsbManager) context.getSystemService(Context.USB_SERVICE);
- Map<String, UsbDevice> deviceList = manager.getDeviceList();
- String currentSecurityLevel =
- SystemPropertiesProxy.getString(SECURITY_PATCH_LEVEL_KEY, null);
-
- Set<TunerDevice> devices = new HashSet<>();
- for (UsbDevice device : deviceList.values()) {
- if (DEBUG) {
- Log.d(TAG, "Device: " + device);
- }
- for (TunerDevice tuner : TUNER_DEVICES) {
- if (tuner.equalsTo(device) && tuner.isSupported(currentSecurityLevel)) {
- Log.i(TAG, "Tuner found");
- devices.add(tuner);
- }
- }
- }
- return devices;
- }
-
- private void handleTunerStatusChanged(
- Context context,
- boolean forceDontKillApp,
- Set<TunerDevice> connectedUsbTuners,
- Integer triggerType) {
- Map<ComponentName, Integer> serviceToEnable = new HashMap<>();
- Set<ComponentName> serviceToDisable = new HashSet<>();
- serviceToDisable.add(builtInTunerComponent);
- serviceToDisable.add(networkTunerComponent);
- if (TunerFeatures.TUNER.isEnabled(context)) {
- // TODO: support both built-in tuner and other tuners at the same time?
- if (TunerHal.useBuiltInTuner(context)) {
- enableTunerTvInputService(
- context, true, false, TunerHal.TUNER_TYPE_BUILT_IN, builtInTunerComponent);
- return;
- }
- SharedPreferences sharedPreferences =
- PreferenceManager.getDefaultSharedPreferences(context);
- if (sharedPreferences.getBoolean(PREFERENCE_IS_NETWORK_TUNER_ATTACHED, false)) {
- serviceToEnable.put(networkTunerComponent, TunerHal.TUNER_TYPE_NETWORK);
- }
- }
- for (TunerDevice device : TUNER_DEVICES) {
- if (TunerFeatures.TUNER.isEnabled(context) && connectedUsbTuners.contains(device)) {
- serviceToEnable.put(mTunerServiceMapping.get(device), TunerHal.TUNER_TYPE_USB);
- } else {
- serviceToDisable.add(mTunerServiceMapping.get(device));
- }
- }
- serviceToDisable.removeAll(serviceToEnable.keySet());
- for (ComponentName serviceComponent : serviceToEnable.keySet()) {
- if (isTunerPackageInstalled(context, serviceComponent)) {
- enableTunerTvInputService(
- context,
- true,
- forceDontKillApp,
- serviceToEnable.get(serviceComponent),
- serviceComponent);
- } else {
- sendNotificationToInstallPackage(context, serviceComponent);
- }
- }
- for (ComponentName serviceComponent : serviceToDisable) {
- if (isTunerPackageInstalled(context, serviceComponent)) {
- enableTunerTvInputService(
- context, false, forceDontKillApp, triggerType, serviceComponent);
- } else {
- cancelNotificationToInstallPackage(context, serviceComponent);
- }
- }
- }
-
- /**
- * Enable/disable the component {@link BaseTunerTvInputService}.
- *
- * @param context {@link Context} instance
- * @param enabled {@code true} to enable the service; otherwise {@code false}
- */
- private static void enableTunerTvInputService(
- Context context,
- boolean enabled,
- boolean forceDontKillApp,
- Integer tunerType,
- ComponentName serviceComponent) {
- if (DEBUG) Log.d(TAG, "enableTunerTvInputService: " + enabled);
- PackageManager pm = context.getPackageManager();
- int newState =
- enabled
- ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED
- : PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
- if (newState != pm.getComponentEnabledSetting(serviceComponent)) {
- int flags = forceDontKillApp ? PackageManager.DONT_KILL_APP : 0;
- if (serviceComponent.getPackageName().equals(context.getPackageName())) {
- // Don't kill APP when handling input count changing. Or the following
- // setComponentEnabledSetting() call won't work.
- ((TvApplication) context.getApplicationContext())
- .handleInputCountChanged(true, enabled, true);
- // Bundled input. Don't kill app if LiveChannels app is active since we don't want
- // to kill the running app.
- if (TvSingletons.getSingletons(context).getMainActivityWrapper().isCreated()) {
- flags |= PackageManager.DONT_KILL_APP;
- }
- // Send/cancel the USB tuner TV input setup notification.
- BaseTunerSetupActivity.onTvInputEnabled(context, enabled, tunerType);
- if (!enabled && tunerType != null) {
- if (tunerType == TunerHal.TUNER_TYPE_USB) {
- Toast.makeText(
- context,
- R.string.msg_usb_tuner_disconnected,
- Toast.LENGTH_SHORT)
- .show();
- } else if (tunerType == TunerHal.TUNER_TYPE_NETWORK) {
- Toast.makeText(
- context,
- R.string.msg_network_tuner_disconnected,
- Toast.LENGTH_SHORT)
- .show();
- }
- }
- }
- // Enable/disable the USB tuner TV input.
- pm.setComponentEnabledSetting(serviceComponent, newState, flags);
- if (DEBUG) Log.d(TAG, "Status updated:" + enabled);
- } else if (enabled && serviceComponent.getPackageName().equals(context.getPackageName())) {
- // 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 void executeNetworkTunerDiscoveryAsyncTask(final Context context) {
- executeNetworkTunerDiscoveryAsyncTask(context, 0, 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.
- * @param deviceIp The previous discovered device IP, 0 if none.
- */
- private void executeNetworkTunerDiscoveryAsyncTask(
- final Context context, final long repeatedDurationMs, final int deviceIp) {
- if (!TunerFeatures.NETWORK_TUNER.isEnabled(context)) {
- return;
- }
- final Intent networkCheckingIntent = new Intent(context, IntentReceiver.class);
- networkCheckingIntent.setAction(CHECKING_NETWORK_TUNER_STATUS);
- if (!isNetworkConnected(context) && repeatedDurationMs > 0) {
- sendCheckingAlarm(context, networkCheckingIntent, repeatedDurationMs);
- } else {
- new AsyncTask<Void, Void, Boolean>() {
- @Override
- protected Boolean doInBackground(Void... params) {
- Boolean result = null;
- // Implement and execute network tuner discovery AsyncTask here.
- return result;
- }
-
- @Override
- protected void onPostExecute(Boolean foundNetworkTuner) {
- if (foundNetworkTuner == null) {
- return;
- }
- sendCheckingAlarm(
- context,
- networkCheckingIntent,
- foundNetworkTuner ? INITIAL_CHECKING_DURATION_MS : repeatedDurationMs);
- onNetworkTunerChanged(context, foundNetworkTuner);
- }
- }.execute();
- }
- }
-
- private static boolean isNetworkConnected(Context context) {
- ConnectivityManager cm =
- (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
- NetworkInfo networkInfo = cm.getActiveNetworkInfo();
- return networkInfo != null && networkInfo.isConnected();
- }
-
- private static void sendCheckingAlarm(Context context, Intent intent, long delayMs) {
- AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
- intent.putExtra(EXTRA_CHECKING_DURATION, delayMs);
- PendingIntent alarmIntent =
- PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
- alarmManager.set(
- AlarmManager.ELAPSED_REALTIME,
- SystemClock.elapsedRealtime() + delayMs,
- alarmIntent);
- }
-
- private static boolean isTunerPackageInstalled(
- Context context, ComponentName serviceComponent) {
- try {
- context.getPackageManager().getPackageInfo(serviceComponent.getPackageName(), 0);
- return true;
- } catch (NameNotFoundException e) {
- return false;
- }
- }
-
- private void sendNotificationToInstallPackage(Context context, ComponentName serviceComponent) {
- if (!BuildConfig.ENG) {
- return;
- }
- String applicationName = mTunerApplicationNames.get(serviceComponent);
- if (applicationName == null) {
- applicationName = context.getString(R.string.tuner_install_default_application_name);
- }
- String contentTitle =
- context.getString(
- R.string.tuner_install_notification_content_title, applicationName);
- String contentText = mNotificationMessages.get(serviceComponent);
- if (contentText == null) {
- contentText = context.getString(R.string.tuner_install_notification_content_text);
- }
- Bitmap largeIcon = mNotificationLargeIcons.get(serviceComponent);
- if (largeIcon == null) {
- // TODO: Make a better default image.
- largeIcon = BitmapFactory.decodeResource(context.getResources(), R.drawable.ic_store);
- }
- NotificationManager notificationManager =
- (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
- if (notificationManager.getNotificationChannel(NOTIFICATION_CHANNEL_ID) == null) {
- createNotificationChannel(context, notificationManager);
- }
- Intent intent = new Intent(Intent.ACTION_VIEW);
- intent.setData(
- Uri.parse(
- String.format(
- PLAY_STORE_LINK_TEMPLATE, serviceComponent.getPackageName())));
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- Notification.Builder builder = new Notification.Builder(context, NOTIFICATION_CHANNEL_ID);
- builder.setAutoCancel(true)
- .setSmallIcon(R.drawable.ic_launcher_s)
- .setLargeIcon(largeIcon)
- .setContentTitle(contentTitle)
- .setContentText(contentText)
- .setCategory(Notification.CATEGORY_RECOMMENDATION)
- .setContentIntent(PendingIntent.getActivity(context, 0, intent, 0));
- notificationManager.notify(serviceComponent.getPackageName(), 0, builder.build());
- }
-
- private static void cancelNotificationToInstallPackage(
- Context context, ComponentName serviceComponent) {
- NotificationManager notificationManager =
- (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
- notificationManager.cancel(serviceComponent.getPackageName(), 0);
- }
-
- private static void createNotificationChannel(
- Context context, NotificationManager notificationManager) {
- notificationManager.createNotificationChannel(
- new NotificationChannel(
- NOTIFICATION_CHANNEL_ID,
- context.getResources()
- .getString(R.string.ut_setup_notification_channel_name),
- NotificationManager.IMPORTANCE_HIGH));
- }
-
- public static class IntentReceiver extends BroadcastReceiver {
-
- @Override
- public void onReceive(Context context, Intent intent) {
- if (DEBUG) Log.d(TAG, "Broadcast intent received:" + intent);
- Starter.start(context);
- TunerInputController tunerInputController =
- TvSingletons.getSingletons(context).getTunerInputController();
- if (!TunerFeatures.TUNER.isEnabled(context)) {
- tunerInputController.handleTunerStatusChanged(
- context, false, Collections.emptySet(), null);
- return;
- }
- switch (intent.getAction()) {
- case Intent.ACTION_BOOT_COMPLETED:
- tunerInputController.executeNetworkTunerDiscoveryAsyncTask(
- context, INITIAL_CHECKING_DURATION_MS, 0);
- // fall through
- case TvApplication.ACTION_APPLICATION_FIRST_LAUNCHED:
- case UsbManager.ACTION_USB_DEVICE_ATTACHED:
- case UsbManager.ACTION_USB_DEVICE_DETACHED:
- tunerInputController.onCheckingUsbTunerStatus(context, intent.getAction());
- break;
- case CHECKING_NETWORK_TUNER_STATUS:
- long repeatedDurationMs =
- intent.getLongExtra(
- EXTRA_CHECKING_DURATION, INITIAL_CHECKING_DURATION_MS);
- tunerInputController.executeNetworkTunerDiscoveryAsyncTask(
- context,
- Math.min(repeatedDurationMs * 2, MAXIMUM_CHECKING_DURATION_MS),
- intent.getIntExtra(EXTRA_DEVICE_IP, 0));
- break;
- default: // fall out
- }
- }
- }
-
- /**
- * Simple data holder for a USB device. Used to represent a tuner model, and compare against
- * {@link UsbDevice}.
- */
- private static class TunerDevice {
- private final int vendorId;
- private final int productId;
-
- // security patch level from which the specific tuner type is supported.
- private final String minSecurityLevel;
-
- private TunerDevice(int vendorId, int productId, String minSecurityLevel) {
- this.vendorId = vendorId;
- this.productId = productId;
- this.minSecurityLevel = minSecurityLevel;
- }
-
- private boolean equalsTo(UsbDevice device) {
- return device.getVendorId() == vendorId && device.getProductId() == productId;
- }
-
- private boolean isSupported(String currentSecurityLevel) {
- if (minSecurityLevel == null) {
- return true;
- }
-
- long supportSecurityLevelTimeStamp = 0;
- long currentSecurityLevelTimestamp = 0;
- try {
- SimpleDateFormat format = new SimpleDateFormat(SECURITY_PATCH_LEVEL_FORMAT);
- supportSecurityLevelTimeStamp = format.parse(minSecurityLevel).getTime();
- currentSecurityLevelTimestamp = format.parse(currentSecurityLevel).getTime();
- } catch (ParseException e) {
- }
- return supportSecurityLevelTimeStamp != 0
- && supportSecurityLevelTimeStamp <= currentSecurityLevelTimestamp;
- }
- }
-
- private static class CheckDvbDeviceHandler extends Handler {
-
- private final TunerInputController mTunerInputController;
- private DvbDeviceAccessor mDvbDeviceAccessor;
-
- CheckDvbDeviceHandler(TunerInputController tunerInputController) {
- super(Looper.getMainLooper());
- this.mTunerInputController = tunerInputController;
- }
-
- @Override
- public void handleMessage(Message msg) {
- switch (msg.what) {
- case MSG_ENABLE_INPUT_SERVICE:
- Context context = (Context) msg.obj;
- if (mDvbDeviceAccessor == null) {
- mDvbDeviceAccessor = new DvbDeviceAccessor(context);
- }
- boolean enabled = mDvbDeviceAccessor.isDvbDeviceAvailable();
- mTunerInputController.handleTunerStatusChanged(
- context,
- false,
- enabled
- ? mTunerInputController.getConnectedUsbTuners(context)
- : Collections.emptySet(),
- TunerHal.TUNER_TYPE_USB);
- break;
- default: // fall out
- }
- }
- }
-}
diff --git a/src/com/android/tv/tunerinputcontroller/BuiltInTunerManager.java b/src/com/android/tv/tunerinputcontroller/BuiltInTunerManager.java
new file mode 100644
index 00000000..e4fa35d9
--- /dev/null
+++ b/src/com/android/tv/tunerinputcontroller/BuiltInTunerManager.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.tunerinputcontroller;
+
+import com.android.tv.common.singletons.HasTvInputId;
+
+/** Controllers and parameters needed to access a built in tuner. */
+public interface BuiltInTunerManager extends HasTvInputId {
+ TunerInputController getTunerInputController();
+}
diff --git a/src/com/android/tv/tunerinputcontroller/HasBuiltInTunerManager.java b/src/com/android/tv/tunerinputcontroller/HasBuiltInTunerManager.java
new file mode 100644
index 00000000..90540bc3
--- /dev/null
+++ b/src/com/android/tv/tunerinputcontroller/HasBuiltInTunerManager.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.tunerinputcontroller;
+
+import com.google.common.base.Optional;
+
+/**
+ * Has optional {@link BuiltInTunerManager}.
+ *
+ * <p>If the {@code BuiltInTunerManager} is absent the built tuner is not enabled.
+ */
+public interface HasBuiltInTunerManager {
+
+ /** @deprecated inject instead */
+ @Deprecated
+ Optional<BuiltInTunerManager> getBuiltInTunerManager();
+}
diff --git a/src/com/android/tv/tunerinputcontroller/TunerInputController.java b/src/com/android/tv/tunerinputcontroller/TunerInputController.java
new file mode 100644
index 00000000..f822dbe0
--- /dev/null
+++ b/src/com/android/tv/tunerinputcontroller/TunerInputController.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tunerinputcontroller;
+
+import android.content.Context;
+import android.content.Intent;
+
+/** Controls the package visibility of built in tuner services. */
+public interface TunerInputController {
+
+ Intent createSetupIntent(Context context);
+
+ void onCheckingUsbTunerStatus(Context context, String action);
+
+ void executeNetworkTunerDiscoveryAsyncTask(Context context);
+
+ /**
+ * Updates tuner input's info.
+ *
+ * @param context {@link Context} instance
+ */
+ void updateTunerInputInfo(Context context);
+}
diff --git a/src/com/android/tv/ui/AppLayerTvView.java b/src/com/android/tv/ui/AppLayerTvView.java
index b2be9f02..e2b64a1e 100644
--- a/src/com/android/tv/ui/AppLayerTvView.java
+++ b/src/com/android/tv/ui/AppLayerTvView.java
@@ -17,10 +17,10 @@
package com.android.tv.ui;
import android.content.Context;
-import android.media.tv.TvView;
import android.util.AttributeSet;
import android.view.SurfaceView;
import android.view.View;
+import com.android.tv.common.compat.TvViewCompat;
import com.android.tv.common.util.CommonUtils;
import com.android.tv.common.util.Debug;
@@ -31,7 +31,7 @@ import com.android.tv.common.util.Debug;
* android.media.tv.TvView#setMain()} does not work because its implementation assumes that the app
* uses only application layer. TODO: remove this class once the TvView.setMain() is revisited.
*/
-public class AppLayerTvView extends TvView {
+public class AppLayerTvView extends TvViewCompat {
public AppLayerTvView(Context context) {
super(context);
}
diff --git a/src/com/android/tv/ui/BlockScreenView.java b/src/com/android/tv/ui/BlockScreenView.java
index 6b2d9a01..b7a2dd95 100644
--- a/src/com/android/tv/ui/BlockScreenView.java
+++ b/src/com/android/tv/ui/BlockScreenView.java
@@ -22,6 +22,7 @@ import android.animation.AnimatorInflater;
import android.animation.AnimatorListenerAdapter;
import android.content.Context;
import android.graphics.drawable.Drawable;
+import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.View;
@@ -180,6 +181,10 @@ public class BlockScreenView extends FrameLayout {
requestLayout();
}
+ public void setInfoTextOnClickListener(@Nullable OnClickListener onClickListener) {
+ mBlockingInfoTextView.setOnClickListener(onClickListener);
+ }
+
/** Changes the view layout according to the {@code blockScreenType}. */
public void onBlockStatusChanged(@BlockScreenType int blockScreenType, boolean withAnimation) {
if (!withAnimation) {
@@ -252,4 +257,8 @@ public class BlockScreenView extends FrameLayout {
mInfoFadeOut.end();
}
}
+
+ public void setInfoTextClickable(boolean clickable) {
+ mBlockingInfoTextView.setClickable(clickable);
+ }
}
diff --git a/src/com/android/tv/ui/ChannelBannerView.java b/src/com/android/tv/ui/ChannelBannerView.java
index 28325197..00ac7e32 100644
--- a/src/com/android/tv/ui/ChannelBannerView.java
+++ b/src/com/android/tv/ui/ChannelBannerView.java
@@ -46,11 +46,10 @@ import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import android.widget.TextView;
-import com.android.tv.MainActivity;
import com.android.tv.R;
-import com.android.tv.TvSingletons;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.common.feature.CommonFeatures;
+import com.android.tv.common.singletons.HasSingletons;
import com.android.tv.data.Program;
import com.android.tv.data.StreamInfo;
import com.android.tv.data.api.Channel;
@@ -59,11 +58,14 @@ import com.android.tv.dvr.data.ScheduledRecording;
import com.android.tv.parental.ContentRatingsManager;
import com.android.tv.ui.TvTransitionManager.TransitionLayout;
import com.android.tv.ui.hideable.AutoHideScheduler;
+import com.android.tv.util.TvInputManagerHelper;
import com.android.tv.util.Utils;
import com.android.tv.util.images.ImageCache;
import com.android.tv.util.images.ImageLoader;
import com.android.tv.util.images.ImageLoader.ImageLoaderCallback;
import com.android.tv.util.images.ImageLoader.LoadTvInputLogoTask;
+import com.google.common.collect.ImmutableList;
+import javax.inject.Provider;
/** A view to render channel banner. */
public class ChannelBannerView extends FrameLayout
@@ -74,6 +76,21 @@ public class ChannelBannerView extends FrameLayout
/** Show all information at the channel banner. */
public static final int LOCK_NONE = 0;
+ /** Singletons needed for this class. */
+ public interface MySingletons {
+ Provider<Channel> getCurrentChannelProvider();
+
+ Provider<Program> getCurrentProgramProvider();
+
+ Provider<TvOverlayManager> getOverlayManagerProvider();
+
+ TvInputManagerHelper getTvInputManagerHelperSingleton();
+
+ Provider<Long> getCurrentPlayingPositionProvider();
+
+ DvrManager getDvrManagerSingleton();
+ }
+
/**
* Lock program details at the channel banner. This is used when a content is locked so we don't
* want to show program details including program description text and poster art.
@@ -94,14 +111,21 @@ public class ChannelBannerView extends FrameLayout
private Program mLockedChannelProgram;
private static String sClosedCaptionMark;
- private final MainActivity mMainActivity;
private final Resources mResources;
+ private final Provider<Channel> mCurrentChannelProvider;
+ private final Provider<Program> mCurrentProgramProvider;
+ private final Provider<Long> mCurrentPlayingPositionProvider;
+ private final TvInputManagerHelper mTvInputManagerHelper;
+ // TvOverlayManager is always created after ChannelBannerView
+ private final Provider<TvOverlayManager> mTvOverlayManager;
+
private View mChannelView;
private TextView mChannelNumberTextView;
private ImageView mChannelLogoImageView;
private TextView mProgramTextView;
private ImageView mTvInputLogoImageView;
+ private ImageView mChannelSignalStrengthView;
private TextView mChannelNameTextView;
private TextView mProgramTimeTextView;
private ProgressBar mRemainingTimeView;
@@ -143,6 +167,32 @@ public class ChannelBannerView extends FrameLayout
private final int mRecordingIconPadding;
private final Interpolator mResizeInterpolator;
+ /**
+ * 0 - 100 represent signal strength percentage. Strength is divided into 5 levels (0 - 4).
+ *
+ * <p>This is the upper boundary of level 0 [0%, 20%], and the lower boundary of level 1 (20%,
+ * 40%].
+ */
+ private static final int SIGNAL_STRENGTH_0_OF_4_UPPER_BOUND = 20;
+
+ /**
+ * This is the upper boundary of level 1 (20%, 40%], and the lower boundary of level 2 (40%,
+ * 60%].
+ */
+ private static final int SIGNAL_STRENGTH_1_OF_4_UPPER_BOUND = 40;
+
+ /**
+ * This is the upper boundary of level of level 2. (40%, 60%], and the lower boundary of level 3
+ * (60%, 80%].
+ */
+ private static final int SIGNAL_STRENGTH_2_OF_4_UPPER_BOUND = 60;
+
+ /**
+ * This is the upper boundary of level of level 3 (60%, 80%], and the lower boundary of level 4
+ * (80%, 100%].
+ */
+ private static final int SIGNAL_STRENGTH_3_OF_4_UPPER_BOUND = 80;
+
private final AnimatorListenerAdapter mResizeAnimatorListener =
new AnimatorListenerAdapter() {
@Override
@@ -172,7 +222,14 @@ public class ChannelBannerView extends FrameLayout
public ChannelBannerView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mResources = getResources();
- mMainActivity = (MainActivity) context;
+
+ @SuppressWarnings("unchecked") // injection
+ MySingletons singletons = HasSingletons.get(MySingletons.class, context);
+ mCurrentChannelProvider = singletons.getCurrentChannelProvider();
+ mCurrentProgramProvider = singletons.getCurrentProgramProvider();
+ mCurrentPlayingPositionProvider = singletons.getCurrentPlayingPositionProvider();
+ mTvInputManagerHelper = singletons.getTvInputManagerHelperSingleton();
+ mTvOverlayManager = singletons.getOverlayManagerProvider();
mShowDurationMillis = mResources.getInteger(R.integer.channel_banner_show_duration);
mChannelLogoImageViewWidth =
@@ -195,20 +252,17 @@ public class ChannelBannerView extends FrameLayout
mProgramDescriptionFadeInAnimator =
AnimatorInflater.loadAnimator(
- mMainActivity, R.animator.channel_banner_program_description_fade_in);
+ context, R.animator.channel_banner_program_description_fade_in);
mProgramDescriptionFadeOutAnimator =
AnimatorInflater.loadAnimator(
- mMainActivity, R.animator.channel_banner_program_description_fade_out);
+ context, R.animator.channel_banner_program_description_fade_out);
- if (CommonFeatures.DVR.isEnabled(mMainActivity)) {
- mDvrManager = TvSingletons.getSingletons(mMainActivity).getDvrManager();
+ if (CommonFeatures.DVR.isEnabled(context)) {
+ mDvrManager = singletons.getDvrManagerSingleton();
} else {
mDvrManager = null;
}
- mContentRatingsManager =
- TvSingletons.getSingletons(getContext())
- .getTvInputManagerHelper()
- .getContentRatingsManager();
+ mContentRatingsManager = mTvInputManagerHelper.getContentRatingsManager();
mNoProgram =
new Program.Builder()
@@ -234,22 +288,23 @@ public class ChannelBannerView extends FrameLayout
mChannelView = findViewById(R.id.channel_banner_view);
- mChannelNumberTextView = (TextView) findViewById(R.id.channel_number);
- mChannelLogoImageView = (ImageView) findViewById(R.id.channel_logo);
- mProgramTextView = (TextView) findViewById(R.id.program_text);
- mTvInputLogoImageView = (ImageView) findViewById(R.id.tvinput_logo);
- mChannelNameTextView = (TextView) findViewById(R.id.channel_name);
- mProgramTimeTextView = (TextView) findViewById(R.id.program_time_text);
- mRemainingTimeView = (ProgressBar) findViewById(R.id.remaining_time);
- mRecordingIndicatorView = (TextView) findViewById(R.id.recording_indicator);
- mClosedCaptionTextView = (TextView) findViewById(R.id.closed_caption);
- mAspectRatioTextView = (TextView) findViewById(R.id.aspect_ratio);
- mResolutionTextView = (TextView) findViewById(R.id.resolution);
- mAudioChannelTextView = (TextView) findViewById(R.id.audio_channel);
- mContentRatingsTextViews[0] = (TextView) findViewById(R.id.content_ratings_0);
- mContentRatingsTextViews[1] = (TextView) findViewById(R.id.content_ratings_1);
- mContentRatingsTextViews[2] = (TextView) findViewById(R.id.content_ratings_2);
- mProgramDescriptionTextView = (TextView) findViewById(R.id.program_description);
+ mChannelNumberTextView = findViewById(R.id.channel_number);
+ mChannelLogoImageView = findViewById(R.id.channel_logo);
+ mProgramTextView = findViewById(R.id.program_text);
+ mTvInputLogoImageView = findViewById(R.id.tvinput_logo);
+ mChannelSignalStrengthView = findViewById(R.id.channel_signal_strength);
+ mChannelNameTextView = findViewById(R.id.channel_name);
+ mProgramTimeTextView = findViewById(R.id.program_time_text);
+ mRemainingTimeView = findViewById(R.id.remaining_time);
+ mRecordingIndicatorView = findViewById(R.id.recording_indicator);
+ mClosedCaptionTextView = findViewById(R.id.closed_caption);
+ mAspectRatioTextView = findViewById(R.id.aspect_ratio);
+ mResolutionTextView = findViewById(R.id.resolution);
+ mAudioChannelTextView = findViewById(R.id.audio_channel);
+ mContentRatingsTextViews[0] = findViewById(R.id.content_ratings_0);
+ mContentRatingsTextViews[1] = findViewById(R.id.content_ratings_1);
+ mContentRatingsTextViews[2] = findViewById(R.id.content_ratings_2);
+ mProgramDescriptionTextView = findViewById(R.id.program_description);
mAnchorView = findViewById(R.id.anchor);
mProgramDescriptionFadeInAnimator.setTarget(mProgramDescriptionTextView);
@@ -310,7 +365,7 @@ public class ChannelBannerView extends FrameLayout
*/
public void setBlockingContentRating(TvContentRating rating) {
mBlockingContentRating = rating;
- updateProgramRatings(mMainActivity.getCurrentProgram());
+ updateProgramRatings(mCurrentProgramProvider.get());
}
/**
@@ -328,20 +383,20 @@ public class ChannelBannerView extends FrameLayout
mAutoHideScheduler.schedule(mShowDurationMillis);
}
mBlockingContentRating = null;
- mCurrentChannel = mMainActivity.getCurrentChannel();
+ mCurrentChannel = mCurrentChannelProvider.get();
mCurrentChannelLogoExists =
mCurrentChannel != null && mCurrentChannel.channelLogoExists();
updateStreamInfo(null);
updateChannelInfo();
}
- updateProgramInfo(mMainActivity.getCurrentProgram());
+ updateProgramInfo(mCurrentProgramProvider.get());
mUpdateOnTune = false;
}
private void hide() {
mCurrentHeight = 0;
- mMainActivity
- .getOverlayManager()
+ mTvOverlayManager
+ .get()
.hideOverlays(
TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_DIALOG
| TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS
@@ -367,10 +422,10 @@ public class ChannelBannerView extends FrameLayout
updateText(
mResolutionTextView,
Utils.getVideoDefinitionLevelString(
- mMainActivity, info.getVideoDefinitionLevel()));
+ getContext(), info.getVideoDefinitionLevel()));
updateText(
mAudioChannelTextView,
- Utils.getAudioChannelString(mMainActivity, info.getAudioChannelCount()));
+ Utils.getAudioChannelString(getContext(), info.getAudioChannelCount()));
} else {
// Channel change has been requested. But, StreamInfo hasn't been updated yet.
mClosedCaptionTextView.setVisibility(View.GONE);
@@ -418,8 +473,7 @@ public class ChannelBannerView extends FrameLayout
}
mChannelNumberTextView.setText(displayNumber);
mChannelNameTextView.setText(displayName);
- TvInputInfo info =
- mMainActivity.getTvInputManagerHelper().getTvInputInfo(getCurrentInputId());
+ TvInputInfo info = mTvInputManagerHelper.getTvInputInfo(getCurrentInputId());
if (info == null
|| !ImageLoader.loadBitmap(
createTvInputLogoLoaderCallback(info, this),
@@ -440,7 +494,7 @@ public class ChannelBannerView extends FrameLayout
}
private String getCurrentInputId() {
- Channel channel = mMainActivity.getCurrentChannel();
+ Channel channel = mCurrentChannelProvider.get();
return channel != null ? channel.getInputId() : null;
}
@@ -503,6 +557,34 @@ public class ChannelBannerView extends FrameLayout
};
}
+ public void updateChannelSignalStrengthView(int value) {
+ int resId = signalStrenghtToResId(value);
+ if (resId != 0) {
+ mChannelSignalStrengthView.setVisibility(View.VISIBLE);
+ mChannelSignalStrengthView.setImageResource(resId);
+ } else {
+ mChannelSignalStrengthView.setVisibility(View.GONE);
+ }
+ }
+
+ private int signalStrenghtToResId(int value) {
+ int signal = 0;
+ if (value >= 0 && value <= 100) {
+ if (value <= SIGNAL_STRENGTH_0_OF_4_UPPER_BOUND) {
+ signal = R.drawable.quantum_ic_signal_cellular_0_bar_white_24;
+ } else if (value <= SIGNAL_STRENGTH_1_OF_4_UPPER_BOUND) {
+ signal = R.drawable.quantum_ic_signal_cellular_1_bar_white_24;
+ } else if (value <= SIGNAL_STRENGTH_2_OF_4_UPPER_BOUND) {
+ signal = R.drawable.quantum_ic_signal_cellular_2_bar_white_24;
+ } else if (value <= SIGNAL_STRENGTH_3_OF_4_UPPER_BOUND) {
+ signal = R.drawable.quantum_ic_signal_cellular_3_bar_white_24;
+ } else {
+ signal = R.drawable.quantum_ic_signal_cellular_4_bar_white_24;
+ }
+ }
+ return signal;
+ }
+
private void updateLogo(@Nullable Bitmap logo) {
if (logo == null) {
// Need to update the text size of the program text view depending on the channel logo.
@@ -651,13 +733,14 @@ public class ChannelBannerView extends FrameLayout
mContentRatingsTextViews[i].setVisibility(View.GONE);
}
} else {
- TvContentRating[] ratings = (program == null) ? null : program.getContentRatings();
+ ImmutableList<TvContentRating> ratings =
+ (program == null) ? null : program.getContentRatings();
for (int i = 0; i < DISPLAYED_CONTENT_RATINGS_COUNT; i++) {
- if (ratings == null || ratings.length <= i) {
+ if (ratings == null || ratings.size() <= i) {
mContentRatingsTextViews[i].setVisibility(View.GONE);
} else {
mContentRatingsTextViews[i].setText(
- mContentRatingsManager.getDisplayNameForRating(ratings[i]));
+ mContentRatingsManager.getDisplayNameForRating(ratings.get(i)));
mContentRatingsTextViews[i].setVisibility(View.VISIBLE);
}
}
@@ -667,13 +750,11 @@ public class ChannelBannerView extends FrameLayout
private void updateProgramTimeInfo(Program program) {
long durationMs = program.getDurationMillis();
long startTimeMs = program.getStartTimeUtcMillis();
- long endTimeMs = program.getEndTimeUtcMillis();
if (mLockType != LOCK_CHANNEL_INFO && durationMs > 0 && startTimeMs > 0) {
mProgramTimeTextView.setVisibility(View.VISIBLE);
mRemainingTimeView.setVisibility(View.VISIBLE);
- mProgramTimeTextView.setText(
- Utils.getDurationString(getContext(), startTimeMs, endTimeMs, true));
+ mProgramTimeTextView.setText(program.getDurationString(getContext()));
} else {
mProgramTimeTextView.setVisibility(View.GONE);
mRemainingTimeView.setVisibility(View.GONE);
@@ -713,7 +794,7 @@ public class ChannelBannerView extends FrameLayout
Program program, @Nullable ScheduledRecording recording) {
long programStartTime = program.getStartTimeUtcMillis();
long programEndTime = program.getEndTimeUtcMillis();
- long currentPosition = mMainActivity.getCurrentPlayingPosition();
+ long currentPosition = mCurrentPlayingPositionProvider.get();
updateRecordingIndicator(recording);
if (recording != null) {
// Recording now. Use recording-style progress bar.
@@ -734,12 +815,12 @@ public class ChannelBannerView extends FrameLayout
if (recording != null) {
if (mRemainingTimeView.getVisibility() == View.GONE) {
mRecordingIndicatorView.setText(
- mMainActivity
+ getContext()
.getResources()
.getString(
R.string.dvr_recording_till_format,
DateUtils.formatDateTime(
- mMainActivity,
+ getContext(),
recording.getEndTimeMs(),
DateUtils.FORMAT_SHOW_TIME)));
mRecordingIndicatorView.setCompoundDrawablePadding(mRecordingIconPadding);
@@ -754,7 +835,7 @@ public class ChannelBannerView extends FrameLayout
}
private boolean isCurrentProgram(ScheduledRecording recording, Program program) {
- long currentPosition = mMainActivity.getCurrentPlayingPosition();
+ long currentPosition = mCurrentPlayingPositionProvider.get();
return (recording.getType() == ScheduledRecording.TYPE_PROGRAM
&& recording.getProgramId() == program.getId())
|| (recording.getType() == ScheduledRecording.TYPE_TIMED
diff --git a/src/com/android/tv/ui/DetailsActivity.java b/src/com/android/tv/ui/DetailsActivity.java
new file mode 100644
index 00000000..80c0f64b
--- /dev/null
+++ b/src/com/android/tv/ui/DetailsActivity.java
@@ -0,0 +1,209 @@
+/*
+ * 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.app.Activity;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.support.annotation.NonNull;
+import android.support.v17.leanback.app.DetailsFragment;
+import android.transition.Transition;
+import android.transition.Transition.TransitionListener;
+import android.util.Log;
+import android.view.View;
+import com.android.tv.R;
+import com.android.tv.Starter;
+import com.android.tv.TvSingletons;
+import com.android.tv.dialog.PinDialogFragment;
+import com.android.tv.dvr.DvrManager;
+import com.android.tv.dvr.ui.browse.CurrentRecordingDetailsFragment;
+import com.android.tv.dvr.ui.browse.RecordedProgramDetailsFragment;
+import com.android.tv.dvr.ui.browse.ScheduledRecordingDetailsFragment;
+import com.android.tv.dvr.ui.browse.SeriesRecordingDetailsFragment;
+
+/** Activity to show details view. */
+public class DetailsActivity extends Activity implements PinDialogFragment.OnPinCheckedListener {
+ private static final String TAG = "DetailsActivity";
+
+ private static final long INVALID_RECORD_ID = -1;
+
+ /** Name of record id added to the Intent. */
+ public static final String RECORDING_ID = "record_id";
+ /** Name of program uri added to the Intent. */
+ public static final String PROGRAM = "program";
+ /** Name of channel id added to the Intent. */
+ public static final String CHANNEL_ID = "channel_id";
+ /** Name of input id added to the Intent. */
+ public static final String INPUT_ID = "input_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;
+
+ /** SERIES_RECORDING_VIEW refers to program. */
+ public static final int PROGRAM_VIEW = 5;
+
+ public static final int REQUEST_DELETE = 1;
+
+ private PinDialogFragment.OnPinCheckedListener mOnPinCheckedListener;
+ private long mRecordId = INVALID_RECORD_ID;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ Starter.start(this);
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_dvr_details);
+ long recordId = getIntent().getLongExtra(RECORDING_ID, INVALID_RECORD_ID);
+ int detailsViewType = getIntent().getIntExtra(DETAILS_VIEW_TYPE, -1);
+ boolean hideViewSchedule = getIntent().getBooleanExtra(HIDE_VIEW_SCHEDULE, false);
+ long channelId = getIntent().getLongExtra(CHANNEL_ID, -1);
+ DetailsFragment detailsFragment = null;
+ Bundle args = new Bundle();
+ if (detailsViewType != -1 && savedInstanceState == null) {
+ if (recordId != INVALID_RECORD_ID) {
+ mRecordId = recordId;
+ args.putLong(RECORDING_ID, mRecordId);
+ 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();
+ }
+ } else if (detailsViewType == PROGRAM_VIEW && channelId != -1) {
+ Parcelable program = getIntent().getParcelableExtra(PROGRAM);
+ if (program != null) {
+ args.putLong(CHANNEL_ID, channelId);
+ args.putParcelable(PROGRAM, program);
+ args.putString(INPUT_ID, getIntent().getStringExtra(INPUT_ID));
+ detailsFragment = new ProgramDetailsFragment();
+ }
+ }
+ if (detailsFragment != null) {
+ detailsFragment.setArguments(args);
+ getFragmentManager()
+ .beginTransaction()
+ .replace(R.id.dvr_details_view_frame, detailsFragment)
+ .commit();
+ }
+ }
+
+ // This is a workaround for the focus on O device
+ addTransitionListener();
+ }
+
+ @Override
+ public void onPinChecked(boolean checked, int type, String rating) {
+ if (mOnPinCheckedListener != null) {
+ mOnPinCheckedListener.onPinChecked(checked, type, rating);
+ }
+ }
+
+ public void setOnPinCheckListener(PinDialogFragment.OnPinCheckedListener listener) {
+ mOnPinCheckedListener = listener;
+ }
+
+ private void addTransitionListener() {
+ getWindow()
+ .getSharedElementEnterTransition()
+ .addListener(
+ new TransitionListener() {
+ @Override
+ public void onTransitionStart(Transition transition) {
+ // Do nothing
+ }
+
+ @Override
+ public void onTransitionEnd(Transition transition) {
+ View actions = findViewById(R.id.details_overview_actions);
+ if (actions != null) {
+ actions.requestFocus();
+ }
+ }
+
+ @Override
+ public void onTransitionCancel(Transition transition) {
+ // Do nothing
+
+ }
+
+ @Override
+ public void onTransitionPause(Transition transition) {
+ // Do nothing
+ }
+
+ @Override
+ public void onTransitionResume(Transition transition) {
+ // Do nothing
+ }
+ });
+ }
+
+ @Override
+ public void onRequestPermissionsResult(
+ int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ switch (requestCode) {
+ case REQUEST_DELETE:
+ // If request is cancelled, the result arrays are empty.
+ if (grantResults.length > 0
+ && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ delete(true);
+ } else {
+ Log.i(
+ TAG,
+ "Write permission denied, Not trying to delete the file for "
+ + mRecordId);
+ delete(false);
+ }
+ break;
+ default:
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ }
+ }
+
+ private void delete(boolean deleteFile) {
+ if (mRecordId != INVALID_RECORD_ID) {
+ DvrManager dvrManager = TvSingletons.getSingletons(this).getDvrManager();
+ dvrManager.removeRecordedProgram(mRecordId, deleteFile);
+ }
+ finish();
+ }
+}
diff --git a/src/com/android/tv/ui/FullscreenDialogView.java b/src/com/android/tv/ui/FullscreenDialogView.java
index 800fa85a..d3fec824 100644
--- a/src/com/android/tv/ui/FullscreenDialogView.java
+++ b/src/com/android/tv/ui/FullscreenDialogView.java
@@ -83,13 +83,7 @@ public class FullscreenDialogView extends FrameLayout
/** Dismisses the host {@link Dialog}. */
protected void dismiss() {
- startExitAnimation(
- new Runnable() {
- @Override
- public void run() {
- mDialog.dismiss();
- }
- });
+ startExitAnimation(() -> mDialog.dismiss());
}
@Override
@@ -110,9 +104,7 @@ public class FullscreenDialogView extends FrameLayout
v.mSkipEnterAlphaAnimation = true;
v.initialize(mActivity, mDialog);
startExitAnimation(
- new Runnable() {
- @Override
- public void run() {
+ () ->
new Handler()
.postDelayed(
new Runnable() {
@@ -122,9 +114,7 @@ public class FullscreenDialogView extends FrameLayout
getDialog().setContentView(v);
}
},
- TRANSITION_INTERVAL_MS);
- }
- });
+ TRANSITION_INTERVAL_MS));
}
/** Called when an enter animation starts. Sub-view specific animation can be implemented. */
diff --git a/src/com/android/tv/ui/InputBannerView.java b/src/com/android/tv/ui/InputBannerView.java
index 5ac715bf..d0609186 100644
--- a/src/com/android/tv/ui/InputBannerView.java
+++ b/src/com/android/tv/ui/InputBannerView.java
@@ -31,9 +31,7 @@ public class InputBannerView extends LinearLayout implements TvTransitionManager
private final long mShowDurationMillis;
private final Runnable mHideRunnable =
- new Runnable() {
- @Override
- public void run() {
+ () ->
((MainActivity) getContext())
.getOverlayManager()
.hideOverlays(
@@ -42,9 +40,6 @@ public class InputBannerView extends LinearLayout implements TvTransitionManager
| TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE
| TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_MENU
| TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT);
- }
- };
-
private TextView mInputLabelTextView;
private TextView mSecondaryInputLabelTextView;
diff --git a/src/com/android/tv/ui/IntroView.java b/src/com/android/tv/ui/IntroView.java
index be9fb691..e7240747 100644
--- a/src/com/android/tv/ui/IntroView.java
+++ b/src/com/android/tv/ui/IntroView.java
@@ -102,13 +102,7 @@ public class IntroView extends FullscreenDialogView {
.setInterpolator(interpolator)
.setDuration(duration)
.withLayer()
- .withEndAction(
- new Runnable() {
- @Override
- public void run() {
- onAnimationEnded.run();
- }
- })
+ .withEndAction(onAnimationEnded)
.start();
}
}
diff --git a/src/com/android/tv/ui/KeypadChannelSwitchView.java b/src/com/android/tv/ui/KeypadChannelSwitchView.java
index e2625811..a26175a4 100644
--- a/src/com/android/tv/ui/KeypadChannelSwitchView.java
+++ b/src/com/android/tv/ui/KeypadChannelSwitchView.java
@@ -148,13 +148,10 @@ public class KeypadChannelSwitchView extends LinearLayout
mChannelItemListView.setFocusable(false);
final Channel channel = ((Channel) mAdapter.getItem(position));
postDelayed(
- new Runnable() {
- @Override
- public void run() {
- mChannelItemListView.setFocusable(true);
- mMainActivity.tuneToChannel(channel);
- mTracker.sendChannelNumberItemClicked();
- }
+ () -> {
+ mChannelItemListView.setFocusable(true);
+ mMainActivity.tuneToChannel(channel);
+ mTracker.sendChannelNumberItemClicked();
},
mRippleAnimDurationMillis);
}
diff --git a/src/com/android/tv/ui/ProgramDetailsFragment.java b/src/com/android/tv/ui/ProgramDetailsFragment.java
new file mode 100644
index 00000000..88a7b2ca
--- /dev/null
+++ b/src/com/android/tv/ui/ProgramDetailsFragment.java
@@ -0,0 +1,359 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.tv.ui;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+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.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.TextUtils;
+import com.android.tv.R;
+import com.android.tv.TvSingletons;
+import com.android.tv.common.feature.CommonFeatures;
+import com.android.tv.data.Program;
+import com.android.tv.data.api.Channel;
+import com.android.tv.dvr.DvrDataManager;
+import com.android.tv.dvr.DvrManager;
+import com.android.tv.dvr.DvrScheduleManager;
+import com.android.tv.dvr.data.ScheduledRecording;
+import com.android.tv.dvr.ui.DvrUiHelper;
+import com.android.tv.dvr.ui.browse.ActionPresenterSelector;
+import com.android.tv.dvr.ui.browse.DetailsContent;
+import com.android.tv.dvr.ui.browse.DetailsContentPresenter;
+import com.android.tv.dvr.ui.browse.DetailsViewBackgroundHelper;
+import com.android.tv.util.images.ImageLoader;
+
+/** A fragment shows the details of a Program */
+public class ProgramDetailsFragment extends DetailsFragment
+ implements DvrDataManager.ScheduledRecordingListener,
+ DvrScheduleManager.OnConflictStateChangeListener {
+ private static final int LOAD_LOGO_IMAGE = 1;
+ private static final int LOAD_BACKGROUND_IMAGE = 2;
+
+ private static final int ACTION_VIEW_SCHEDULE = 1;
+ private static final int ACTION_CANCEL = 2;
+ private static final int ACTION_SCHEDULE_RECORDING = 3;
+
+ protected DetailsViewBackgroundHelper mBackgroundHelper;
+ private ArrayObjectAdapter mRowsAdapter;
+ private DetailsOverviewRow mDetailsOverview;
+ private Program mProgram;
+ private String mInputId;
+ private ScheduledRecording mScheduledRecording;
+ private DvrManager mDvrManager;
+ private DvrDataManager mDvrDataManager;
+ private DvrScheduleManager mDvrScheduleManager;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (!onLoadDetails(getArguments())) {
+ getActivity().finish();
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ mDvrDataManager.removeScheduledRecordingListener(this);
+ mDvrScheduleManager.removeOnConflictStateChangeListener(this);
+ super.onDestroy();
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ 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(), DetailsActivity.SHARED_ELEMENT_NAME);
+ rowPresenter.setOnActionClickedListener(onCreateOnActionClickedListener());
+ mRowsAdapter = new ArrayObjectAdapter(onCreatePresenterSelector(rowPresenter));
+ setAdapter(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;
+ }
+
+ /** Updates actions of details overview. */
+ protected void updateActions() {
+ mDetailsOverview.setActionsAdapter(onCreateActionsAdapter());
+ }
+
+ /**
+ * Loads program details according to the arguments the fragment got.
+ *
+ * @return false if cannot find valid programs, else return true. If the return value is false,
+ * the detail activity and fragment will be ended.
+ */
+ private boolean onLoadDetails(Bundle args) {
+ Program program = args.getParcelable(DetailsActivity.PROGRAM);
+ long channelId = args.getLong(DetailsActivity.CHANNEL_ID);
+ String inputId = args.getString(DetailsActivity.INPUT_ID);
+ if (program != null && channelId != Channel.INVALID_ID && !TextUtils.isEmpty(inputId)) {
+ mProgram = program;
+ mInputId = inputId;
+ TvSingletons singletons = TvSingletons.getSingletons(getContext());
+ mDvrDataManager = singletons.getDvrDataManager();
+ mDvrManager = singletons.getDvrManager();
+ mDvrScheduleManager = singletons.getDvrScheduleManager();
+ mScheduledRecording =
+ mDvrDataManager.getScheduledRecordingForProgramId(program.getId());
+ mBackgroundHelper = new DetailsViewBackgroundHelper(getActivity());
+ setupAdapter();
+ setDetailsOverviewRow(DetailsContent.createFromProgram(getContext(), mProgram));
+ mDvrDataManager.addScheduledRecordingListener(this);
+ mDvrScheduleManager.addOnConflictStateChangeListener(this);
+ return true;
+ }
+ return false;
+ }
+
+ private int getScheduleIconId() {
+ if (mDvrManager.isConflicting(mScheduledRecording)) {
+ return R.drawable.ic_warning_white_32dp;
+ } else {
+ return R.drawable.ic_schedule_32dp;
+ }
+ }
+
+ /** Creates actions users can interact with and their adaptor for this fragment. */
+ private SparseArrayObjectAdapter onCreateActionsAdapter() {
+ SparseArrayObjectAdapter adapter =
+ new SparseArrayObjectAdapter(new ActionPresenterSelector());
+ Resources res = getResources();
+ if (mScheduledRecording != null) {
+ adapter.set(
+ ACTION_VIEW_SCHEDULE,
+ new Action(
+ ACTION_VIEW_SCHEDULE,
+ res.getString(R.string.dvr_detail_view_schedule),
+ null,
+ res.getDrawable(getScheduleIconId())));
+ adapter.set(
+ ACTION_CANCEL,
+ new Action(
+ ACTION_CANCEL,
+ res.getString(R.string.dvr_detail_cancel_recording),
+ null,
+ res.getDrawable(R.drawable.ic_dvr_cancel_32dp)));
+ } else if (CommonFeatures.DVR.isEnabled(getActivity())
+ && mDvrManager.isProgramRecordable(mProgram)) {
+ adapter.set(
+ ACTION_SCHEDULE_RECORDING,
+ new Action(
+ ACTION_SCHEDULE_RECORDING,
+ res.getString(R.string.dvr_detail_schedule_recording),
+ null,
+ res.getDrawable(R.drawable.ic_schedule_32dp)));
+ }
+ return adapter;
+ }
+
+ /**
+ * Creates actions listeners to implement the behavior of the fragment after users click some
+ * action buttons.
+ */
+ private OnActionClickedListener onCreateOnActionClickedListener() {
+ return new OnActionClickedListener() {
+ @Override
+ public void onActionClicked(Action action) {
+ long actionId = action.getId();
+ if (actionId == ACTION_VIEW_SCHEDULE) {
+ DvrUiHelper.startSchedulesActivity(getContext(), mScheduledRecording);
+ } else if (actionId == ACTION_CANCEL) {
+ mDvrManager.removeScheduledRecording(mScheduledRecording);
+ } else if (actionId == ACTION_SCHEDULE_RECORDING) {
+ DvrUiHelper.checkStorageStatusAndShowErrorMessage(
+ getActivity(),
+ mInputId,
+ () ->
+ DvrUiHelper.requestRecordingFutureProgram(
+ getActivity(), mProgram, false));
+ }
+ }
+ };
+ }
+
+ /** 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()));
+ }
+ }
+
+ @Override
+ public void onScheduledRecordingAdded(ScheduledRecording... scheduledRecordings) {
+ for (ScheduledRecording recording : scheduledRecordings) {
+ if (recording.getProgramId() == mProgram.getId()) {
+ mScheduledRecording = recording;
+ updateActions();
+ return;
+ }
+ }
+ }
+
+ @Override
+ public void onScheduledRecordingRemoved(ScheduledRecording... scheduledRecordings) {
+ if (mScheduledRecording == null) {
+ return;
+ }
+ for (ScheduledRecording recording : scheduledRecordings) {
+ if (recording.getId() == mScheduledRecording.getId()) {
+ mScheduledRecording = null;
+ updateActions();
+ return;
+ }
+ }
+ }
+
+ @Override
+ public void onScheduledRecordingStatusChanged(ScheduledRecording... scheduledRecordings) {
+ if (mScheduledRecording == null) {
+ return;
+ }
+ for (ScheduledRecording recording : scheduledRecordings) {
+ if (recording.getId() == mScheduledRecording.getId()) {
+ mScheduledRecording = recording;
+ updateActions();
+ return;
+ }
+ }
+ }
+
+ @Override
+ public void onConflictStateChange(boolean conflict, ScheduledRecording... scheduledRecordings) {
+ onScheduledRecordingStatusChanged(scheduledRecordings);
+ }
+
+ private static class MyImageLoaderCallback
+ extends ImageLoader.ImageLoaderCallback<ProgramDetailsFragment> {
+ private final Context mContext;
+ private final int mLoadType;
+
+ public MyImageLoaderCallback(
+ ProgramDetailsFragment fragment, int loadType, Context context) {
+ super(fragment);
+ mLoadType = loadType;
+ mContext = context;
+ }
+
+ @Override
+ public void onBitmapLoaded(ProgramDetailsFragment 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/ui/TunableTvView.java b/src/com/android/tv/ui/TunableTvView.java
index bb98d974..5ac6bd83 100644
--- a/src/com/android/tv/ui/TunableTvView.java
+++ b/src/com/android/tv/ui/TunableTvView.java
@@ -20,11 +20,7 @@ import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.TimeInterpolator;
import android.app.Activity;
-import android.app.AlertDialog;
-import android.app.ApplicationErrorReport;
import android.content.Context;
-import android.content.DialogInterface;
-import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.graphics.Bitmap;
@@ -38,7 +34,6 @@ import android.media.tv.TvInputManager;
import android.media.tv.TvTrackInfo;
import android.media.tv.TvView;
import android.media.tv.TvView.OnUnhandledInputEventListener;
-import android.media.tv.TvView.TvInputCallback;
import android.net.ConnectivityManager;
import android.net.Uri;
import android.os.AsyncTask;
@@ -47,6 +42,7 @@ import android.os.Bundle;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.util.AttributeSet;
@@ -55,16 +51,17 @@ import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.SurfaceView;
import android.view.View;
+import android.view.accessibility.AccessibilityManager;
import android.widget.FrameLayout;
import android.widget.ImageView;
import com.android.tv.InputSessionManager;
import com.android.tv.InputSessionManager.TvViewSession;
import com.android.tv.R;
-import com.android.tv.TvFeatures;
import com.android.tv.TvSingletons;
import com.android.tv.analytics.Tracker;
-import com.android.tv.common.BuildConfig;
import com.android.tv.common.CommonConstants;
+import com.android.tv.common.compat.TvInputConstantCompat;
+import com.android.tv.common.compat.TvViewCompat.TvInputCallbackCompat;
import com.android.tv.common.feature.CommonFeatures;
import com.android.tv.common.util.CommonUtils;
import com.android.tv.common.util.Debug;
@@ -75,9 +72,11 @@ import com.android.tv.data.ProgramDataManager;
import com.android.tv.data.StreamInfo;
import com.android.tv.data.WatchedHistoryManager;
import com.android.tv.data.api.Channel;
+import com.android.tv.features.TvFeatures;
import com.android.tv.parental.ContentRatingsManager;
import com.android.tv.parental.ParentalControlSettings;
import com.android.tv.recommendation.NotificationService;
+import com.android.tv.ui.api.TunableTvViewPlayingApi;
import com.android.tv.util.NetworkUtils;
import com.android.tv.util.TvInputManagerHelper;
import com.android.tv.util.Utils;
@@ -95,8 +94,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV
public static final int VIDEO_UNAVAILABLE_REASON_NO_RESOURCE = -2;
public static final int VIDEO_UNAVAILABLE_REASON_SCREEN_BLOCKED = -3;
public static final int VIDEO_UNAVAILABLE_REASON_NONE = -100;
-
- private OnTalkBackDpadKeyListener mOnTalkBackDpadKeyListener;
+ private final AccessibilityManager mAccessibilityManager;
@Retention(RetentionPolicy.SOURCE)
@IntDef({BLOCK_SCREEN_TYPE_NO_UI, BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW, BLOCK_SCREEN_TYPE_NORMAL})
@@ -132,7 +130,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV
private AppLayerTvView mTvView;
private TvViewSession mTvViewSession;
- private Channel mCurrentChannel;
+ @Nullable private Channel mCurrentChannel;
private TvInputManagerHelper mInputManagerHelper;
private ContentRatingsManager mContentRatingsManager;
private ParentalControlSettings mParentalControlSettings;
@@ -190,8 +188,10 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV
private final ConnectivityManager mConnectivityManager;
private final InputSessionManager mInputSessionManager;
- private final TvInputCallback mCallback =
- new TvInputCallback() {
+ private int mChannelSignalStrength;
+
+ private final TvInputCallbackCompat mCallback =
+ new TvInputCallbackCompat() {
@Override
public void onConnectionFailed(String inputId) {
Log.w(TAG, "Failed to bind an input");
@@ -252,7 +252,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV
}
}
if (mOnTuneListener != null) {
- mOnTuneListener.onStreamInfoChanged(TunableTvView.this);
+ mOnTuneListener.onStreamInfoChanged(TunableTvView.this, true);
}
}
@@ -305,7 +305,10 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV
}
}
if (mOnTuneListener != null) {
- mOnTuneListener.onStreamInfoChanged(TunableTvView.this);
+ // should not change audio track automatically when an audio track or a
+ // subtitle track is selected
+ mOnTuneListener.onStreamInfoChanged(
+ TunableTvView.this, type == TvTrackInfo.TYPE_VIDEO);
}
}
@@ -316,60 +319,15 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV
.log(
"Start up of Live TV ends,"
+ " TunableTvView.onVideoAvailable resets timer");
- long startUpDurationTime = Debug.getTimer(Debug.TAG_START_UP_TIMER).reset();
+ Debug.getTimer(Debug.TAG_START_UP_TIMER).reset();
Debug.removeTimer(Debug.TAG_START_UP_TIMER);
- if (BuildConfig.ENG
- && startUpDurationTime > Debug.TIME_START_UP_DURATION_THRESHOLD) {
- showAlertDialogForLongStartUp();
- }
mVideoUnavailableReason = VIDEO_UNAVAILABLE_REASON_NONE;
updateBlockScreenAndMuting();
if (mOnTuneListener != null) {
- mOnTuneListener.onStreamInfoChanged(TunableTvView.this);
+ mOnTuneListener.onStreamInfoChanged(TunableTvView.this, true);
}
}
- 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.cancel, null)
- .show();
- }
-
@Override
public void onVideoUnavailable(String inputId, int reason) {
if (reason != TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING
@@ -390,12 +348,13 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV
}
updateBlockScreenAndMuting();
if (mOnTuneListener != null) {
- mOnTuneListener.onStreamInfoChanged(TunableTvView.this);
+ mOnTuneListener.onStreamInfoChanged(TunableTvView.this, true);
}
switch (reason) {
case TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN:
case TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING:
case TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL:
+ case CommonConstants.VIDEO_UNAVAILABLE_REASON_NOT_CONNECTED:
mTracker.sendChannelVideoUnavailable(mCurrentChannel, reason);
break;
default:
@@ -441,6 +400,14 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV
boolean available = status == TvInputManager.TIME_SHIFT_STATUS_AVAILABLE;
setTimeShiftAvailable(available);
}
+
+ @Override
+ public void onSignalStrength(String inputId, int value) {
+ mChannelSignalStrength = value;
+ if (mOnTuneListener != null) {
+ mOnTuneListener.onChannelSignalStrength();
+ }
+ }
};
public TunableTvView(Context context) {
@@ -502,35 +469,12 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV
}
}
});
- View placeholder = findViewById(R.id.placeholder);
- placeholder.requestFocus();
- findViewById(R.id.channel_up)
- .setOnFocusChangeListener(
- (v, hasFocus) -> {
- if (hasFocus) {
- placeholder.requestFocus();
- if (mOnTalkBackDpadKeyListener != null) {
- mOnTalkBackDpadKeyListener.onTalkBackDpadKey(
- KeyEvent.KEYCODE_DPAD_UP);
- }
- }
- });
- findViewById(R.id.channel_down)
- .setOnFocusChangeListener(
- (v, hasFocus) -> {
- if (hasFocus) {
- placeholder.requestFocus();
- if (mOnTalkBackDpadKeyListener != null) {
- mOnTalkBackDpadKeyListener.onTalkBackDpadKey(
- KeyEvent.KEYCODE_DPAD_DOWN);
- }
- }
- });
+ mAccessibilityManager = context.getSystemService(AccessibilityManager.class);
}
public void initialize(
ProgramDataManager programDataManager, TvInputManagerHelper tvInputManagerHelper) {
- mTvView = (AppLayerTvView) findViewById(R.id.tv_view);
+ mTvView = findViewById(R.id.tv_view);
mProgramDataManager = programDataManager;
mInputManagerHelper = tvInputManagerHelper;
mContentRatingsManager = tvInputManagerHelper.getContentRatingsManager();
@@ -621,6 +565,14 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV
mIsUnderShrunken = isUnderShrunken;
}
+ public int getChannelSignalStrength() {
+ return mChannelSignalStrength;
+ }
+
+ public void resetChannelSignalStrength() {
+ mChannelSignalStrength = TvInputConstantCompat.SIGNAL_STRENGTH_NOT_USED;
+ }
+
@Override
public boolean isPlaying() {
return mStarted;
@@ -714,12 +666,13 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV
}
updateBlockScreenAndMuting();
if (mOnTuneListener != null) {
- mOnTuneListener.onStreamInfoChanged(this);
+ mOnTuneListener.onStreamInfoChanged(this, true);
}
return true;
}
@Override
+ @Nullable
public Channel getCurrentChannel() {
return mCurrentChannel;
}
@@ -795,13 +748,15 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV
void onUnexpectedStop(Channel channel);
- void onStreamInfoChanged(StreamInfo info);
+ void onStreamInfoChanged(StreamInfo info, boolean allowAutoSelectionOfTrack);
void onChannelRetuned(Uri channel);
void onContentBlocked();
void onContentAllowed();
+
+ void onChannelSignalStrength();
}
public void unblockContent(TvContentRating rating) {
@@ -869,14 +824,15 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV
mTvView.setOnUnhandledInputEventListener(listener);
}
- public void setOnTalkBackDpadKeyListener(OnTalkBackDpadKeyListener listener) {
- mOnTalkBackDpadKeyListener = listener;
- }
-
public void setClosedCaptionEnabled(boolean enabled) {
mTvView.setCaptionEnabled(enabled);
}
+ @VisibleForTesting
+ public void setOnTuneListener(OnTuneListener listener) {
+ mOnTuneListener = listener;
+ }
+
public List<TvTrackInfo> getTracks(int type) {
return mTvView.getTracks(type);
}
@@ -1044,6 +1000,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV
if (text != null) {
mBlockScreenView.setInfoText(text);
}
+ mBlockScreenView.setInfoTextClickable(mScreenBlocked && mParentControlEnabled);
}
/**
@@ -1053,6 +1010,8 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV
private String getBlockScreenText() {
// TODO: add a test for this method
Resources res = getResources();
+ boolean isA11y = mAccessibilityManager.isEnabled();
+
if (mScreenBlocked && mParentControlEnabled) {
switch (mBlockScreenType) {
case BLOCK_SCREEN_TYPE_NO_UI:
@@ -1060,7 +1019,10 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV
return "";
case BLOCK_SCREEN_TYPE_NORMAL:
if (mCanModifyParentalControls) {
- return res.getString(R.string.tvview_channel_locked);
+ return res.getString(
+ isA11y
+ ? R.string.tvview_channel_locked_talkback
+ : R.string.tvview_channel_locked);
} else {
return res.getString(R.string.tvview_channel_locked_no_permission);
}
@@ -1081,15 +1043,26 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV
case BLOCK_SCREEN_TYPE_NORMAL:
if (TextUtils.isEmpty(name)) {
if (mCanModifyParentalControls) {
- return res.getString(R.string.tvview_content_locked);
+ return res.getString(
+ isA11y
+ ? R.string.tvview_content_locked_talkback
+ : R.string.tvview_content_locked);
} else {
return res.getString(R.string.tvview_content_locked_no_permission);
}
} else {
if (mCanModifyParentalControls) {
return name.equals(res.getString(R.string.unrated_rating_name))
- ? res.getString(R.string.tvview_content_locked_unrated)
- : res.getString(R.string.tvview_content_locked_format, name);
+ ? res.getString(
+ isA11y
+ ? R.string
+ .tvview_content_locked_unrated_talkback
+ : R.string.tvview_content_locked_unrated)
+ : res.getString(
+ isA11y
+ ? R.string.tvview_content_locked_format_talkback
+ : R.string.tvview_content_locked_format,
+ name);
} else {
return name.equals(res.getString(R.string.unrated_rating_name))
? res.getString(
@@ -1106,6 +1079,8 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV
return res.getString(R.string.tvview_msg_audio_only);
case TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL:
return res.getString(R.string.tvview_msg_weak_signal);
+ case CommonConstants.VIDEO_UNAVAILABLE_REASON_NOT_CONNECTED:
+ return res.getString(R.string.msg_channel_unavailable_not_connected);
case VIDEO_UNAVAILABLE_REASON_NO_RESOURCE:
return getTuneConflictMessage();
default:
@@ -1122,7 +1097,9 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV
&& (mScreenBlocked
|| mBlockedContentRating != null
|| mVideoUnavailableReason
- == TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN)) {
+ == TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN
+ || mVideoUnavailableReason
+ == CommonConstants.VIDEO_UNAVAILABLE_REASON_NOT_CONNECTED)) {
((Activity) getContext()).finish();
return true;
}
@@ -1237,20 +1214,11 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV
.setDuration(durationMillis)
.setInterpolator(interpolator)
.withStartAction(
- new Runnable() {
- @Override
- public void run() {
- mFadeState = FADING_OUT;
- mActionAfterFade = actionAfterFade;
- }
+ () -> {
+ mFadeState = FADING_OUT;
+ mActionAfterFade = actionAfterFade;
})
- .withEndAction(
- new Runnable() {
- @Override
- public void run() {
- mFadeState = FADED_OUT;
- }
- });
+ .withEndAction(() -> mFadeState = FADED_OUT);
}
/** Fade in this TunableTvView. Fade in by decreasing the dimming. */
@@ -1264,20 +1232,14 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV
.setDuration(durationMillis)
.setInterpolator(interpolator)
.withStartAction(
- new Runnable() {
- @Override
- public void run() {
- mFadeState = FADING_IN;
- mActionAfterFade = actionAfterFade;
- }
+ () -> {
+ mFadeState = FADING_IN;
+ mActionAfterFade = actionAfterFade;
})
.withEndAction(
- new Runnable() {
- @Override
- public void run() {
- mFadeState = FADED_IN;
- mDimScreenView.setVisibility(View.GONE);
- }
+ () -> {
+ mFadeState = FADED_IN;
+ mDimScreenView.setVisibility(View.GONE);
});
}
@@ -1298,6 +1260,10 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV
mTimeShiftListener = listener;
}
+ public void setBlockedInfoOnClickListener(@Nullable OnClickListener onClickListener) {
+ mBlockScreenView.setInfoTextOnClickListener(onClickListener);
+ }
+
private void setTimeShiftAvailable(boolean isTimeShiftAvailable) {
if (mTimeShiftAvailable == isTimeShiftAvailable) {
return;
@@ -1336,7 +1302,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV
/** Plays the media, if the current input supports time-shifting. */
@Override
- public void timeshiftPlay() {
+ public void timeShiftPlay() {
if (!isTimeShiftAvailable()) {
throw new IllegalStateException("Time-shift is not supported for the current channel");
}
@@ -1348,7 +1314,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV
/** Pauses the media, if the current input supports time-shifting. */
@Override
- public void timeshiftPause() {
+ public void timeShiftPause() {
if (!isTimeShiftAvailable()) {
throw new IllegalStateException("Time-shift is not supported for the current channel");
}
@@ -1364,7 +1330,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV
* @param speed The speed to rewind the media. e.g. 2 for 2x, 3 for 3x and 4 for 4x.
*/
@Override
- public void timeshiftRewind(int speed) {
+ public void timeShiftRewind(int speed) {
if (!isTimeShiftAvailable()) {
throw new IllegalStateException("Time-shift is not supported for the current channel");
} else {
@@ -1384,7 +1350,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV
* @param speed The speed to forward the media. e.g. 2 for 2x, 3 for 3x and 4 for 4x.
*/
@Override
- public void timeshiftFastForward(int speed) {
+ public void timeShiftFastForward(int speed) {
if (!isTimeShiftAvailable()) {
throw new IllegalStateException("Time-shift is not supported for the current channel");
} else {
@@ -1404,7 +1370,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV
* @param timeMs The time in milliseconds to seek to.
*/
@Override
- public void timeshiftSeekTo(long timeMs) {
+ public void timeShiftSeekTo(long timeMs) {
if (!isTimeShiftAvailable()) {
throw new IllegalStateException("Time-shift is not supported for the current channel");
}
@@ -1413,14 +1379,14 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV
/** Returns the current playback position in milliseconds. */
@Override
- public long timeshiftGetCurrentPositionMs() {
+ public long timeShiftGetCurrentPositionMs() {
if (!isTimeShiftAvailable()) {
throw new IllegalStateException("Time-shift is not supported for the current channel");
}
if (DEBUG) {
Log.d(
TAG,
- "timeshiftGetCurrentPositionMs: current position ="
+ "timeShiftGetCurrentPositionMs: current position ="
+ Utils.toTimeString(mTimeShiftCurrentPositionMs));
}
return mTimeShiftCurrentPositionMs;
@@ -1446,12 +1412,6 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV
};
}
- /** Listens for dpad actions that are otherwise trapped by talkback */
- public interface OnTalkBackDpadKeyListener {
-
- void onTalkBackDpadKey(int keycode);
- }
-
/** A listener which receives the notification when the screen is blocked/unblocked. */
public abstract static class OnScreenBlockingChangedListener {
/** Called when the screen is blocked/unblocked. */
diff --git a/src/com/android/tv/ui/TvOverlayManager.java b/src/com/android/tv/ui/TvOverlayManager.java
index 222fcb3a..b2854a1f 100644
--- a/src/com/android/tv/ui/TvOverlayManager.java
+++ b/src/com/android/tv/ui/TvOverlayManager.java
@@ -86,19 +86,18 @@ public class TvOverlayManager implements AccessibilityStateChangeListener {
@Retention(RetentionPolicy.SOURCE)
@IntDef(
- flag = true,
- value = {
- FLAG_HIDE_OVERLAYS_DEFAULT,
- FLAG_HIDE_OVERLAYS_WITHOUT_ANIMATION,
- FLAG_HIDE_OVERLAYS_KEEP_SCENE,
- FLAG_HIDE_OVERLAYS_KEEP_DIALOG,
- FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS,
- FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANEL_HISTORY,
- FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE,
- FLAG_HIDE_OVERLAYS_KEEP_MENU,
- FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT
- }
- )
+ flag = true,
+ value = {
+ FLAG_HIDE_OVERLAYS_DEFAULT,
+ FLAG_HIDE_OVERLAYS_WITHOUT_ANIMATION,
+ FLAG_HIDE_OVERLAYS_KEEP_SCENE,
+ FLAG_HIDE_OVERLAYS_KEEP_DIALOG,
+ FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS,
+ FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANEL_HISTORY,
+ FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE,
+ FLAG_HIDE_OVERLAYS_KEEP_MENU,
+ FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT
+ })
private @interface HideOverlayFlag {}
// FLAG_HIDE_OVERLAYs must be bitwise exclusive.
public static final int FLAG_HIDE_OVERLAYS_DEFAULT = 0b000000000;
@@ -115,20 +114,19 @@ public class TvOverlayManager implements AccessibilityStateChangeListener {
@Retention(RetentionPolicy.SOURCE)
@IntDef(
- flag = true,
- value = {
- OVERLAY_TYPE_NONE,
- OVERLAY_TYPE_MENU,
- OVERLAY_TYPE_SIDE_FRAGMENT,
- OVERLAY_TYPE_DIALOG,
- OVERLAY_TYPE_GUIDE,
- OVERLAY_TYPE_SCENE_CHANNEL_BANNER,
- OVERLAY_TYPE_SCENE_INPUT_BANNER,
- OVERLAY_TYPE_SCENE_KEYPAD_CHANNEL_SWITCH,
- OVERLAY_TYPE_SCENE_SELECT_INPUT,
- OVERLAY_TYPE_FRAGMENT
- }
- )
+ flag = true,
+ value = {
+ OVERLAY_TYPE_NONE,
+ OVERLAY_TYPE_MENU,
+ OVERLAY_TYPE_SIDE_FRAGMENT,
+ OVERLAY_TYPE_DIALOG,
+ OVERLAY_TYPE_GUIDE,
+ OVERLAY_TYPE_SCENE_CHANNEL_BANNER,
+ OVERLAY_TYPE_SCENE_INPUT_BANNER,
+ OVERLAY_TYPE_SCENE_KEYPAD_CHANNEL_SWITCH,
+ OVERLAY_TYPE_SCENE_SELECT_INPUT,
+ OVERLAY_TYPE_FRAGMENT
+ })
private @interface TvOverlayType {}
// OVERLAY_TYPEs must be bitwise exclusive.
/** The overlay type which indicates that there are no overlays. */
@@ -176,6 +174,8 @@ public class TvOverlayManager implements AccessibilityStateChangeListener {
public static final int UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK = 5;
/** Updates channel banner because of stream info updating. */
public static final int UPDATE_CHANNEL_BANNER_REASON_UPDATE_STREAM_INFO = 6;
+ /** Updates channel banner because of channel signal updating. */
+ public static final int UPDATE_CHANNEL_BANNER_REASON_UPDATE_SIGNAL_STRENGTH = 7;
private static final String FRAGMENT_TAG_SETUP_SOURCES = "tag_setup_sources";
private static final String FRAGMENT_TAG_NEW_SOURCES = "tag_new_sources";
@@ -287,35 +287,17 @@ public class TvOverlayManager implements AccessibilityStateChangeListener {
mSideFragmentManager =
new SideFragmentManager(
mainActivity,
- new Runnable() {
- @Override
- public void run() {
- onOverlayOpened(OVERLAY_TYPE_SIDE_FRAGMENT);
- hideOverlays(FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS);
- }
+ () -> {
+ onOverlayOpened(OVERLAY_TYPE_SIDE_FRAGMENT);
+ hideOverlays(FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS);
},
- new Runnable() {
- @Override
- public void run() {
- showChannelBannerIfHiddenBySideFragment();
- onOverlayClosed(OVERLAY_TYPE_SIDE_FRAGMENT);
- }
+ () -> {
+ showChannelBannerIfHiddenBySideFragment();
+ onOverlayClosed(OVERLAY_TYPE_SIDE_FRAGMENT);
});
// Program Guide
- Runnable preShowRunnable =
- new Runnable() {
- @Override
- public void run() {
- onOverlayOpened(OVERLAY_TYPE_GUIDE);
- }
- };
- Runnable postHideRunnable =
- new Runnable() {
- @Override
- public void run() {
- onOverlayClosed(OVERLAY_TYPE_GUIDE);
- }
- };
+ Runnable preShowRunnable = () -> onOverlayOpened(OVERLAY_TYPE_GUIDE);
+ Runnable postHideRunnable = () -> onOverlayClosed(OVERLAY_TYPE_GUIDE);
DvrDataManager dvrDataManager =
CommonFeatures.DVR.isEnabled(mainActivity) ? singletons.getDvrDataManager() : null;
mProgramGuide =
@@ -520,16 +502,13 @@ public class TvOverlayManager implements AccessibilityStateChangeListener {
hideOverlays(FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT);
onOverlayOpened(OVERLAY_TYPE_FRAGMENT);
runAfterSideFragmentsAreClosed(
- new Runnable() {
- @Override
- public void run() {
- if (DEBUG) Log.d(TAG, "showFragment(" + fragment + ")");
- mMainActivity
- .getFragmentManager()
- .beginTransaction()
- .replace(R.id.fragment_container, fragment, tag)
- .commit();
- }
+ () -> {
+ if (DEBUG) Log.d(TAG, "showFragment(" + fragment + ")");
+ mMainActivity
+ .getFragmentManager()
+ .beginTransaction()
+ .replace(R.id.fragment_container, fragment, tag)
+ .commit();
});
}
@@ -678,12 +657,7 @@ public class TvOverlayManager implements AccessibilityStateChangeListener {
/** Shows the program guide. */
public void showProgramGuide() {
mProgramGuide.show(
- new Runnable() {
- @Override
- public void run() {
- hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE);
- }
- });
+ () -> hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE));
}
/**
@@ -855,6 +829,10 @@ public class TvOverlayManager implements AccessibilityStateChangeListener {
&& lockType != ChannelBannerView.LOCK_PROGRAM_DETAIL) {
mChannelBannerView.updateViews(false);
}
+ } else if (CommonFeatures.TUNER_SIGNAL_STRENGTH.isEnabled(mMainActivity)
+ && reason == UPDATE_CHANNEL_BANNER_REASON_UPDATE_SIGNAL_STRENGTH) {
+ mChannelBannerView.updateChannelSignalStrengthView(
+ mTvView.getChannelSignalStrength());
} else {
mChannelBannerView.updateViews(
reason == UPDATE_CHANNEL_BANNER_REASON_TUNE
diff --git a/src/com/android/tv/ui/TvTransitionManager.java b/src/com/android/tv/ui/TvTransitionManager.java
index 5af3e6f2..f60337f1 100644
--- a/src/com/android/tv/ui/TvTransitionManager.java
+++ b/src/com/android/tv/ui/TvTransitionManager.java
@@ -174,28 +174,19 @@ public class TvTransitionManager extends TransitionManager {
mEmptyScene = new Scene(mSceneContainer, (View) mEmptyView);
mEmptyScene.setEnterAction(
- new Runnable() {
- @Override
- public void run() {
- FrameLayout.LayoutParams emptySceneLayoutParams =
- (FrameLayout.LayoutParams) mEmptyView.getLayoutParams();
- ViewGroup.MarginLayoutParams lp =
- (ViewGroup.MarginLayoutParams) mCurrentSceneView.getLayoutParams();
- emptySceneLayoutParams.topMargin = mCurrentSceneView.getTop();
- emptySceneLayoutParams.setMarginStart(lp.getMarginStart());
- emptySceneLayoutParams.height = mCurrentSceneView.getHeight();
- emptySceneLayoutParams.width = mCurrentSceneView.getWidth();
- mEmptyView.setLayoutParams(emptySceneLayoutParams);
- setCurrentScene(mEmptyScene, mEmptyView);
- }
- });
- mEmptyScene.setExitAction(
- new Runnable() {
- @Override
- public void run() {
- removeAllViewsFromOverlay();
- }
+ () -> {
+ FrameLayout.LayoutParams emptySceneLayoutParams =
+ (FrameLayout.LayoutParams) mEmptyView.getLayoutParams();
+ ViewGroup.MarginLayoutParams lp =
+ (ViewGroup.MarginLayoutParams) mCurrentSceneView.getLayoutParams();
+ emptySceneLayoutParams.topMargin = mCurrentSceneView.getTop();
+ emptySceneLayoutParams.setMarginStart(lp.getMarginStart());
+ emptySceneLayoutParams.height = mCurrentSceneView.getHeight();
+ emptySceneLayoutParams.width = mCurrentSceneView.getWidth();
+ mEmptyView.setLayoutParams(emptySceneLayoutParams);
+ setCurrentScene(mEmptyScene, mEmptyView);
});
+ mEmptyScene.setExitAction(this::removeAllViewsFromOverlay);
mChannelBannerScene = buildScene(mSceneContainer, mChannelBannerView);
mInputBannerScene = buildScene(mSceneContainer, mInputBannerView);
@@ -274,21 +265,15 @@ public class TvTransitionManager extends TransitionManager {
private Scene buildScene(ViewGroup sceneRoot, final TransitionLayout layout) {
final Scene scene = new Scene(sceneRoot, (View) layout);
scene.setEnterAction(
- new Runnable() {
- @Override
- public void run() {
- boolean wasEmptyScene = (mCurrentScene == mEmptyScene);
- setCurrentScene(scene, (ViewGroup) layout);
- layout.onEnterAction(wasEmptyScene);
- }
+ () -> {
+ boolean wasEmptyScene = (mCurrentScene == mEmptyScene);
+ setCurrentScene(scene, (ViewGroup) layout);
+ layout.onEnterAction(wasEmptyScene);
});
scene.setExitAction(
- new Runnable() {
- @Override
- public void run() {
- removeAllViewsFromOverlay();
- layout.onExitAction();
- }
+ () -> {
+ removeAllViewsFromOverlay();
+ layout.onExitAction();
});
return scene;
}
diff --git a/src/com/android/tv/ui/TvViewUiManager.java b/src/com/android/tv/ui/TvViewUiManager.java
index 7e354db3..b7e8b433 100644
--- a/src/com/android/tv/ui/TvViewUiManager.java
+++ b/src/com/android/tv/ui/TvViewUiManager.java
@@ -43,9 +43,9 @@ import android.view.ViewGroup.MarginLayoutParams;
import android.view.animation.AnimationUtils;
import android.widget.FrameLayout;
import com.android.tv.R;
-import com.android.tv.TvFeatures;
import com.android.tv.TvOptionsManager;
import com.android.tv.data.DisplayMode;
+import com.android.tv.features.TvFeatures;
import com.android.tv.util.TvSettings;
/**
@@ -460,12 +460,7 @@ public class TvViewUiManager {
return;
}
mHandler.post(
- new Runnable() {
- @Override
- public void run() {
- setTvViewPosition(mTvViewLayoutParams, mTvViewFrame, false);
- }
- });
+ () -> setTvViewPosition(mTvViewLayoutParams, mTvViewFrame, false));
}
});
mTvViewAnimator.addUpdateListener(
@@ -496,13 +491,7 @@ public class TvViewUiManager {
new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
- mHandler.post(
- new Runnable() {
- @Override
- public void run() {
- mContentView.setBackgroundColor(mBackgroundColor);
- }
- });
+ mHandler.post(() -> mContentView.setBackgroundColor(mBackgroundColor));
}
});
}
diff --git a/src/com/android/tv/ui/TunableTvViewPlayingApi.java b/src/com/android/tv/ui/api/TunableTvViewPlayingApi.java
index 3f19b61f..eb1f030d 100644
--- a/src/com/android/tv/ui/TunableTvViewPlayingApi.java
+++ b/src/com/android/tv/ui/api/TunableTvViewPlayingApi.java
@@ -14,7 +14,7 @@
* limitations under the License
*/
-package com.android.tv.ui;
+package com.android.tv.ui.api;
/** API to play pause and set the volume of a TunableTvView */
public interface TunableTvViewPlayingApi {
@@ -27,17 +27,17 @@ public interface TunableTvViewPlayingApi {
boolean isTimeShiftAvailable();
- void timeshiftPlay();
+ void timeShiftPlay();
- void timeshiftPause();
+ void timeShiftPause();
- void timeshiftRewind(int speed);
+ void timeShiftRewind(int speed);
- void timeshiftFastForward(int speed);
+ void timeShiftFastForward(int speed);
- void timeshiftSeekTo(long timeMs);
+ void timeShiftSeekTo(long timeMs);
- long timeshiftGetCurrentPositionMs();
+ long timeShiftGetCurrentPositionMs();
/** Used to receive the time-shift events. */
abstract class TimeShiftListener {
diff --git a/src/com/android/tv/ui/hideable/AutoHideScheduler.java b/src/com/android/tv/ui/hideable/AutoHideScheduler.java
index 75859792..8bf70de1 100644
--- a/src/com/android/tv/ui/hideable/AutoHideScheduler.java
+++ b/src/com/android/tv/ui/hideable/AutoHideScheduler.java
@@ -1,3 +1,18 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
package com.android.tv.ui.hideable;
import android.content.Context;
diff --git a/src/com/android/tv/ui/sidepanel/CustomizeChannelListFragment.java b/src/com/android/tv/ui/sidepanel/CustomizeChannelListFragment.java
index 48b80723..62130b64 100644
--- a/src/com/android/tv/ui/sidepanel/CustomizeChannelListFragment.java
+++ b/src/com/android/tv/ui/sidepanel/CustomizeChannelListFragment.java
@@ -37,7 +37,6 @@ import com.android.tv.util.TvInputManagerHelper;
import com.android.tv.util.Utils;
import java.util.ArrayList;
import java.util.Collections;
-import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
@@ -213,17 +212,14 @@ public class CustomizeChannelListFragment extends SideFragment {
ArrayList<Channel> channels = new ArrayList<>(mChannels);
Collections.sort(
channels,
- new Comparator<Channel>() {
- @Override
- public int compare(Channel lhs, Channel rhs) {
- boolean lhsHd = isHdChannel(lhs);
- boolean rhsHd = isHdChannel(rhs);
- if (lhsHd == rhsHd) {
- return ChannelNumber.compare(
- lhs.getDisplayNumber(), rhs.getDisplayNumber());
- } else {
- return lhsHd ? -1 : 1;
- }
+ (Channel lhs, Channel rhs) -> {
+ boolean lhsHd = isHdChannel(lhs);
+ boolean rhsHd = isHdChannel(rhs);
+ if (lhsHd == rhsHd) {
+ return ChannelNumber.compare(
+ lhs.getDisplayNumber(), rhs.getDisplayNumber());
+ } else {
+ return lhsHd ? -1 : 1;
}
});
diff --git a/src/com/android/tv/ui/sidepanel/MultiAudioFragment.java b/src/com/android/tv/ui/sidepanel/MultiAudioFragment.java
index 03b71c8c..7a65247f 100644
--- a/src/com/android/tv/ui/sidepanel/MultiAudioFragment.java
+++ b/src/com/android/tv/ui/sidepanel/MultiAudioFragment.java
@@ -20,7 +20,7 @@ import android.media.tv.TvTrackInfo;
import android.text.TextUtils;
import android.view.KeyEvent;
import com.android.tv.R;
-import com.android.tv.util.Utils;
+import com.android.tv.util.TvTrackInfoUtils;
import java.util.ArrayList;
import java.util.List;
@@ -51,12 +51,13 @@ public class MultiAudioFragment extends SideFragment {
List<Item> items = new ArrayList<>();
if (tracks != null) {
- boolean needToShowSampleRate = Utils.needToShowSampleRate(getActivity(), tracks);
+ boolean needToShowSampleRate = TvTrackInfoUtils
+ .needToShowSampleRate(getActivity(), tracks);
int pos = 0;
for (final TvTrackInfo track : tracks) {
RadioButtonItem item =
new MultiAudioOptionItem(
- Utils.getMultiAudioString(
+ TvTrackInfoUtils.getMultiAudioString(
getActivity(), track, needToShowSampleRate),
track.getId());
if (track.getId().equals(mSelectedTrackId)) {
diff --git a/src/com/android/tv/ui/sidepanel/SettingsFragment.java b/src/com/android/tv/ui/sidepanel/SettingsFragment.java
index 31d00fa6..aa71fb75 100644
--- a/src/com/android/tv/ui/sidepanel/SettingsFragment.java
+++ b/src/com/android/tv/ui/sidepanel/SettingsFragment.java
@@ -16,8 +16,6 @@
package com.android.tv.ui.sidepanel;
-import static com.android.tv.TvFeatures.TUNER;
-
import android.app.ApplicationErrorReport;
import android.content.Intent;
import android.media.tv.TvInputInfo;
@@ -81,10 +79,9 @@ public class SettingsFragment extends SideFragment {
customizeChannelListItem.setEnabled(false);
items.add(customizeChannelListItem);
final MainActivity activity = getMainActivity();
+ TvSingletons singletons = TvSingletons.getSingletons(getContext());
boolean hasNewInput =
- TvSingletons.getSingletons(getContext())
- .getSetupUtils()
- .hasNewInput(activity.getTvInputManagerHelper());
+ singletons.getSetupUtils().hasNewInput(activity.getTvInputManagerHelper());
items.add(
new ActionItem(
getString(R.string.settings_channel_source_item_setup),
@@ -127,11 +124,9 @@ public class SettingsFragment extends SideFragment {
// It's TBD.
}
boolean showTrickplaySetting = false;
- if (TUNER.isEnabled(getContext())) {
+ if (singletons.getBuiltInTunerManager().isPresent()) {
for (TvInputInfo inputInfo :
- TvSingletons.getSingletons(getContext())
- .getTvInputManagerHelper()
- .getTvInputInfos(true, true)) {
+ singletons.getTvInputManagerHelper().getTvInputInfos(true, true)) {
if (Utils.isInternalTvInput(getContext(), inputInfo.getId())) {
showTrickplaySetting = true;
break;
diff --git a/src/com/android/tv/ui/sidepanel/SideFragment.java b/src/com/android/tv/ui/sidepanel/SideFragment.java
index 2902ea7f..590f1300 100644
--- a/src/com/android/tv/ui/sidepanel/SideFragment.java
+++ b/src/com/android/tv/ui/sidepanel/SideFragment.java
@@ -342,12 +342,9 @@ public abstract class SideFragment<T extends Item> extends Fragment implements H
}
if (view.getBackground() instanceof RippleDrawable) {
view.postDelayed(
- new Runnable() {
- @Override
- public void run() {
- if (mItem != null) {
- mItem.onSelected();
- }
+ () -> {
+ if (mItem != null) {
+ mItem.onSelected();
}
},
view.getResources().getInteger(R.integer.side_panel_ripple_anim_duration));
diff --git a/src/com/android/tv/ui/sidepanel/parentalcontrols/ChannelsBlockedFragment.java b/src/com/android/tv/ui/sidepanel/parentalcontrols/ChannelsBlockedFragment.java
index 4e3cf7fb..b14bf78d 100644
--- a/src/com/android/tv/ui/sidepanel/parentalcontrols/ChannelsBlockedFragment.java
+++ b/src/com/android/tv/ui/sidepanel/parentalcontrols/ChannelsBlockedFragment.java
@@ -41,7 +41,6 @@ import com.android.tv.ui.sidepanel.Item;
import com.android.tv.ui.sidepanel.SideFragment;
import java.util.ArrayList;
import java.util.Collections;
-import java.util.Comparator;
import java.util.List;
public class ChannelsBlockedFragment extends SideFragment {
@@ -132,15 +131,11 @@ public class ChannelsBlockedFragment extends SideFragment {
mChannels.addAll(getChannelDataManager().getChannelList());
Collections.sort(
mChannels,
- new Comparator<Channel>() {
- @Override
- public int compare(Channel lhs, Channel rhs) {
- if (lhs.isBrowsable() != rhs.isBrowsable()) {
- return lhs.isBrowsable() ? -1 : 1;
- }
- return ChannelNumber.compare(
- lhs.getDisplayNumber(), rhs.getDisplayNumber());
+ (Channel lhs, Channel rhs) -> {
+ if (lhs.isBrowsable() != rhs.isBrowsable()) {
+ return lhs.isBrowsable() ? -1 : 1;
}
+ return ChannelNumber.compare(lhs.getDisplayNumber(), rhs.getDisplayNumber());
});
final long currentChannelId = getMainActivity().getCurrentChannelId();
diff --git a/src/com/android/tv/ui/sidepanel/parentalcontrols/RatingsFragment.java b/src/com/android/tv/ui/sidepanel/parentalcontrols/RatingsFragment.java
index 128fcd1a..d1ae4423 100644
--- a/src/com/android/tv/ui/sidepanel/parentalcontrols/RatingsFragment.java
+++ b/src/com/android/tv/ui/sidepanel/parentalcontrols/RatingsFragment.java
@@ -39,6 +39,7 @@ import com.android.tv.ui.sidepanel.RadioButtonItem;
import com.android.tv.ui.sidepanel.SideFragment;
import com.android.tv.util.TvSettings;
import com.android.tv.util.TvSettings.ContentRatingLevel;
+import com.google.common.collect.ImmutableList;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -167,7 +168,7 @@ public class RatingsFragment extends SideFragment {
super.onUpdate();
setChecked(
mParentalControlSettings.isRatingBlocked(
- new TvContentRating[] {TvContentRating.UNRATED}));
+ ImmutableList.of(TvContentRating.UNRATED)));
}
@Override
@@ -239,7 +240,7 @@ public class RatingsFragment extends SideFragment {
// set checked if UNRATED is blocked, and set unchecked otherwise.
mBlockUnratedItem.setChecked(
mParentalControlSettings.isRatingBlocked(
- new TvContentRating[] {TvContentRating.UNRATED}));
+ ImmutableList.of(TvContentRating.UNRATED)));
}
notifyItemsChanged(mRatingLevelItems.size());
}
diff --git a/src/com/android/tv/util/AsyncDbTask.java b/src/com/android/tv/util/AsyncDbTask.java
index 60fa3018..b3523952 100644
--- a/src/com/android/tv/util/AsyncDbTask.java
+++ b/src/com/android/tv/util/AsyncDbTask.java
@@ -17,6 +17,7 @@
package com.android.tv.util;
import android.content.ContentResolver;
+import android.content.Context;
import android.database.Cursor;
import android.media.tv.TvContract;
import android.media.tv.TvContract.Programs;
@@ -34,9 +35,12 @@ import com.android.tv.data.ChannelImpl;
import com.android.tv.data.Program;
import com.android.tv.data.api.Channel;
import com.android.tv.dvr.data.RecordedProgram;
+import com.google.common.base.Predicate;
+import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
+import javax.inject.Qualifier;
/**
* {@link AsyncTask} that defaults to executing on its own single threaded Executor Service.
@@ -50,6 +54,10 @@ public abstract class AsyncDbTask<Params, Progress, Result>
private static final String TAG = "AsyncDbTask";
private static final boolean DEBUG = false;
+ /** Annotation for requesting the {@link Executor} for data base access. */
+ @Qualifier
+ public @interface DbExecutor {}
+
private final Executor mExecutor;
boolean mCalledExecuteOnDbThread;
@@ -67,23 +75,23 @@ public abstract class AsyncDbTask<Params, Progress, Result>
* @param <Result> the type of result returned by {@link #onQuery(Cursor)}
*/
public abstract static class AsyncQueryTask<Result> extends AsyncDbTask<Void, Void, Result> {
- private final ContentResolver mContentResolver;
+ private final WeakReference<Context> mContextReference;
private final Uri mUri;
- private final String[] mProjection;
private final String mSelection;
private final String[] mSelectionArgs;
private final String mOrderBy;
+ private String[] mProjection;
public AsyncQueryTask(
- Executor executor,
- ContentResolver contentResolver,
+ @DbExecutor Executor executor,
+ Context context,
Uri uri,
String[] projection,
String selection,
String[] selectionArgs,
String orderBy) {
super(executor);
- mContentResolver = contentResolver;
+ mContextReference = new WeakReference<>(context);
mUri = uri;
mProjection = projection;
mSelection = selection;
@@ -110,12 +118,35 @@ public abstract class AsyncDbTask<Params, Progress, Result>
// This is guaranteed to never call onPostExecute because the task is canceled.
return null;
}
+ Context context = mContextReference.get();
+ if (context == null) {
+ return null;
+ }
+ if (Utils.isProgramsUri(mUri)
+ && TvProviderUtils.checkSeriesIdColumn(context, Programs.CONTENT_URI)) {
+ mProjection =
+ TvProviderUtils.addExtraColumnsToProjection(
+ mProjection, TvProviderUtils.EXTRA_PROGRAM_COLUMN_SERIES_ID);
+ } else if (Utils.isRecordedProgramsUri(mUri)) {
+ if (TvProviderUtils.checkSeriesIdColumn(
+ context, TvContract.RecordedPrograms.CONTENT_URI)) {
+ mProjection =
+ TvProviderUtils.addExtraColumnsToProjection(
+ mProjection, TvProviderUtils.EXTRA_PROGRAM_COLUMN_SERIES_ID);
+ }
+ if (TvProviderUtils.checkStateColumn(
+ context, TvContract.RecordedPrograms.CONTENT_URI)) {
+ mProjection =
+ TvProviderUtils.addExtraColumnsToProjection(
+ mProjection, TvProviderUtils.EXTRA_PROGRAM_COLUMN_STATE);
+ }
+ }
if (DEBUG) {
Log.v(TAG, "Starting query for " + this);
}
try (Cursor c =
- mContentResolver.query(
- mUri, mProjection, mSelection, mSelectionArgs, mOrderBy)) {
+ context.getContentResolver()
+ .query(mUri, mProjection, mSelection, mSelectionArgs, mOrderBy)) {
if (c != null && !isCancelled()) {
Result result = onQuery(c);
if (DEBUG) {
@@ -164,33 +195,25 @@ public abstract class AsyncDbTask<Params, Progress, Result>
public AsyncQueryListTask(
Executor executor,
- ContentResolver contentResolver,
+ Context context,
Uri uri,
String[] projection,
String selection,
String[] selectionArgs,
String orderBy) {
- this(
- executor,
- contentResolver,
- uri,
- projection,
- selection,
- selectionArgs,
- orderBy,
- null);
+ this(executor, context, uri, projection, selection, selectionArgs, orderBy, null);
}
public AsyncQueryListTask(
Executor executor,
- ContentResolver contentResolver,
+ Context context,
Uri uri,
String[] projection,
String selection,
String[] selectionArgs,
String orderBy,
CursorFilter filter) {
- super(executor, contentResolver, uri, projection, selection, selectionArgs, orderBy);
+ super(executor, context, uri, projection, selection, selectionArgs, orderBy);
mFilter = filter;
}
@@ -202,7 +225,7 @@ public abstract class AsyncDbTask<Params, Progress, Result>
// This is guaranteed to never call onPostExecute because the task is canceled.
return null;
}
- if (mFilter != null && !mFilter.filter(c)) {
+ if (mFilter != null && !mFilter.apply(c)) {
continue;
}
T t = fromCursor(c);
@@ -237,13 +260,13 @@ public abstract class AsyncDbTask<Params, Progress, Result>
public AsyncQueryItemTask(
Executor executor,
- ContentResolver contentResolver,
+ Context context,
Uri uri,
String[] projection,
String selection,
String[] selectionArgs,
String orderBy) {
- super(executor, contentResolver, uri, projection, selection, selectionArgs, orderBy);
+ super(executor, context, uri, projection, selection, selectionArgs, orderBy);
}
@Override
@@ -283,10 +306,10 @@ public abstract class AsyncDbTask<Params, Progress, Result>
/** Gets an {@link List} of {@link Channel}s from {@link TvContract.Channels#CONTENT_URI}. */
public abstract static class AsyncChannelQueryTask extends AsyncQueryListTask<Channel> {
- public AsyncChannelQueryTask(Executor executor, ContentResolver contentResolver) {
+ public AsyncChannelQueryTask(Executor executor, Context context) {
super(
executor,
- contentResolver,
+ context,
TvContract.Channels.CONTENT_URI,
ChannelImpl.PROJECTION,
null,
@@ -302,20 +325,13 @@ public abstract class AsyncDbTask<Params, Progress, Result>
/** Gets an {@link List} of {@link Program}s from {@link TvContract.Programs#CONTENT_URI}. */
public abstract static class AsyncProgramQueryTask extends AsyncQueryListTask<Program> {
- public AsyncProgramQueryTask(Executor executor, ContentResolver contentResolver) {
- super(
- executor,
- contentResolver,
- Programs.CONTENT_URI,
- Program.PROJECTION,
- null,
- null,
- null);
+ public AsyncProgramQueryTask(Executor executor, Context context) {
+ super(executor, context, Programs.CONTENT_URI, Program.PROJECTION, null, null, null);
}
public AsyncProgramQueryTask(
Executor executor,
- ContentResolver contentResolver,
+ Context context,
Uri uri,
String selection,
String[] selectionArgs,
@@ -323,7 +339,7 @@ public abstract class AsyncDbTask<Params, Progress, Result>
CursorFilter filter) {
super(
executor,
- contentResolver,
+ context,
uri,
Program.PROJECTION,
selection,
@@ -341,9 +357,8 @@ public abstract class AsyncDbTask<Params, Progress, Result>
/** Gets an {@link List} of {@link TvContract.RecordedPrograms}s. */
public abstract static class AsyncRecordedProgramQueryTask
extends AsyncQueryListTask<RecordedProgram> {
- public AsyncRecordedProgramQueryTask(
- Executor executor, ContentResolver contentResolver, Uri uri) {
- super(executor, contentResolver, uri, RecordedProgram.PROJECTION, null, null, null);
+ public AsyncRecordedProgramQueryTask(Executor executor, Context context, Uri uri) {
+ super(executor, context, uri, RecordedProgram.PROJECTION, null, null, null);
}
@Override
@@ -370,13 +385,10 @@ public abstract class AsyncDbTask<Params, Progress, Result>
protected final long mChannelId;
public LoadProgramsForChannelTask(
- Executor executor,
- ContentResolver contentResolver,
- long channelId,
- @Nullable Range<Long> period) {
+ Executor executor, Context context, long channelId, @Nullable Range<Long> period) {
super(
executor,
- contentResolver,
+ context,
period == null
? TvContract.buildProgramsUriForChannel(channelId)
: TvContract.buildProgramsUriForChannel(
@@ -401,11 +413,10 @@ public abstract class AsyncDbTask<Params, Progress, Result>
/** Gets a single {@link Program} from {@link TvContract.Programs#CONTENT_URI}. */
public static class AsyncQueryProgramTask extends AsyncQueryItemTask<Program> {
- public AsyncQueryProgramTask(
- Executor executor, ContentResolver contentResolver, long programId) {
+ public AsyncQueryProgramTask(Executor executor, Context context, long programId) {
super(
executor,
- contentResolver,
+ context,
TvContract.buildProgramUri(programId),
Program.PROJECTION,
null,
@@ -420,5 +431,5 @@ public abstract class AsyncDbTask<Params, Progress, Result>
}
/** An interface which filters the row. */
- public interface CursorFilter extends Filter<Cursor> {}
+ public interface CursorFilter extends Predicate<Cursor> {}
}
diff --git a/src/com/android/tv/util/RecurringRunner.java b/src/com/android/tv/util/RecurringRunner.java
index 764689c2..82e8a94a 100644
--- a/src/com/android/tv/util/RecurringRunner.java
+++ b/src/com/android/tv/util/RecurringRunner.java
@@ -99,17 +99,14 @@ public final class RecurringRunner {
long delay = Math.max(next - now, 0);
boolean posted =
mHandler.postDelayed(
- new Runnable() {
- @Override
- public void run() {
- try {
- if (DEBUG) Log.i(TAG, "Starting " + mName);
- mRunnable.run();
- } catch (Exception e) {
- Log.w(TAG, "Error running " + mName, e);
- }
- postAt(resetNextRunTime());
+ () -> {
+ try {
+ if (DEBUG) Log.i(TAG, "Starting " + mName);
+ mRunnable.run();
+ } catch (Exception e) {
+ Log.w(TAG, "Error running " + mName, e);
}
+ postAt(resetNextRunTime());
},
delay);
if (!posted) {
diff --git a/src/com/android/tv/util/SetupUtils.java b/src/com/android/tv/util/SetupUtils.java
index 0d536320..a9b67fa8 100644
--- a/src/com/android/tv/util/SetupUtils.java
+++ b/src/com/android/tv/util/SetupUtils.java
@@ -28,20 +28,25 @@ import android.media.tv.TvInputManager;
import android.preference.PreferenceManager;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
-import android.support.annotation.VisibleForTesting;
import android.text.TextUtils;
import android.util.ArraySet;
import android.util.Log;
import com.android.tv.TvSingletons;
-import com.android.tv.common.BaseApplication;
import com.android.tv.common.SoftPreconditions;
+import com.android.tv.common.dagger.annotations.ApplicationContext;
+import com.android.tv.common.singletons.HasTvInputId;
import com.android.tv.data.ChannelDataManager;
import com.android.tv.data.api.Channel;
+import com.android.tv.tunerinputcontroller.BuiltInTunerManager;
+import com.google.common.base.Optional;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
+import javax.inject.Inject;
+import javax.inject.Singleton;
/** A utility class related to input setup. */
+@Singleton
public class SetupUtils {
private static final String TAG = "SetupUtils";
private static final boolean DEBUG = false;
@@ -61,10 +66,12 @@ public class SetupUtils {
private final Set<String> mSetUpInputs;
private final Set<String> mRecognizedInputs;
private boolean mIsFirstTune;
- private final String mTunerInputId;
+ private final Optional<String> mOptionalTunerInputId;
- @VisibleForTesting
- protected SetupUtils(Context context) {
+ @Inject
+ public SetupUtils(
+ @ApplicationContext Context context,
+ Optional<BuiltInTunerManager> optionalBuiltInTunerManager) {
mContext = context;
mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
mSetUpInputs = new ArraySet<>();
@@ -77,16 +84,8 @@ public class SetupUtils {
mRecognizedInputs.addAll(
mSharedPreferences.getStringSet(PREF_KEY_RECOGNIZED_INPUTS, mKnownInputs));
mIsFirstTune = mSharedPreferences.getBoolean(PREF_KEY_IS_FIRST_TUNE, true);
- mTunerInputId = BaseApplication.getSingletons(context).getEmbeddedTunerInputId();
- }
-
- /**
- * Creates an instance of {@link SetupUtils}.
- *
- * <p><b>WARNING</b> this should only be called by the top level application.
- */
- public static SetupUtils createForTvSingletons(Context context) {
- return new SetupUtils(context.getApplicationContext());
+ mOptionalTunerInputId =
+ optionalBuiltInTunerManager.transform(HasTvInputId::getEmbeddedTunerInputId);
}
/** Additional work after the setup of TV input. */
@@ -124,32 +123,29 @@ public class SetupUtils {
TvSingletons tvSingletons = TvSingletons.getSingletons(context);
final ChannelDataManager manager = tvSingletons.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)) {
- if (!channel.isBrowsable()) {
- manager.updateBrowsable(channel.getId(), true, true);
- browsableChanged = true;
- }
- if (firstChannelForInput == null) {
- firstChannelForInput = channel;
- }
+ () -> {
+ Channel firstChannelForInput = null;
+ boolean browsableChanged = false;
+ for (Channel channel : manager.getChannelList()) {
+ if (channel.getInputId().equals(inputId)) {
+ if (!channel.isBrowsable()) {
+ 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();
- }
- if (postRunnable != null) {
- postRunnable.run();
- }
+ }
+ if (firstChannelForInput != null) {
+ Utils.setLastWatchedChannel(context, firstChannelForInput);
+ }
+ if (browsableChanged) {
+ manager.notifyChannelBrowsableChanged();
+ manager.applyUpdatedValuesToDb();
+ }
+ if (postRunnable != null) {
+ postRunnable.run();
}
});
}
@@ -332,7 +328,9 @@ public class SetupUtils {
// A USB tuner device can be temporarily unplugged. We do not remove the USB tuner input
// from the known inputs so that the input won't appear as a new input whenever the user
// plugs in the USB tuner device again.
- removedInputList.remove(mTunerInputId);
+ if (mOptionalTunerInputId.isPresent()) {
+ removedInputList.remove(mOptionalTunerInputId.get());
+ }
if (!removedInputList.isEmpty()) {
boolean inputPackageDeleted = false;
diff --git a/src/com/android/tv/util/SqlParams.java b/src/com/android/tv/util/SqlParams.java
index c4b803b6..fa557ba2 100644
--- a/src/com/android/tv/util/SqlParams.java
+++ b/src/com/android/tv/util/SqlParams.java
@@ -17,15 +17,16 @@
package com.android.tv.util;
import android.database.DatabaseUtils;
+import android.support.annotation.Nullable;
import java.util.Arrays;
/** Convenience class for SQL operations. */
public class SqlParams {
private String mTables;
- private String mSelection;
- private String[] mSelectionArgs;
+ private @Nullable String mSelection;
+ private @Nullable String[] mSelectionArgs;
- public SqlParams(String tables, String selection, String... selectionArgs) {
+ public SqlParams(String tables, @Nullable String selection, @Nullable String... selectionArgs) {
setTables(tables);
setWhere(selection, selectionArgs);
}
@@ -34,11 +35,11 @@ public class SqlParams {
return mTables;
}
- public String getSelection() {
+ public @Nullable String getSelection() {
return mSelection;
}
- public String[] getSelectionArgs() {
+ public @Nullable String[] getSelectionArgs() {
return mSelectionArgs;
}
diff --git a/src/com/android/tv/util/TvInputManagerHelper.java b/src/com/android/tv/util/TvInputManagerHelper.java
index 625fb7b2..cb7d9854 100644
--- a/src/com/android/tv/util/TvInputManagerHelper.java
+++ b/src/com/android/tv/util/TvInputManagerHelper.java
@@ -19,21 +19,29 @@ package com.android.tv.util;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
+import android.database.ContentObserver;
import android.graphics.drawable.Drawable;
import android.hardware.hdmi.HdmiDeviceInfo;
import android.media.tv.TvContentRatingSystemInfo;
import android.media.tv.TvInputInfo;
import android.media.tv.TvInputManager;
import android.media.tv.TvInputManager.TvInputCallback;
+import android.net.Uri;
import android.os.Handler;
+import android.provider.Settings;
+import android.provider.Settings.SettingNotFoundException;
import android.support.annotation.Nullable;
+import android.support.annotation.UiThread;
import android.support.annotation.VisibleForTesting;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.Log;
-import com.android.tv.TvFeatures;
import com.android.tv.common.SoftPreconditions;
+import com.android.tv.common.compat.TvInputInfoCompat;
+import com.android.tv.common.dagger.annotations.ApplicationContext;
import com.android.tv.common.util.CommonUtils;
+import com.android.tv.common.util.SystemProperties;
+import com.android.tv.features.TvFeatures;
import com.android.tv.parental.ContentRatingsManager;
import com.android.tv.parental.ParentalControlSettings;
import com.android.tv.util.images.ImageCache;
@@ -46,7 +54,12 @@ import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+/** Helper class for {@link TvInputManager}. */
+@UiThread
+@Singleton
public class TvInputManagerHelper {
private static final String TAG = "TvInputManagerHelper";
private static final boolean DEBUG = false;
@@ -117,6 +130,12 @@ public class TvInputManagerHelper {
};
private static final String META_LABEL_SORT_KEY = "input_sort_key";
+ private static final String TV_INPUT_ALLOW_3RD_PARTY_INPUTS = "tv_input_allow_3rd_party_inputs";
+
+ private static final String[] SYSTEM_INPUT_ID_BLACKLIST = {
+ "com.google.android.videos/" // Play Movies
+ };
+
/** The default tv input priority to show. */
private static final ArrayList<Integer> DEFAULT_TV_INPUT_PRIORITY = new ArrayList<>();
@@ -149,21 +168,24 @@ public class TvInputManagerHelper {
private final PackageManager mPackageManager;
protected final TvInputManagerInterface mTvInputManager;
private final Map<String, Integer> mInputStateMap = new HashMap<>();
- private final Map<String, TvInputInfo> mInputMap = new HashMap<>();
+ private final Map<String, TvInputInfoCompat> mInputMap = new HashMap<>();
private final Map<String, String> mTvInputLabels = new ArrayMap<>();
private final Map<String, String> mTvInputCustomLabels = new ArrayMap<>();
private final Map<String, Boolean> mInputIdToPartnerInputMap = new HashMap<>();
private final Map<String, CharSequence> mTvInputApplicationLabels = new ArrayMap<>();
private final Map<String, Drawable> mTvInputApplicationIcons = new ArrayMap<>();
- private final Map<String, Drawable> mTvInputAppliactionBanners = new ArrayMap<>();
+ private final Map<String, Drawable> mTvInputApplicationBanners = new ArrayMap<>();
+
+ private final ContentObserver mContentObserver;
private final TvInputCallback mInternalCallback =
new TvInputCallback() {
@Override
public void onInputStateChanged(String inputId, int state) {
if (DEBUG) Log.d(TAG, "onInputStateChanged " + inputId + " state=" + state);
- if (isInBlackList(inputId)) {
+ TvInputInfo info = mInputMap.get(inputId).getTvInputInfo();
+ if (info == null || isInputBlocked(info)) {
return;
}
mInputStateMap.put(inputId, state);
@@ -175,12 +197,12 @@ public class TvInputManagerHelper {
@Override
public void onInputAdded(String inputId) {
if (DEBUG) Log.d(TAG, "onInputAdded " + inputId);
- if (isInBlackList(inputId)) {
+ TvInputInfo info = mTvInputManager.getTvInputInfo(inputId);
+ if (info == null || isInputBlocked(info)) {
return;
}
- TvInputInfo info = mTvInputManager.getTvInputInfo(inputId);
if (info != null) {
- mInputMap.put(inputId, info);
+ mInputMap.put(inputId, new TvInputInfoCompat(mContext, info));
CharSequence label = info.loadLabel(mContext);
// in tests the label may be missing just use the input id
mTvInputLabels.put(inputId, label != null ? label.toString() : inputId);
@@ -205,7 +227,7 @@ public class TvInputManagerHelper {
mTvInputCustomLabels.remove(inputId);
mTvInputApplicationLabels.remove(inputId);
mTvInputApplicationIcons.remove(inputId);
- mTvInputAppliactionBanners.remove(inputId);
+ mTvInputApplicationBanners.remove(inputId);
mInputStateMap.remove(inputId);
mInputIdToPartnerInputMap.remove(inputId);
mContentRatingsManager.update();
@@ -219,11 +241,11 @@ public class TvInputManagerHelper {
@Override
public void onInputUpdated(String inputId) {
if (DEBUG) Log.d(TAG, "onInputUpdated " + inputId);
- if (isInBlackList(inputId)) {
+ TvInputInfo info = mTvInputManager.getTvInputInfo(inputId);
+ if (info == null || isInputBlocked(info)) {
return;
}
- TvInputInfo info = mTvInputManager.getTvInputInfo(inputId);
- mInputMap.put(inputId, info);
+ mInputMap.put(inputId, new TvInputInfoCompat(mContext, info));
mTvInputLabels.put(inputId, info.loadLabel(mContext).toString());
CharSequence inputCustomLabel = info.loadCustomLabel(mContext);
if (inputCustomLabel != null) {
@@ -231,7 +253,7 @@ public class TvInputManagerHelper {
}
mTvInputApplicationLabels.remove(inputId);
mTvInputApplicationIcons.remove(inputId);
- mTvInputAppliactionBanners.remove(inputId);
+ mTvInputApplicationBanners.remove(inputId);
for (TvInputCallback callback : mCallbacks) {
callback.onInputUpdated(inputId);
}
@@ -242,7 +264,10 @@ public class TvInputManagerHelper {
@Override
public void onTvInputInfoUpdated(TvInputInfo inputInfo) {
if (DEBUG) Log.d(TAG, "onTvInputInfoUpdated " + inputInfo);
- mInputMap.put(inputInfo.getId(), inputInfo);
+ if (isInputBlocked(inputInfo)) {
+ return;
+ }
+ mInputMap.put(inputInfo.getId(), new TvInputInfoCompat(mContext, inputInfo));
mTvInputLabels.put(inputInfo.getId(), inputInfo.loadLabel(mContext).toString());
CharSequence inputCustomLabel = inputInfo.loadCustomLabel(mContext);
if (inputCustomLabel != null) {
@@ -264,8 +289,10 @@ public class TvInputManagerHelper {
private final ContentRatingsManager mContentRatingsManager;
private final ParentalControlSettings mParentalControlSettings;
private final Comparator<TvInputInfo> mTvInputInfoComparator;
+ private boolean mAllow3rdPartyInputs;
- public TvInputManagerHelper(Context context) {
+ @Inject
+ public TvInputManagerHelper(@ApplicationContext Context context) {
this(context, createTvInputManagerWrapper(context));
}
@@ -285,6 +312,22 @@ public class TvInputManagerHelper {
mContentRatingsManager = new ContentRatingsManager(context, tvInputManager);
mParentalControlSettings = new ParentalControlSettings(context);
mTvInputInfoComparator = new InputComparatorInternal(this);
+ mContentObserver =
+ new ContentObserver(mHandler) {
+ @Override
+ public void onChange(boolean selfChange, Uri uri) {
+ String option = uri.getLastPathSegment();
+ if (option == null || !option.equals(TV_INPUT_ALLOW_3RD_PARTY_INPUTS)) {
+ return;
+ }
+ boolean previousSetting = mAllow3rdPartyInputs;
+ updateAllow3rdPartyInputs();
+ if (previousSetting == mAllow3rdPartyInputs) {
+ return;
+ }
+ initInputMaps();
+ }
+ };
}
public void start() {
@@ -297,30 +340,14 @@ public class TvInputManagerHelper {
}
if (DEBUG) Log.d(TAG, "start");
mStarted = true;
+ mContext.getContentResolver()
+ .registerContentObserver(
+ Settings.Global.getUriFor(TV_INPUT_ALLOW_3RD_PARTY_INPUTS),
+ true,
+ mContentObserver);
+ updateAllow3rdPartyInputs();
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()) {
- if (DEBUG) Log.d(TAG, "Input detected " + input);
- String inputId = input.getId();
- if (isInBlackList(inputId)) {
- continue;
- }
- mInputMap.put(inputId, input);
- int state = mTvInputManager.getInputState(inputId);
- mInputStateMap.put(inputId, state);
- mInputIdToPartnerInputMap.put(inputId, isPartnerInput(input));
- }
- SoftPreconditions.checkState(
- mInputStateMap.size() == mInputMap.size(),
- TAG,
- "mInputStateMap not the same size as mInputMap");
+ initInputMaps();
mContentRatingsManager.update();
}
@@ -329,6 +356,7 @@ public class TvInputManagerHelper {
return;
}
mTvInputManager.unregisterCallback(mInternalCallback);
+ mContext.getContentResolver().unregisterContentObserver(mContentObserver);
mStarted = false;
mInputStateMap.clear();
mInputMap.clear();
@@ -336,8 +364,7 @@ public class TvInputManagerHelper {
mTvInputCustomLabels.clear();
mTvInputApplicationLabels.clear();
mTvInputApplicationIcons.clear();
- mTvInputAppliactionBanners.clear();
- ;
+ mTvInputApplicationBanners.clear();
mInputIdToPartnerInputMap.clear();
}
@@ -355,6 +382,9 @@ public class TvInputManagerHelper {
continue;
}
TvInputInfo input = getTvInputInfo(pair.getKey());
+ if (input == null || isInputBlocked(input)) {
+ continue;
+ }
if (tunerOnly && input.getType() != TvInputInfo.TYPE_TUNER) {
continue;
}
@@ -460,12 +490,12 @@ public class TvInputManagerHelper {
/** Gets the tv input application's banner. */
public Drawable getTvInputApplicationBanner(String inputId) {
- return mTvInputAppliactionBanners.get(inputId);
+ return mTvInputApplicationBanners.get(inputId);
}
/** Stores the tv input application's banner. */
public void setTvInputApplicationBanner(String inputId, Drawable banner) {
- mTvInputAppliactionBanners.put(inputId, banner);
+ mTvInputApplicationBanners.put(inputId, banner);
}
/** Returns if TV input exists with the input id. */
@@ -475,7 +505,14 @@ public class TvInputManagerHelper {
return mStarted && !TextUtils.isEmpty(inputId) && mInputMap.get(inputId) != null;
}
+ @Nullable
public TvInputInfo getTvInputInfo(String inputId) {
+ TvInputInfoCompat inputInfo = getTvInputInfoCompat(inputId);
+ return inputInfo == null ? null : inputInfo.getTvInputInfo();
+ }
+
+ @Nullable
+ public TvInputInfoCompat getTvInputInfoCompat(String inputId) {
SoftPreconditions.checkState(
mStarted, TAG, "getTvInputInfo() called before TvInputManagerHelper was started.");
if (!mStarted) {
@@ -494,7 +531,7 @@ public class TvInputManagerHelper {
public int getTunerTvInputSize() {
int size = 0;
- for (TvInputInfo input : mInputMap.values()) {
+ for (TvInputInfoCompat input : mInputMap.values()) {
if (input.getType() == TvInputInfo.TYPE_TUNER) {
++size;
}
@@ -601,6 +638,61 @@ public class TvInputManagerHelper {
return false;
}
+ private void initInputMaps() {
+ mInputMap.clear();
+ mTvInputLabels.clear();
+ mTvInputCustomLabels.clear();
+ mTvInputApplicationLabels.clear();
+ mTvInputApplicationIcons.clear();
+ mTvInputApplicationBanners.clear();
+ mInputStateMap.clear();
+ mInputIdToPartnerInputMap.clear();
+ for (TvInputInfo input : mTvInputManager.getTvInputList()) {
+ if (DEBUG) {
+ Log.d(TAG, "Input detected " + input);
+ }
+ String inputId = input.getId();
+ if (isInputBlocked(input)) {
+ continue;
+ }
+ mInputMap.put(inputId, new TvInputInfoCompat(mContext, input));
+ int state = mTvInputManager.getInputState(inputId);
+ mInputStateMap.put(inputId, state);
+ mInputIdToPartnerInputMap.put(inputId, isPartnerInput(input));
+ }
+ SoftPreconditions.checkState(
+ mInputStateMap.size() == mInputMap.size(),
+ TAG,
+ "mInputStateMap not the same size as mInputMap");
+ }
+
+ private void updateAllow3rdPartyInputs() {
+ int setting;
+ try {
+ setting =
+ Settings.Global.getInt(
+ mContext.getContentResolver(), TV_INPUT_ALLOW_3RD_PARTY_INPUTS);
+ } catch (SettingNotFoundException e) {
+ mAllow3rdPartyInputs = SystemProperties.ALLOW_THIRD_PARTY_INPUTS.getValue();
+ return;
+ }
+ mAllow3rdPartyInputs = setting == 1;
+ }
+
+ private boolean isInputBlocked(TvInputInfo info) {
+ if (!mAllow3rdPartyInputs) {
+ if (!isSystemInput(info)) {
+ return true;
+ }
+ for (String id : SYSTEM_INPUT_ID_BLACKLIST) {
+ if (info.getId().startsWith(id)) {
+ return true;
+ }
+ }
+ }
+ return isInBlackList(info.getId());
+ }
+
/**
* Default comparator for TvInputInfo.
*
diff --git a/src/com/android/tv/util/TvProviderUtils.java b/src/com/android/tv/util/TvProviderUtils.java
new file mode 100644
index 00000000..6b5aaecc
--- /dev/null
+++ b/src/com/android/tv/util/TvProviderUtils.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.util;
+
+import static java.lang.Boolean.TRUE;
+
+import android.content.Context;
+import android.media.tv.TvContract;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.annotation.StringDef;
+import android.support.annotation.VisibleForTesting;
+import android.support.annotation.WorkerThread;
+import android.util.Log;
+import com.android.tv.data.BaseProgram;
+import com.android.tv.features.PartnerFeatures;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/** A utility class related to TvProvider. */
+public final class TvProviderUtils {
+ private static final String TAG = "TvProviderUtils";
+
+ public static final String EXTRA_PROGRAM_COLUMN_SERIES_ID = BaseProgram.COLUMN_SERIES_ID;
+ public static final String EXTRA_PROGRAM_COLUMN_STATE = BaseProgram.COLUMN_STATE;
+
+ /** Possible extra columns in TV provider. */
+ @Retention(RetentionPolicy.SOURCE)
+ @StringDef({EXTRA_PROGRAM_COLUMN_SERIES_ID, EXTRA_PROGRAM_COLUMN_STATE})
+ public @interface TvProviderExtraColumn {}
+
+ private static boolean sProgramHasSeriesIdColumn;
+ private static boolean sRecordedProgramHasSeriesIdColumn;
+ private static boolean sRecordedProgramHasStateColumn;
+
+ /**
+ * Checks whether a table contains a series ID column.
+ *
+ * <p>This method is different from {@link #getProgramHasSeriesIdColumn()} and {@link
+ * #getRecordedProgramHasSeriesIdColumn()} because it may access to database, so it should be
+ * run in worker thread.
+ *
+ * @return {@code true} if the corresponding table contains a series ID column; {@code false}
+ * otherwise.
+ */
+ @WorkerThread
+ public static synchronized boolean checkSeriesIdColumn(Context context, Uri uri) {
+ boolean canCreateColumn = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O);
+ if (!canCreateColumn) {
+ return false;
+ }
+ return (Utils.isRecordedProgramsUri(uri)
+ && checkRecordedProgramTableSeriesIdColumn(context, uri))
+ || (Utils.isProgramsUri(uri) && checkProgramTableSeriesIdColumn(context, uri));
+ }
+
+ @WorkerThread
+ private static synchronized boolean checkProgramTableSeriesIdColumn(Context context, Uri uri) {
+ if (!sProgramHasSeriesIdColumn) {
+ if (getExistingColumns(context, uri).contains(EXTRA_PROGRAM_COLUMN_SERIES_ID)) {
+ sProgramHasSeriesIdColumn = true;
+ } else if (addColumnToTable(context, uri, EXTRA_PROGRAM_COLUMN_SERIES_ID)) {
+ sProgramHasSeriesIdColumn = true;
+ }
+ }
+ return sProgramHasSeriesIdColumn;
+ }
+
+ @WorkerThread
+ private static synchronized boolean checkRecordedProgramTableSeriesIdColumn(
+ Context context, Uri uri) {
+ if (!sRecordedProgramHasSeriesIdColumn) {
+ if (getExistingColumns(context, uri).contains(EXTRA_PROGRAM_COLUMN_SERIES_ID)) {
+ sRecordedProgramHasSeriesIdColumn = true;
+ } else if (addColumnToTable(context, uri, EXTRA_PROGRAM_COLUMN_SERIES_ID)) {
+ sRecordedProgramHasSeriesIdColumn = true;
+ }
+ }
+ return sRecordedProgramHasSeriesIdColumn;
+ }
+
+ /**
+ * Checks whether a table contains a state column.
+ *
+ * <p>This method is different from {@link #getRecordedProgramHasStateColumn()} because it may
+ * access to database, so it should be run in worker thread.
+ *
+ * @return {@code true} if the corresponding table contains a state column; {@code false}
+ * otherwise.
+ */
+ @WorkerThread
+ public static synchronized boolean checkStateColumn(Context context, Uri uri) {
+ boolean canCreateColumn = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O);
+ if (!canCreateColumn) {
+ return false;
+ }
+ return (Utils.isRecordedProgramsUri(uri)
+ && checkRecordedProgramTableStateColumn(context, uri));
+ }
+
+ @WorkerThread
+ private static synchronized boolean checkRecordedProgramTableStateColumn(
+ Context context, Uri uri) {
+ if (!sRecordedProgramHasStateColumn) {
+ if (getExistingColumns(context, uri).contains(EXTRA_PROGRAM_COLUMN_STATE)) {
+ sRecordedProgramHasStateColumn = true;
+ } else if (addColumnToTable(context, uri, EXTRA_PROGRAM_COLUMN_STATE)) {
+ sRecordedProgramHasStateColumn = true;
+ }
+ }
+ return sRecordedProgramHasStateColumn;
+ }
+
+ public static synchronized boolean getProgramHasSeriesIdColumn() {
+ return TRUE.equals(sProgramHasSeriesIdColumn);
+ }
+
+ public static synchronized boolean getRecordedProgramHasSeriesIdColumn() {
+ return TRUE.equals(sRecordedProgramHasSeriesIdColumn);
+ }
+
+ public static synchronized boolean getRecordedProgramHasStateColumn() {
+ return TRUE.equals(sRecordedProgramHasStateColumn);
+ }
+
+ public static String[] addExtraColumnsToProjection(String[] projection,
+ @TvProviderExtraColumn String column) {
+ List<String> projectionList = new ArrayList<>(Arrays.asList(projection));
+ if (!projectionList.contains(column)) {
+ projectionList.add(column);
+ }
+ projection = projectionList.toArray(projection);
+ return projection;
+ }
+
+ /**
+ * Gets column names of a table
+ *
+ * @param uri the corresponding URI of the table
+ */
+ @VisibleForTesting
+ static Set<String> getExistingColumns(Context context, Uri uri) {
+ Bundle result = null;
+ try {
+ result =
+ context.getContentResolver()
+ .call(uri, TvContract.METHOD_GET_COLUMNS, uri.toString(), null);
+ } catch (Exception e) {
+ Log.e(TAG, "Error trying to get existing columns.", e);
+ }
+ if (result != null) {
+ String[] columns = result.getStringArray(TvContract.EXTRA_EXISTING_COLUMN_NAMES);
+ if (columns != null) {
+ return new HashSet<>(Arrays.asList(columns));
+ }
+ }
+ Log.e(TAG, "Query existing column names from " + uri + " returned null");
+ return Collections.emptySet();
+ }
+
+ /**
+ * Add a column to the table
+ *
+ * @return {@code true} if the column is added successfully; {@code false} otherwise.
+ */
+ private static boolean addColumnToTable(Context context, Uri contentUri, String columnName) {
+ Bundle extra = new Bundle();
+ extra.putCharSequence(TvContract.EXTRA_COLUMN_NAME, columnName);
+ extra.putCharSequence(TvContract.EXTRA_DATA_TYPE, "TEXT");
+ // If the add operation fails, the following just returns null without crashing.
+ Bundle allColumns = null;
+ try {
+ allColumns =
+ context.getContentResolver()
+ .call(
+ contentUri,
+ TvContract.METHOD_ADD_COLUMN,
+ contentUri.toString(),
+ extra);
+ } catch (Exception e) {
+ Log.e(TAG, "Error trying to add column.", e);
+ }
+ if (allColumns == null) {
+ Log.w(TAG, "Adding new column failed. Uri=" + contentUri);
+ }
+ return allColumns != null;
+ }
+
+ private TvProviderUtils() {}
+}
diff --git a/src/com/android/tv/util/TvTrackInfoUtils.java b/src/com/android/tv/util/TvTrackInfoUtils.java
index 09874502..4ec96c62 100644
--- a/src/com/android/tv/util/TvTrackInfoUtils.java
+++ b/src/com/android/tv/util/TvTrackInfoUtils.java
@@ -15,13 +15,28 @@
*/
package com.android.tv.util;
+import android.content.Context;
import android.media.tv.TvTrackInfo;
+import android.text.TextUtils;
+import android.util.Log;
+import com.android.tv.R;
import java.util.Comparator;
+import java.util.HashSet;
import java.util.List;
+import java.util.Locale;
+import java.util.Objects;
+import java.util.Set;
/** Static utilities for {@link TvTrackInfo}. */
public class TvTrackInfoUtils {
+ private static final String TAG = "TvTrackInfoUtils";
+ private static final int AUDIO_CHANNEL_NONE = 0;
+ private static final int AUDIO_CHANNEL_MONO = 1;
+ private static final int AUDIO_CHANNEL_STEREO = 2;
+ private static final int AUDIO_CHANNEL_SURROUND_6 = 6;
+ private static final int AUDIO_CHANNEL_SURROUND_8 = 8;
+
/**
* Compares how closely two {@link android.media.tv.TvTrackInfo}s match {@code language}, {@code
* channelCount} and {@code id} in that precedence.
@@ -34,40 +49,36 @@ public class TvTrackInfoUtils {
*/
public static Comparator<TvTrackInfo> createComparator(
final String id, final String language, final int channelCount) {
- return new Comparator<TvTrackInfo>() {
-
- @Override
- public int compare(TvTrackInfo lhs, TvTrackInfo rhs) {
- if (lhs == rhs) {
- return 0;
- }
- if (lhs == null) {
- return -1;
- }
- if (rhs == null) {
- return 1;
- }
- // 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 lhsLangMatch =
- language == null || Utils.isEqualLanguage(lhs.getLanguage(), language);
- 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 Boolean.compare(lhsCountMatch, rhsCountMatch);
- }
+ return (TvTrackInfo lhs, TvTrackInfo rhs) -> {
+ if (Objects.equals(lhs, rhs)) {
+ return 0;
+ }
+ if (lhs == null) {
+ return -1;
+ }
+ if (rhs == null) {
+ return 1;
+ }
+ // 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 lhsLangMatch =
+ language == null || Utils.isEqualLanguage(lhs.getLanguage(), language);
+ 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 Boolean.compare(lhsLangMatch, rhsLangMatch);
+ return Boolean.compare(lhsCountMatch, rhsCountMatch);
}
+ } else {
+ return Boolean.compare(lhsLangMatch, rhsLangMatch);
}
};
}
@@ -96,5 +107,132 @@ public class TvTrackInfoUtils {
return best;
}
+ public static boolean needToShowSampleRate(Context context, List<TvTrackInfo> tracks) {
+ Set<String> multiAudioStrings = new HashSet<>();
+ for (TvTrackInfo track : tracks) {
+ String multiAudioString = getMultiAudioString(context, track, false);
+ if (multiAudioStrings.contains(multiAudioString)) {
+ return true;
+ }
+ multiAudioStrings.add(multiAudioString);
+ }
+ return false;
+ }
+
+ public static String getMultiAudioString(
+ Context context, TvTrackInfo track, boolean showSampleRate) {
+ if (track.getType() != TvTrackInfo.TYPE_AUDIO) {
+ throw new IllegalArgumentException("Not an audio track: " + toString(track));
+ }
+ String language = context.getString(R.string.multi_audio_unknown_language);
+ if (!TextUtils.isEmpty(track.getLanguage())) {
+ language = new Locale(track.getLanguage()).getDisplayName();
+ } else {
+ Log.d(
+ TAG,
+ "No language information found for the audio track: "
+ + toString(track)
+ );
+ }
+
+ StringBuilder metadata = new StringBuilder();
+ switch (track.getAudioChannelCount()) {
+ case AUDIO_CHANNEL_NONE:
+ break;
+ case AUDIO_CHANNEL_MONO:
+ metadata.append(context.getString(R.string.multi_audio_channel_mono));
+ break;
+ case AUDIO_CHANNEL_STEREO:
+ metadata.append(context.getString(R.string.multi_audio_channel_stereo));
+ break;
+ case AUDIO_CHANNEL_SURROUND_6:
+ metadata.append(context.getString(R.string.multi_audio_channel_surround_6));
+ break;
+ case AUDIO_CHANNEL_SURROUND_8:
+ metadata.append(context.getString(R.string.multi_audio_channel_surround_8));
+ break;
+ default:
+ if (track.getAudioChannelCount() > 0) {
+ metadata.append(
+ context.getString(
+ R.string.multi_audio_channel_suffix,
+ track.getAudioChannelCount()));
+ } else {
+ Log.d(
+ TAG,
+ "Invalid audio channel count ("
+ + track.getAudioChannelCount()
+ + ") found for the audio track: "
+ + toString(track));
+ }
+ break;
+ }
+ if (showSampleRate) {
+ int sampleRate = track.getAudioSampleRate();
+ if (sampleRate > 0) {
+ if (metadata.length() > 0) {
+ metadata.append(", ");
+ }
+ int integerPart = sampleRate / 1000;
+ int tenths = (sampleRate % 1000) / 100;
+ metadata.append(integerPart);
+ if (tenths != 0) {
+ metadata.append(".");
+ metadata.append(tenths);
+ }
+ metadata.append("kHz");
+ }
+ }
+
+ if (metadata.length() == 0) {
+ return language;
+ }
+ return context.getString(
+ R.string.multi_audio_display_string_with_channel, language, metadata.toString());
+ }
+
+ private static String trackTypeToString(int trackType) {
+ switch (trackType) {
+ case TvTrackInfo.TYPE_AUDIO:
+ return "Audio";
+ case TvTrackInfo.TYPE_VIDEO:
+ return "Video";
+ case TvTrackInfo.TYPE_SUBTITLE:
+ return "Subtitle";
+ default:
+ return "Invalid Type";
+ }
+ }
+
+ public static String toString(TvTrackInfo info) {
+ int trackType = info.getType();
+ return "TvTrackInfo{"
+ + "type="
+ + trackTypeToString(trackType)
+ + ", id="
+ + info.getId()
+ + ", language="
+ + info.getLanguage()
+ + ", description="
+ + info.getDescription()
+ + (trackType == TvTrackInfo.TYPE_AUDIO
+ ?
+ (", audioChannelCount="
+ + info.getAudioChannelCount()
+ + ", audioSampleRate="
+ + info.getAudioSampleRate()) : "")
+ + (trackType == TvTrackInfo.TYPE_VIDEO
+ ?
+ (", videoWidth="
+ + info.getVideoWidth()
+ + ", videoHeight="
+ + info.getVideoHeight()
+ + ", videoFrameRate="
+ + info.getVideoFrameRate()
+ + ", videoPixelAspectRatio="
+ + info.getVideoPixelAspectRatio()) : "")
+ + "}";
+ }
+
private TvTrackInfoUtils() {}
}
diff --git a/src/com/android/tv/util/Utils.java b/src/com/android/tv/util/Utils.java
index a75bd446..51173739 100644
--- a/src/com/android/tv/util/Utils.java
+++ b/src/com/android/tv/util/Utils.java
@@ -29,7 +29,6 @@ import android.media.tv.TvContract;
import android.media.tv.TvContract.Channels;
import android.media.tv.TvContract.Programs.Genres;
import android.media.tv.TvInputInfo;
-import android.media.tv.TvTrackInfo;
import android.net.Uri;
import android.os.Looper;
import android.preference.PreferenceManager;
@@ -42,6 +41,7 @@ import android.util.Log;
import android.view.View;
import com.android.tv.R;
import com.android.tv.TvSingletons;
+import com.android.tv.common.BaseSingletons;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.common.util.Clock;
import com.android.tv.data.GenreItems;
@@ -99,12 +99,6 @@ public class Utils {
private static final int VIDEO_ULTRA_HD_WIDTH = 2048;
private static final int VIDEO_ULTRA_HD_HEIGHT = 1536;
- private static final int AUDIO_CHANNEL_NONE = 0;
- private static final int AUDIO_CHANNEL_MONO = 1;
- private static final int AUDIO_CHANNEL_STEREO = 2;
- private static final int AUDIO_CHANNEL_SURROUND_6 = 6;
- 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);
@@ -141,6 +135,7 @@ public class Utils {
return sb.toString();
}
+ @Nullable
@WorkerThread
public static String getInputIdForChannel(Context context, long channelId) {
if (channelId == Channel.INVALID_ID) {
@@ -153,6 +148,8 @@ public class Utils {
if (cursor != null && cursor.moveToNext()) {
return Utils.intern(cursor.getString(0));
}
+ } catch (Exception e) {
+ Log.e(TAG, "Error get input id for channel", e);
}
return null;
}
@@ -325,8 +322,17 @@ public class Utils {
Uri uri =
TvContract.buildProgramsUriForChannel(
TvContract.buildChannelUri(channelId), timeMs, timeMs);
- try (Cursor cursor =
- context.getContentResolver().query(uri, Program.PROJECTION, null, null, null)) {
+ ContentResolver resolver = context.getContentResolver();
+
+ String[] projection = Program.PROJECTION;
+ if (TvProviderUtils.checkSeriesIdColumn(context, TvContract.Programs.CONTENT_URI)) {
+ if (Utils.isProgramsUri(uri)) {
+ projection =
+ TvProviderUtils.addExtraColumnsToProjection(
+ projection, TvProviderUtils.EXTRA_PROGRAM_COLUMN_SERIES_ID);
+ }
+ }
+ try (Cursor cursor = resolver.query(uri, projection, null, null, null)) {
if (cursor != null && cursor.moveToNext()) {
return Program.fromCursor(cursor);
}
@@ -360,11 +366,10 @@ public class Utils {
Context context, long startUtcMillis, long endUtcMillis, boolean useShortFormat) {
return getDurationString(
context,
- System.currentTimeMillis(),
+ ((BaseSingletons) context.getApplicationContext()).getClock(),
startUtcMillis,
endUtcMillis,
- useShortFormat,
- 0);
+ useShortFormat);
}
/**
@@ -400,7 +405,7 @@ public class Utils {
long startUtcMillis,
long endUtcMillis,
boolean useShortFormat,
- int flag) {
+ int flags) {
return getDurationString(
context,
startUtcMillis,
@@ -408,7 +413,7 @@ public class Utils {
useShortFormat,
!isInGivenDay(baseMillis, startUtcMillis),
true,
- flag);
+ flags);
}
/**
@@ -422,16 +427,20 @@ public class Utils {
boolean useShortFormat,
boolean showDate,
boolean showTime,
- int flag) {
- flag |=
+ int flags) {
+ flags |=
DateUtils.FORMAT_ABBREV_MONTH
| ((useShortFormat) ? DateUtils.FORMAT_NUMERIC_DATE : 0);
SoftPreconditions.checkArgument(showTime || showDate);
if (showTime) {
- flag |= DateUtils.FORMAT_SHOW_TIME;
+ flags |= DateUtils.FORMAT_SHOW_TIME;
}
if (showDate) {
- flag |= DateUtils.FORMAT_SHOW_DATE;
+ flags |= DateUtils.FORMAT_SHOW_DATE;
+ }
+ if (!showDate || (flags & DateUtils.FORMAT_SHOW_YEAR) == 0) {
+ // year is not shown unless DateUtils.FORMAT_SHOW_YEAR is set explicitly
+ flags |= DateUtils.FORMAT_NO_YEAR;
}
if (startUtcMillis != endUtcMillis && useShortFormat) {
// Do special handling for 12:00 AM when checking if it's in the given day.
@@ -443,15 +452,15 @@ public class Utils {
// Subtracting one day is needed because {@link DateUtils@formatDateRange}
// automatically shows date if the duration covers multiple days.
return DateUtils.formatDateRange(
- context, startUtcMillis, endUtcMillis - TimeUnit.DAYS.toMillis(1), flag);
+ context, startUtcMillis, endUtcMillis - TimeUnit.DAYS.toMillis(1), flags);
}
}
// Workaround of b/28740989.
// Add 1 msec to endUtcMillis to avoid DateUtils' bug with a duration of 12:00AM~12:00AM.
- String dateRange = DateUtils.formatDateRange(context, startUtcMillis, endUtcMillis, flag);
+ String dateRange = DateUtils.formatDateRange(context, startUtcMillis, endUtcMillis, flags);
return startUtcMillis == endUtcMillis || dateRange.contains("–")
? dateRange
- : DateUtils.formatDateRange(context, startUtcMillis, endUtcMillis + 1, flag);
+ : DateUtils.formatDateRange(context, startUtcMillis, endUtcMillis + 1, flags);
}
/**
@@ -572,86 +581,6 @@ public class Utils {
return "";
}
- public static boolean needToShowSampleRate(Context context, List<TvTrackInfo> tracks) {
- Set<String> multiAudioStrings = new HashSet<>();
- for (TvTrackInfo track : tracks) {
- String multiAudioString = getMultiAudioString(context, track, false);
- if (multiAudioStrings.contains(multiAudioString)) {
- return true;
- }
- multiAudioStrings.add(multiAudioString);
- }
- return false;
- }
-
- public static String getMultiAudioString(
- Context context, TvTrackInfo track, boolean showSampleRate) {
- if (track.getType() != TvTrackInfo.TYPE_AUDIO) {
- throw new IllegalArgumentException("Not an audio track: " + track);
- }
- String language = context.getString(R.string.multi_audio_unknown_language);
- if (!TextUtils.isEmpty(track.getLanguage())) {
- language = new Locale(track.getLanguage()).getDisplayName();
- } else {
- Log.d(TAG, "No language information found for the audio track: " + track);
- }
-
- StringBuilder metadata = new StringBuilder();
- switch (track.getAudioChannelCount()) {
- case AUDIO_CHANNEL_NONE:
- break;
- case AUDIO_CHANNEL_MONO:
- metadata.append(context.getString(R.string.multi_audio_channel_mono));
- break;
- case AUDIO_CHANNEL_STEREO:
- metadata.append(context.getString(R.string.multi_audio_channel_stereo));
- break;
- case AUDIO_CHANNEL_SURROUND_6:
- metadata.append(context.getString(R.string.multi_audio_channel_surround_6));
- break;
- case AUDIO_CHANNEL_SURROUND_8:
- metadata.append(context.getString(R.string.multi_audio_channel_surround_8));
- break;
- default:
- if (track.getAudioChannelCount() > 0) {
- metadata.append(
- context.getString(
- R.string.multi_audio_channel_suffix,
- track.getAudioChannelCount()));
- } else {
- Log.d(
- TAG,
- "Invalid audio channel count ("
- + track.getAudioChannelCount()
- + ") found for the audio track: "
- + track);
- }
- break;
- }
- if (showSampleRate) {
- int sampleRate = track.getAudioSampleRate();
- if (sampleRate > 0) {
- if (metadata.length() > 0) {
- metadata.append(", ");
- }
- int integerPart = sampleRate / 1000;
- int tenths = (sampleRate % 1000) / 100;
- metadata.append(integerPart);
- if (tenths != 0) {
- metadata.append(".");
- metadata.append(tenths);
- }
- metadata.append("kHz");
- }
- }
-
- if (metadata.length() == 0) {
- return language;
- }
- return context.getString(
- R.string.multi_audio_display_string_with_channel, language, metadata.toString());
- }
-
public static boolean isEqualLanguage(String lang1, String lang2) {
if (lang1 == null) {
return lang2 == null;
@@ -708,7 +637,6 @@ public class Utils {
if (fullFormat) {
return new Date(timeMillis).toString();
} else {
- long currentTime = System.currentTimeMillis();
return (String)
DateUtils.formatSameDayTime(
timeMillis,
@@ -815,8 +743,11 @@ public class Utils {
/** Checks whether the input is internal or not. */
public static boolean isInternalTvInput(Context context, String inputId) {
- return context.getPackageName()
- .equals(ComponentName.unflattenFromString(inputId).getPackageName());
+ ComponentName unflattenInputId = ComponentName.unflattenFromString(inputId);
+ if (unflattenInputId == null) {
+ return false;
+ }
+ return context.getPackageName().equals(unflattenInputId.getPackageName());
}
/** Returns the TV input for the given {@code program}. */
diff --git a/src/com/android/tv/util/images/BitmapUtils.java b/src/com/android/tv/util/images/BitmapUtils.java
index d6bd5a31..39524503 100644
--- a/src/com/android/tv/util/images/BitmapUtils.java
+++ b/src/com/android/tv/util/images/BitmapUtils.java
@@ -20,13 +20,16 @@ import android.content.ContentResolver;
import android.content.Context;
import android.database.sqlite.SQLiteException;
import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.net.TrafficStats;
import android.net.Uri;
import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.Log;
import com.android.tv.common.util.NetworkTrafficTags;
@@ -88,6 +91,19 @@ public final class BitmapUtils {
calculateInSampleSize(bm.getWidth(), bm.getHeight(), maxWidth, maxHeight));
}
+ @Nullable
+ public static Bitmap drawableToBitmap(Drawable drawable) {
+ if (drawable == null) {
+ return null;
+ }
+ Bitmap bm = Bitmap.createBitmap(
+ drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Config.ARGB_8888);
+ Canvas canvas = new Canvas(bm);
+ drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
+ drawable.draw(canvas);
+ return bm;
+ }
+
/** Decode large sized bitmap into requested size. */
public static ScaledBitmapInfo decodeSampledBitmapFromUriString(
Context context, String uriString, int reqWidth, int reqHeight) {
diff --git a/src/com/android/tv/util/images/ImageLoader.java b/src/com/android/tv/util/images/ImageLoader.java
index e844e2ca..d2ad0eb1 100644
--- a/src/com/android/tv/util/images/ImageLoader.java
+++ b/src/com/android/tv/util/images/ImageLoader.java
@@ -24,7 +24,6 @@ import android.media.tv.TvInputInfo;
import android.os.AsyncTask;
import android.os.Handler;
import android.os.Looper;
-import android.support.annotation.MainThread;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
import android.support.annotation.WorkerThread;
@@ -145,22 +144,14 @@ public final class ImageLoader {
final Context appContext = context.getApplicationContext();
getMainHandler()
.post(
- new Runnable() {
- @Override
- @MainThread
- public void run() {
- // Calling from the main thread prevents a
- // ConcurrentModificationException
- // in LoadBitmapTask.onPostExecute
+ () ->
doLoadBitmap(
appContext,
uriString,
maxWidth,
maxHeight,
null,
- AsyncTask.SERIAL_EXECUTOR);
- }
- });
+ AsyncTask.SERIAL_EXECUTOR));
}
}
@@ -423,14 +414,12 @@ public final class ImageLoader {
@Override
public ScaledBitmapInfo doGetBitmapInBackground() {
Drawable drawable = mInfo.loadIcon(mAppContext);
- if (!(drawable instanceof BitmapDrawable)) {
- return null;
- }
- Bitmap original = ((BitmapDrawable) drawable).getBitmap();
- if (original == null) {
- return null;
- }
- return BitmapUtils.createScaledBitmapInfo(getKey(), original, mMaxWidth, mMaxHeight);
+ Bitmap bm = drawable instanceof BitmapDrawable
+ ? ((BitmapDrawable) drawable).getBitmap()
+ : BitmapUtils.drawableToBitmap(drawable);
+ return bm == null
+ ? null
+ : BitmapUtils.createScaledBitmapInfo(getKey(), bm, mMaxWidth, mMaxHeight);
}
/** Returns key of TV input logo. */
diff --git a/tests/common/Android.mk b/tests/common/Android.mk
index 3ab16c02..7a111d0c 100644
--- a/tests/common/Android.mk
+++ b/tests/common/Android.mk
@@ -8,11 +8,11 @@ LOCAL_SRC_FILES := \
LOCAL_STATIC_JAVA_LIBRARIES := \
android-support-annotations \
- android-support-test \
- guava \
+ androidx.test.runner \
+ androidx.test.rules \
+ tv-guava-android-jar \
mockito-target \
- platform-robolectric-3.6.2-prebuilt \
- truth-0-36-prebuilt-jar \
+ tv-lib-truth \
ub-uiautomator \
# Link tv-common as shared library to avoid the problem of initialization of the constants
diff --git a/tests/common/AndroidManifest.xml b/tests/common/AndroidManifest.xml
index 8afd8dc9..3a769a8d 100644
--- a/tests/common/AndroidManifest.xml
+++ b/tests/common/AndroidManifest.xml
@@ -18,6 +18,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.tv.testing"
android:versionCode="1">
- <uses-sdk android:targetSdkVersion="26" android:minSdkVersion="21"/>
+ <uses-sdk android:targetSdkVersion="27" android:minSdkVersion="23"/>
<application />
</manifest>
diff --git a/tests/common/src/com/android/tv/testing/DbTestingUtils.java b/tests/common/src/com/android/tv/testing/DbTestingUtils.java
index 53e26ca7..e71a714f 100644
--- a/tests/common/src/com/android/tv/testing/DbTestingUtils.java
+++ b/tests/common/src/com/android/tv/testing/DbTestingUtils.java
@@ -29,7 +29,7 @@ public final class DbTestingUtils {
while (cursor.moveToNext()) {
List<String> row = new ArrayList<>(colCount);
for (int i = 0; i < colCount; i++) {
- row.add(cursor.getString(i));
+ row.add(cursor.isNull(i) ? "null" : cursor.getString(i));
}
result.add(row);
}
diff --git a/tests/common/src/com/android/tv/testing/EpgTestData.java b/tests/common/src/com/android/tv/testing/EpgTestData.java
index 49a92181..362f336a 100644
--- a/tests/common/src/com/android/tv/testing/EpgTestData.java
+++ b/tests/common/src/com/android/tv/testing/EpgTestData.java
@@ -30,18 +30,19 @@ import java.util.concurrent.TimeUnit;
/** EPG data for use in tests. */
public abstract class EpgTestData {
- public static final android.support.media.tv.Channel CHANNEL_10 =
- new android.support.media.tv.Channel.Builder()
+ public static final androidx.tvprovider.media.tv.Channel CHANNEL_10 =
+ new androidx.tvprovider.media.tv.Channel.Builder()
.setDisplayName("Channel TEN")
.setDisplayNumber("10")
+ .setNetworkAffiliation("Channel 10 Network Affiliation")
.build();
- public static final android.support.media.tv.Channel CHANNEL_11 =
- new android.support.media.tv.Channel.Builder()
+ public static final androidx.tvprovider.media.tv.Channel CHANNEL_11 =
+ new androidx.tvprovider.media.tv.Channel.Builder()
.setDisplayName("Channel Eleven")
.setDisplayNumber("11")
.build();
- public static final android.support.media.tv.Channel CHANNEL_90_2 =
- new android.support.media.tv.Channel.Builder()
+ public static final androidx.tvprovider.media.tv.Channel CHANNEL_90_2 =
+ new androidx.tvprovider.media.tv.Channel.Builder()
.setDisplayName("Channel Ninety dot Two")
.setDisplayNumber("90.2")
.build();
@@ -162,21 +163,23 @@ public abstract class EpgTestData {
loadData(testSingletonApp.fakeClock, testSingletonApp.epgReader);
}
- private static Iterable<Channel> toTvChannels(android.support.media.tv.Channel... channels) {
+ private static Iterable<Channel> toTvChannels(
+ androidx.tvprovider.media.tv.Channel... channels) {
return Iterables.transform(
ImmutableList.copyOf(channels),
- new Function<android.support.media.tv.Channel, Channel>() {
+ new Function<androidx.tvprovider.media.tv.Channel, Channel>() {
@Override
- public Channel apply(android.support.media.tv.Channel original) {
+ public Channel apply(androidx.tvprovider.media.tv.Channel original) {
return toTvChannel(original);
}
});
}
- public static Channel toTvChannel(android.support.media.tv.Channel original) {
+ public static Channel toTvChannel(androidx.tvprovider.media.tv.Channel original) {
return new ChannelImpl.Builder()
.setDisplayName(original.getDisplayName())
.setDisplayNumber(original.getDisplayNumber())
+ .setNetworkAffiliation(original.getNetworkAffiliation())
// TODO implement the reset
.build();
}
diff --git a/tests/common/src/com/android/tv/testing/FakeEpgReader.java b/tests/common/src/com/android/tv/testing/FakeEpgReader.java
index 710ada55..fb35c652 100644
--- a/tests/common/src/com/android/tv/testing/FakeEpgReader.java
+++ b/tests/common/src/com/android/tv/testing/FakeEpgReader.java
@@ -37,6 +37,7 @@ import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Set;
/** Fake {@link EpgReader} for testing. */
@@ -93,7 +94,15 @@ public final class FakeEpgReader implements EpgReader {
if (match != null) {
ChannelImpl updatedChannel = new ChannelImpl.Builder(match).build();
updatedChannel.setLogoUri(channel.getLogoUri());
- result.add(EpgChannel.createEpgChannel(updatedChannel, channel.getDisplayNumber()));
+ boolean dbUpdateNeeded = false;
+ if (!Objects.equals(
+ channel.getNetworkAffiliation(), updatedChannel.getNetworkAffiliation())) {
+ dbUpdateNeeded = true;
+ updatedChannel.setNetworkAffiliation(channel.getNetworkAffiliation());
+ }
+ result.add(
+ EpgChannel.createEpgChannel(
+ updatedChannel, channel.getDisplayNumber(), dbUpdateNeeded));
}
}
return result;
diff --git a/tests/common/src/com/android/tv/testing/FakeRemoteConfig.java b/tests/common/src/com/android/tv/testing/FakeRemoteConfig.java
deleted file mode 100644
index 89e6a0a2..00000000
--- a/tests/common/src/com/android/tv/testing/FakeRemoteConfig.java
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.tv.testing;
-
-import android.text.TextUtils;
-import com.android.tv.common.config.api.RemoteConfig;
-import java.util.HashMap;
-import java.util.Map;
-
-/** Fake {@link RemoteConfig} suitable for testing. */
-public class FakeRemoteConfig implements RemoteConfig {
- public final Map<String, String> values = new HashMap();
-
- @Override
- public void fetch(OnRemoteConfigUpdatedListener listener) {}
-
- @Override
- public String getString(String key) {
- return values.get(key);
- }
-
- @Override
- public boolean getBoolean(String key) {
- String value = values.get(key);
- return TextUtils.isEmpty(value) ? false : Boolean.valueOf(key);
- }
-
- @Override
- public long getLong(String key) {
- return getLong(key, 0);
- }
-
- @Override
- public long getLong(String key, long defaultValue) {
- if (values.containsKey(key)) {
- String value = values.get(key);
- return TextUtils.isEmpty(value) ? defaultValue : Long.valueOf(value);
- }
- return defaultValue;
- }
-}
diff --git a/tests/common/src/com/android/tv/testing/FakeTvProvider.java b/tests/common/src/com/android/tv/testing/FakeTvProvider.java
index 24c26f39..20903c60 100644
--- a/tests/common/src/com/android/tv/testing/FakeTvProvider.java
+++ b/tests/common/src/com/android/tv/testing/FakeTvProvider.java
@@ -44,16 +44,16 @@ import android.os.ParcelFileDescriptor.AutoCloseInputStream;
import android.preference.PreferenceManager;
import android.provider.BaseColumns;
import android.support.annotation.VisibleForTesting;
-import android.support.media.tv.TvContractCompat;
-import android.support.media.tv.TvContractCompat.BaseTvColumns;
-import android.support.media.tv.TvContractCompat.Channels;
-import android.support.media.tv.TvContractCompat.PreviewPrograms;
-import android.support.media.tv.TvContractCompat.Programs;
-import android.support.media.tv.TvContractCompat.Programs.Genres;
-import android.support.media.tv.TvContractCompat.RecordedPrograms;
-import android.support.media.tv.TvContractCompat.WatchNextPrograms;
import android.text.TextUtils;
import android.util.Log;
+import androidx.tvprovider.media.tv.TvContractCompat;
+import androidx.tvprovider.media.tv.TvContractCompat.BaseTvColumns;
+import androidx.tvprovider.media.tv.TvContractCompat.Channels;
+import androidx.tvprovider.media.tv.TvContractCompat.PreviewPrograms;
+import androidx.tvprovider.media.tv.TvContractCompat.Programs;
+import androidx.tvprovider.media.tv.TvContractCompat.Programs.Genres;
+import androidx.tvprovider.media.tv.TvContractCompat.RecordedPrograms;
+import androidx.tvprovider.media.tv.TvContractCompat.WatchNextPrograms;
import com.android.tv.util.SqlParams;
import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
diff --git a/tests/common/src/com/android/tv/testing/TestSingletonApp.java b/tests/common/src/com/android/tv/testing/TestSingletonApp.java
index f55ed8d4..f1a98ff5 100644
--- a/tests/common/src/com/android/tv/testing/TestSingletonApp.java
+++ b/tests/common/src/com/android/tv/testing/TestSingletonApp.java
@@ -17,9 +17,6 @@
package com.android.tv.testing;
import android.app.Application;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
import android.media.tv.TvInputManager;
import android.os.AsyncTask;
import com.android.tv.InputSessionManager;
@@ -28,9 +25,14 @@ import com.android.tv.TvSingletons;
import com.android.tv.analytics.Analytics;
import com.android.tv.analytics.Tracker;
import com.android.tv.common.BaseApplication;
-import com.android.tv.common.config.api.RemoteConfig;
import com.android.tv.common.experiments.ExperimentLoader;
+import com.android.tv.common.flags.impl.DefaultBackendKnobsFlags;
+import com.android.tv.common.flags.impl.DefaultCloudEpgFlags;
+import com.android.tv.common.flags.impl.DefaultConcurrentDvrPlaybackFlags;
+import com.android.tv.common.flags.impl.DefaultTunerFlags;
+import com.android.tv.common.flags.impl.DefaultUiFlags;
import com.android.tv.common.recording.RecordingStorageStatusManager;
+import com.android.tv.common.singletons.HasSingletons;
import com.android.tv.common.util.Clock;
import com.android.tv.data.ChannelDataManager;
import com.android.tv.data.PreviewDataManager;
@@ -43,21 +45,27 @@ import com.android.tv.dvr.DvrScheduleManager;
import com.android.tv.dvr.DvrWatchedPositionManager;
import com.android.tv.dvr.recorder.RecordingScheduler;
import com.android.tv.perf.PerformanceMonitor;
-import com.android.tv.perf.StubPerformanceMonitor;
+import com.android.tv.perf.stub.StubPerformanceMonitor;
import com.android.tv.testing.dvr.DvrDataManagerInMemoryImpl;
import com.android.tv.testing.testdata.TestData;
-import com.android.tv.tuner.TunerInputController;
+import com.android.tv.tuner.singletons.TunerSingletons;
+import com.android.tv.tuner.source.TsDataSourceManager;
+import com.android.tv.tuner.source.TunerTsStreamerManager;
+import com.android.tv.tuner.tvinput.factory.TunerSessionFactory;
+import com.android.tv.tuner.tvinput.factory.TunerSessionFactoryImpl;
+import com.android.tv.tunerinputcontroller.BuiltInTunerManager;
import com.android.tv.util.SetupUtils;
import com.android.tv.util.TvInputManagerHelper;
import com.android.tv.util.account.AccountHelper;
+import com.google.common.base.Optional;
import java.util.concurrent.Executor;
import javax.inject.Provider;
/** Test application for Live TV. */
-public class TestSingletonApp extends Application implements TvSingletons {
+public class TestSingletonApp extends Application
+ implements TvSingletons, TunerSingletons, HasSingletons<TvSingletons> {
public final FakeClock fakeClock = FakeClock.createWithCurrentTime();
public final FakeEpgReader epgReader = new FakeEpgReader(fakeClock);
- public final FakeRemoteConfig remoteConfig = new FakeRemoteConfig();
public final FakeEpgFetcher epgFetcher = new FakeEpgFetcher();
public FakeTvInputManagerHelper tvInputManagerHelper;
@@ -66,19 +74,27 @@ public class TestSingletonApp extends Application implements TvSingletons {
public DvrDataManager mDvrDataManager;
private final Provider<EpgReader> mEpgReaderProvider = SingletonProvider.create(epgReader);
- private TunerInputController mTunerInputController;
+ private final Optional<BuiltInTunerManager> mBuiltInTunerManagerOptional = Optional.absent();
+ private final DefaultBackendKnobsFlags mBackendKnobs = new DefaultBackendKnobsFlags();
+ private final DefaultCloudEpgFlags mCloudEpgFlags = new DefaultCloudEpgFlags();
+ private final DefaultUiFlags mUiFlags = new DefaultUiFlags();
+ private final DefaultConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags =
+ new DefaultConcurrentDvrPlaybackFlags();
+ private final TsDataSourceManager.Factory mTsDataSourceManagerFactory =
+ new TsDataSourceManager.Factory(() -> new TunerTsStreamerManager(null));
+ private final TunerSessionFactoryImpl mTunerSessionFactory =
+ new TunerSessionFactoryImpl(
+ new DefaultTunerFlags(),
+ mConcurrentDvrPlaybackFlags,
+ mTsDataSourceManagerFactory);
private PerformanceMonitor mPerformanceMonitor;
private ChannelDataManager mChannelDataManager;
@Override
public void onCreate() {
super.onCreate();
- mTunerInputController =
- new TunerInputController(
- ComponentName.unflattenFromString(getEmbeddedTunerInputId()));
-
tvInputManagerHelper = new FakeTvInputManagerHelper(this);
- setupUtils = SetupUtils.createForTvSingletons(this);
+ setupUtils = new SetupUtils(this, mBuiltInTunerManagerOptional);
tvInputManagerHelper.start();
mChannelDataManager = new ChannelDataManager(this, tvInputManagerHelper);
mChannelDataManager.start();
@@ -154,7 +170,7 @@ public class TestSingletonApp extends Application implements TvSingletons {
@Override
public InputSessionManager getInputSessionManager() {
- return null;
+ return new InputSessionManager(this);
}
@Override
@@ -183,8 +199,8 @@ public class TestSingletonApp extends Application implements TvSingletons {
}
@Override
- public TunerInputController getTunerInputController() {
- return mTunerInputController;
+ public Optional<BuiltInTunerManager> getBuiltInTunerManager() {
+ return mBuiltInTunerManagerOptional;
}
@Override
@@ -213,16 +229,6 @@ public class TestSingletonApp extends Application implements TvSingletons {
}
@Override
- public RemoteConfig getRemoteConfig() {
- return remoteConfig;
- }
-
- @Override
- public Intent getTunerSetupIntent(Context context) {
- return null;
- }
-
- @Override
public boolean isRunningInMainProcess() {
return false;
}
@@ -244,4 +250,38 @@ public class TestSingletonApp extends Application implements TvSingletons {
public Executor getDbExecutor() {
return AsyncTask.SERIAL_EXECUTOR;
}
+
+ @Override
+ public DefaultBackendKnobsFlags getBackendKnobs() {
+ return mBackendKnobs;
+ }
+
+ @Override
+ public DefaultCloudEpgFlags getCloudEpgFlags() {
+ return mCloudEpgFlags;
+ }
+
+ @Override
+ public DefaultUiFlags getUiFlags() {
+ return mUiFlags;
+ }
+
+ @Override
+ public BuildType getBuildType() {
+ return BuildType.ENG;
+ }
+
+ @Override
+ public DefaultConcurrentDvrPlaybackFlags getConcurrentDvrPlaybackFlags() {
+ return mConcurrentDvrPlaybackFlags;
+ }
+
+ public TunerSessionFactory getTunerSessionFactory() {
+ return mTunerSessionFactory;
+ }
+
+ @Override
+ public TvSingletons singletons() {
+ return this;
+ }
}
diff --git a/tests/common/src/com/android/tv/testing/activities/BaseMainActivityTestCase.java b/tests/common/src/com/android/tv/testing/activities/BaseMainActivityTestCase.java
index 666f8181..495ff20d 100644
--- a/tests/common/src/com/android/tv/testing/activities/BaseMainActivityTestCase.java
+++ b/tests/common/src/com/android/tv/testing/activities/BaseMainActivityTestCase.java
@@ -15,12 +15,12 @@
*/
package com.android.tv.testing.activities;
-import static android.support.test.InstrumentationRegistry.getInstrumentation;
+import static androidx.test.InstrumentationRegistry.getInstrumentation;
import android.content.Context;
import android.os.SystemClock;
-import android.support.test.rule.ActivityTestRule;
import android.text.TextUtils;
+import androidx.test.rule.ActivityTestRule;
import com.android.tv.MainActivity;
import com.android.tv.data.ChannelDataManager;
import com.android.tv.data.api.Channel;
diff --git a/tests/common/src/com/android/tv/testing/data/ProgramInfo.java b/tests/common/src/com/android/tv/testing/data/ProgramInfo.java
index 6d801425..3e7b608e 100644
--- a/tests/common/src/com/android/tv/testing/data/ProgramInfo.java
+++ b/tests/common/src/com/android/tv/testing/data/ProgramInfo.java
@@ -22,7 +22,7 @@ import android.media.tv.TvContentRating;
import android.media.tv.TvContract;
import com.android.tv.testing.R;
import com.android.tv.testing.utils.Utils;
-import java.util.Arrays;
+import com.google.common.collect.ImmutableList;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
@@ -100,7 +100,7 @@ public final class ProgramInfo {
public final String description;
public final long durationMs;
public final String genre;
- public final TvContentRating[] contentRatings;
+ public final ImmutableList<TvContentRating> contentRatings;
public final String resourceUri;
public static ProgramInfo fromCursor(Cursor c) {
@@ -129,7 +129,7 @@ public final class ProgramInfo {
String posterArtUri,
String description,
long durationMs,
- TvContentRating[] contentRatings,
+ ImmutableList<TvContentRating> contentRatings,
String genre,
String resourceUri) {
this.title = title;
@@ -248,7 +248,7 @@ public final class ProgramInfo {
&& Objects.equals(posterArtUri, that.posterArtUri)
&& Objects.equals(description, that.description)
&& Objects.equals(genre, that.genre)
- && Arrays.equals(contentRatings, that.contentRatings)
+ && Objects.equals(contentRatings, that.contentRatings)
&& Objects.equals(resourceUri, that.resourceUri);
}
@@ -265,7 +265,7 @@ public final class ProgramInfo {
private String mPosterArtUri = GEN_POSTER;
private String mDescription;
private long mDurationMs = GEN_DURATION;
- private TvContentRating[] mContentRatings;
+ private ImmutableList<TvContentRating> mContentRatings;
private String mGenre = GEN_GENRE;
private String mResourceUri;
@@ -304,7 +304,7 @@ public final class ProgramInfo {
return this;
}
- public Builder setContentRatings(TvContentRating[] contentRatings) {
+ public Builder setContentRatings(ImmutableList<TvContentRating> contentRatings) {
mContentRatings = contentRatings;
return this;
}
diff --git a/tests/common/src/com/android/tv/testing/dvr/DvrDataManagerInMemoryImpl.java b/tests/common/src/com/android/tv/testing/dvr/DvrDataManagerInMemoryImpl.java
index b8a055c7..66707fb6 100644
--- a/tests/common/src/com/android/tv/testing/dvr/DvrDataManagerInMemoryImpl.java
+++ b/tests/common/src/com/android/tv/testing/dvr/DvrDataManagerInMemoryImpl.java
@@ -204,11 +204,7 @@ public final class DvrDataManagerInMemoryImpl extends BaseDvrDataManager {
recordedProgram.getId() == RecordedProgram.ID_NOT_SET,
TAG,
"expected id of " + RecordedProgram.ID_NOT_SET + " but was " + recordedProgram);
- recordedProgram =
- RecordedProgram
- .buildFrom(recordedProgram)
- .setId(mNextId.incrementAndGet())
- .build();
+ recordedProgram = recordedProgram.withId(mNextId.incrementAndGet());
}
mRecordedPrograms.put(recordedProgram.getId(), recordedProgram);
notifyRecordedProgramsAdded(recordedProgram);
diff --git a/tests/common/src/com/android/tv/testing/robo/ContentProviders.java b/tests/common/src/com/android/tv/testing/robo/ContentProviders.java
deleted file mode 100644
index aaaa11df..00000000
--- a/tests/common/src/com/android/tv/testing/robo/ContentProviders.java
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-package com.android.tv.testing.robo;
-
-import android.content.ContentProvider;
-import android.content.pm.ProviderInfo;
-import org.robolectric.Robolectric;
-import org.robolectric.android.controller.ContentProviderController;
-import org.robolectric.shadows.ShadowContentResolver;
-
-/** Static utilities for using content providers in tests. */
-public final class ContentProviders {
-
- /** Builds creates and register a ContentProvider with the given authority. */
- public static <T extends ContentProvider> T register(Class<T> providerClass, String authority) {
- ProviderInfo info = new ProviderInfo();
- info.authority = authority;
- ContentProviderController<T> contentProviderController =
- Robolectric.buildContentProvider(providerClass);
- T provider = contentProviderController.create(info).get();
- provider.onCreate();
- ShadowContentResolver.registerProviderInternal(authority, provider);
- return provider;
- }
-
- private ContentProviders() {}
-}
diff --git a/tests/common/src/com/android/tv/testing/robo/RobotTestAppHelper.java b/tests/common/src/com/android/tv/testing/robo/RobotTestAppHelper.java
deleted file mode 100644
index 9eb79298..00000000
--- a/tests/common/src/com/android/tv/testing/robo/RobotTestAppHelper.java
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-package com.android.tv.testing.robo;
-
-import android.media.tv.TvContract;
-import com.android.tv.testing.FakeTvProvider;
-import com.android.tv.testing.TestSingletonApp;
-import com.android.tv.testing.testdata.TestData;
-import java.util.concurrent.TimeUnit;
-import org.robolectric.Robolectric;
-
-/** Static utilities for using {@link TestSingletonApp} in roboletric tests. */
-public final class RobotTestAppHelper {
-
- public static void loadTestData(TestSingletonApp app, TestData testData) {
- ContentProviders.register(FakeTvProvider.class, TvContract.AUTHORITY);
- app.loadTestData(testData, TimeUnit.DAYS.toMillis(1));
- Robolectric.flushBackgroundThreadScheduler();
- Robolectric.flushForegroundThreadScheduler();
- }
-
- private RobotTestAppHelper() {}
-}
diff --git a/tests/common/src/com/android/tv/testing/shadows/ShadowMediaSession.java b/tests/common/src/com/android/tv/testing/shadows/ShadowMediaSession.java
deleted file mode 100644
index 5a2c41e6..00000000
--- a/tests/common/src/com/android/tv/testing/shadows/ShadowMediaSession.java
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.tv.testing.shadows;
-
-import android.app.PendingIntent;
-import android.content.Context;
-import android.media.MediaMetadata;
-import android.media.session.MediaSession;
-import android.media.session.PlaybackState;
-import org.robolectric.annotation.Implementation;
-import org.robolectric.annotation.Implements;
-
-/** Shadow {@link MediaSession}. */
-@Implements(MediaSession.class)
-public class ShadowMediaSession {
-
- public MediaSession.Callback mCallback;
- public PendingIntent mMediaButtonReceiver;
- public PendingIntent mSessionActivity;
- public PlaybackState mPlaybackState;
- public MediaMetadata mMediaMetadata;
- public int mFlags;
- public boolean mActive;
- public boolean mReleased;
-
- /** Stand-in for the MediaSession constructor with the same parameters. */
- public void __constructor__(Context context, String tag, int userID) {
- // This empty method prevents the real MediaSession constructor from being called.
- }
-
- @Implementation
- public void setCallback(MediaSession.Callback callback) {
- mCallback = callback;
- }
-
- @Implementation
- public void setMediaButtonReceiver(PendingIntent mbr) {
- mMediaButtonReceiver = mbr;
- }
-
- @Implementation
- public void setSessionActivity(PendingIntent activity) {
- mSessionActivity = activity;
- }
-
- @Implementation
- public void setPlaybackState(PlaybackState state) {
- mPlaybackState = state;
- }
-
- @Implementation
- public void setMetadata(MediaMetadata metadata) {
- mMediaMetadata = metadata;
- }
-
- @Implementation
- public void setFlags(int flags) {
- mFlags = flags;
- }
-
- @Implementation
- public boolean isActive() {
- return mActive;
- }
-
- @Implementation
- public void setActive(boolean active) {
- mActive = active;
- }
-
- @Implementation
- public void release() {
- mReleased = true;
- }
-}
diff --git a/tests/func/Android.mk b/tests/func/Android.mk
index 855e8ebf..53c869ee 100644
--- a/tests/func/Android.mk
+++ b/tests/func/Android.mk
@@ -10,7 +10,7 @@ LOCAL_SRC_FILES := $(call all-java-files-under, src)
LOCAL_PACKAGE_NAME := TVFuncTests
LOCAL_STATIC_JAVA_LIBRARIES := \
- android-support-test \
+ androidx.test.runner \
tv-test-common \
ub-uiautomator \
diff --git a/tests/func/AndroidManifest.xml b/tests/func/AndroidManifest.xml
index 708dc220..3d7d775f 100644
--- a/tests/func/AndroidManifest.xml
+++ b/tests/func/AndroidManifest.xml
@@ -18,10 +18,10 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.tv.tests.ui" >
- <uses-sdk android:targetSdkVersion="26" android:minSdkVersion="21" />
+ <uses-sdk android:targetSdkVersion="27" android:minSdkVersion="23" />
<instrumentation
- android:name="android.support.test.runner.AndroidJUnitRunner"
+ android:name="androidx.test.runner.AndroidJUnitRunner"
android:label="Live Channel Functional Tests"
android:targetPackage="com.android.tv" />
diff --git a/tests/func/src/com/android/tv/tests/ui/ChannelBannerViewTest.java b/tests/func/src/com/android/tv/tests/ui/ChannelBannerViewTest.java
index 600b52b6..c06c859c 100644
--- a/tests/func/src/com/android/tv/tests/ui/ChannelBannerViewTest.java
+++ b/tests/func/src/com/android/tv/tests/ui/ChannelBannerViewTest.java
@@ -16,8 +16,8 @@
package com.android.tv.tests.ui;
-import android.support.test.filters.MediumTest;
import android.support.test.uiautomator.Until;
+import androidx.test.filters.MediumTest;
import com.android.tv.R;
import com.android.tv.testing.uihelper.Constants;
import org.junit.Before;
diff --git a/tests/func/src/com/android/tv/tests/ui/ChannelSourcesTest.java b/tests/func/src/com/android/tv/tests/ui/ChannelSourcesTest.java
index 53e27f1b..2467de21 100644
--- a/tests/func/src/com/android/tv/tests/ui/ChannelSourcesTest.java
+++ b/tests/func/src/com/android/tv/tests/ui/ChannelSourcesTest.java
@@ -15,9 +15,9 @@
*/
package com.android.tv.tests.ui;
-import android.support.test.filters.MediumTest;
import android.support.test.uiautomator.BySelector;
import android.support.test.uiautomator.Until;
+import androidx.test.filters.MediumTest;
import com.android.tv.R;
import com.android.tv.testing.uihelper.ByResource;
import org.junit.Before;
diff --git a/tests/func/src/com/android/tv/tests/ui/LiveChannelsAppTest.java b/tests/func/src/com/android/tv/tests/ui/LiveChannelsAppTest.java
index 1a5ceb45..ac2aad43 100644
--- a/tests/func/src/com/android/tv/tests/ui/LiveChannelsAppTest.java
+++ b/tests/func/src/com/android/tv/tests/ui/LiveChannelsAppTest.java
@@ -16,9 +16,9 @@
package com.android.tv.tests.ui;
-import android.support.test.filters.MediumTest;
import android.support.test.uiautomator.BySelector;
import android.support.test.uiautomator.Until;
+import androidx.test.filters.MediumTest;
import com.android.tv.R;
import com.android.tv.testing.testinput.ChannelStateData;
import com.android.tv.testing.testinput.TvTestInputConstants;
diff --git a/tests/func/src/com/android/tv/tests/ui/LiveChannelsTestController.java b/tests/func/src/com/android/tv/tests/ui/LiveChannelsTestController.java
index 03d30ca4..fa3335d9 100644
--- a/tests/func/src/com/android/tv/tests/ui/LiveChannelsTestController.java
+++ b/tests/func/src/com/android/tv/tests/ui/LiveChannelsTestController.java
@@ -23,7 +23,6 @@ import android.content.Context;
import android.content.res.Resources;
import android.os.Build;
import android.os.SystemClock;
-import android.support.test.InstrumentationRegistry;
import android.support.test.uiautomator.BySelector;
import android.support.test.uiautomator.Configurator;
import android.support.test.uiautomator.SearchCondition;
@@ -31,6 +30,7 @@ import android.support.test.uiautomator.UiDevice;
import android.support.test.uiautomator.Until;
import android.view.InputDevice;
import android.view.KeyEvent;
+import androidx.test.InstrumentationRegistry;
import com.android.tv.testing.data.ChannelInfo;
import com.android.tv.testing.testinput.ChannelStateData;
import com.android.tv.testing.testinput.TestInputControlConnection;
diff --git a/tests/func/src/com/android/tv/tests/ui/ParentalControlsTest.java b/tests/func/src/com/android/tv/tests/ui/ParentalControlsTest.java
index ee039d7c..bff0e7d7 100644
--- a/tests/func/src/com/android/tv/tests/ui/ParentalControlsTest.java
+++ b/tests/func/src/com/android/tv/tests/ui/ParentalControlsTest.java
@@ -19,10 +19,10 @@ package com.android.tv.tests.ui;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertTrue;
-import android.support.test.filters.MediumTest;
import android.support.test.uiautomator.BySelector;
import android.support.test.uiautomator.UiObject2;
import android.support.test.uiautomator.Until;
+import androidx.test.filters.MediumTest;
import com.android.tv.R;
import com.android.tv.testing.uihelper.ByResource;
import com.android.tv.testing.uihelper.DialogHelper;
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 7c982782..efc7ecf4 100644
--- a/tests/func/src/com/android/tv/tests/ui/PlayControlsRowViewTest.java
+++ b/tests/func/src/com/android/tv/tests/ui/PlayControlsRowViewTest.java
@@ -22,11 +22,11 @@ import static com.android.tv.testing.uihelper.Constants.MENU;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertNotNull;
-import android.support.test.filters.SmallTest;
import android.support.test.uiautomator.BySelector;
import android.support.test.uiautomator.UiObject2;
import android.support.test.uiautomator.Until;
import android.view.KeyEvent;
+import androidx.test.filters.SmallTest;
import com.android.tv.R;
import com.android.tv.testing.testinput.TvTestInputConstants;
import com.android.tv.testing.uihelper.Constants;
diff --git a/tests/func/src/com/android/tv/tests/ui/ProgramGuideTest.java b/tests/func/src/com/android/tv/tests/ui/ProgramGuideTest.java
index 4adf448a..0a6a85d6 100644
--- a/tests/func/src/com/android/tv/tests/ui/ProgramGuideTest.java
+++ b/tests/func/src/com/android/tv/tests/ui/ProgramGuideTest.java
@@ -15,8 +15,8 @@
*/
package com.android.tv.tests.ui;
-import android.support.test.filters.MediumTest;
import android.support.test.uiautomator.Until;
+import androidx.test.filters.MediumTest;
import com.android.tv.guide.ProgramGuide;
import com.android.tv.testing.uihelper.Constants;
import org.junit.Rule;
diff --git a/tests/func/src/com/android/tv/tests/ui/TimeoutTest.java b/tests/func/src/com/android/tv/tests/ui/TimeoutTest.java
index 4b6befe4..73e869f1 100644
--- a/tests/func/src/com/android/tv/tests/ui/TimeoutTest.java
+++ b/tests/func/src/com/android/tv/tests/ui/TimeoutTest.java
@@ -15,8 +15,8 @@
*/
package com.android.tv.tests.ui;
-import android.support.test.filters.LargeTest;
import android.support.test.uiautomator.Until;
+import androidx.test.filters.LargeTest;
import com.android.tv.R;
import com.android.tv.testing.uihelper.Constants;
import org.junit.Ignore;
diff --git a/tests/func/src/com/android/tv/tests/ui/dvr/DvrLibraryTest.java b/tests/func/src/com/android/tv/tests/ui/dvr/DvrLibraryTest.java
index d0ebed91..8998b458 100644
--- a/tests/func/src/com/android/tv/tests/ui/dvr/DvrLibraryTest.java
+++ b/tests/func/src/com/android/tv/tests/ui/dvr/DvrLibraryTest.java
@@ -19,11 +19,11 @@ package com.android.tv.tests.ui.dvr;
import static com.android.tv.testing.uihelper.UiDeviceAsserts.assertWaitUntilFocused;
import android.os.Build;
-import android.support.test.filters.MediumTest;
-import android.support.test.filters.SdkSuppress;
import android.support.test.uiautomator.By;
import android.support.test.uiautomator.BySelector;
import android.support.test.uiautomator.Until;
+import androidx.test.filters.MediumTest;
+import androidx.test.filters.SdkSuppress;
import com.android.tv.R;
import com.android.tv.testing.uihelper.ByResource;
import com.android.tv.testing.uihelper.Constants;
diff --git a/tests/func/src/com/android/tv/tests/ui/sidepanel/CustomizeChannelListFragmentTest.java b/tests/func/src/com/android/tv/tests/ui/sidepanel/CustomizeChannelListFragmentTest.java
index 09b855e2..d035874a 100644
--- a/tests/func/src/com/android/tv/tests/ui/sidepanel/CustomizeChannelListFragmentTest.java
+++ b/tests/func/src/com/android/tv/tests/ui/sidepanel/CustomizeChannelListFragmentTest.java
@@ -21,11 +21,11 @@ import static junit.framework.Assert.assertNotNull;
import static junit.framework.Assert.assertTrue;
import android.graphics.Point;
-import android.support.test.filters.MediumTest;
import android.support.test.uiautomator.BySelector;
import android.support.test.uiautomator.Direction;
import android.support.test.uiautomator.UiObject2;
import android.support.test.uiautomator.Until;
+import androidx.test.filters.MediumTest;
import com.android.tv.R;
import com.android.tv.testing.uihelper.Constants;
import com.android.tv.tests.ui.LiveChannelsTestController;
diff --git a/tests/input/AndroidManifest.xml b/tests/input/AndroidManifest.xml
index 9b5df2ff..fa52946e 100644
--- a/tests/input/AndroidManifest.xml
+++ b/tests/input/AndroidManifest.xml
@@ -18,7 +18,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.tv.testinput">
- <uses-sdk android:targetSdkVersion="26" android:minSdkVersion="23"/>
+ <uses-sdk android:targetSdkVersion="27" android:minSdkVersion="23"/>
<!-- Required to update or read existing channel and program information in TvProvider. -->
<uses-permission android:name="com.android.providers.tv.permission.READ_EPG_DATA" />
diff --git a/tests/input/jank.sh b/tests/input/jank.sh
index c6311a4d..0710a4fc 100644
--- a/tests/input/jank.sh
+++ b/tests/input/jank.sh
@@ -17,8 +17,8 @@
# text fixture setup for unit tests
-echo "text fixture setup for func tests"
+echo "text fixture setup for jank tests"
am instrument \
-e testSetupMode jank \
- -w com.android.tv.testinput/.instrument.TestSetupInstrumentation \ No newline at end of file
+ -w com.android.tv.testinput/.instrument.TestSetupInstrumentation
diff --git a/tests/input/tools/get_test_logos.sh b/tests/input/tools/get_test_logos.sh
index 4dd87a3a..649c51ae 100755
--- a/tests/input/tools/get_test_logos.sh
+++ b/tests/input/tools/get_test_logos.sh
@@ -25,7 +25,7 @@ icons=(
bus cafe camping car-dealer car-rental
car-repair casino caution cemetery-grave cemetery-tomb
cinema civic-building computer corporate courthouse
- fire flag floral helicopter home
+ fire flag helicopter home
info landslide legal location locomotive
medical mobile motorcycle music parking
pet petrol phone picnic postal
diff --git a/tests/jank/Android.mk b/tests/jank/Android.mk
index 1b67ac3f..7df77ea0 100644
--- a/tests/jank/Android.mk
+++ b/tests/jank/Android.mk
@@ -10,7 +10,7 @@ LOCAL_SRC_FILES := $(call all-java-files-under, src)
LOCAL_PACKAGE_NAME := TVJankTests
LOCAL_STATIC_JAVA_LIBRARIES := \
- android-support-test \
+ androidx.test.runner \
tv-test-common \
ub-janktesthelper \
ub-uiautomator \
diff --git a/tests/jank/AndroidManifest.xml b/tests/jank/AndroidManifest.xml
index 5ea72b44..7c0997ac 100644
--- a/tests/jank/AndroidManifest.xml
+++ b/tests/jank/AndroidManifest.xml
@@ -18,10 +18,10 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.tv.tests.jank" >
- <uses-sdk android:targetSdkVersion="26" android:minSdkVersion="21" />
+ <uses-sdk android:targetSdkVersion="27" android:minSdkVersion="23" />
<instrumentation
- android:name="android.support.test.runner.AndroidJUnitRunner"
+ android:name="androidx.test.runner.AndroidJUnitRunner"
android:label="Live Channel Jank Tests"
android:targetPackage="com.android.tv" />
diff --git a/tests/jank/README.md b/tests/jank/README.md
new file mode 100644
index 00000000..c40eb229
--- /dev/null
+++ b/tests/jank/README.md
@@ -0,0 +1,32 @@
+# Jank tests for Live Channels
+
+
+## AOSP instructions
+
+To run the jank tests
+
+```bash
+echo "Compiling"
+m -j LiveTv TVTestInput TVJankTests
+echo "Installing"
+adb install -r ${OUT}/system/priv-app/LiveTv/LiveTv.apk
+adb install -r ${OUT}/system/app/TVTestInput/TVTestInput.apk
+adb install -r ${OUT}/testcases/TVJankTests/TVJankTests.apk
+echo "Setting up test input"
+adb shell am instrument \
+ -e testSetupMode jank \
+ -w com.android.tv.testinput/.instrument.TestSetupInstrumentation
+echo "Running the test"
+adb shell am instrument \
+ -w com.android.tv.tests.jank/android.support.test.runner.AndroidJUnitRunner
+
+```
+
+If it is your first time installing LiveTv you will need to do
+
+```bash
+adb root
+adb remount
+adb push ${OUT}/system/priv-app/LiveTv/LiveTv.apk /system/priv-app/LiveTv/LiveTv.apk
+adb reboot
+``` \ No newline at end of file
diff --git a/tests/jank/src/com/android/tv/tests/jank/ChannelZappingJankTest.java b/tests/jank/src/com/android/tv/tests/jank/ChannelZappingJankTest.java
index eee2328b..02ca6730 100644
--- a/tests/jank/src/com/android/tv/tests/jank/ChannelZappingJankTest.java
+++ b/tests/jank/src/com/android/tv/tests/jank/ChannelZappingJankTest.java
@@ -15,9 +15,9 @@
*/
package com.android.tv.tests.jank;
-import android.support.test.filters.MediumTest;
import android.support.test.jank.GfxMonitor;
import android.support.test.jank.JankTest;
+import androidx.test.filters.MediumTest;
/** Jank tests for channel zapping. */
@MediumTest
diff --git a/tests/jank/src/com/android/tv/tests/jank/MenuJankTest.java b/tests/jank/src/com/android/tv/tests/jank/MenuJankTest.java
index ea80eb3d..6b0dcd0e 100644
--- a/tests/jank/src/com/android/tv/tests/jank/MenuJankTest.java
+++ b/tests/jank/src/com/android/tv/tests/jank/MenuJankTest.java
@@ -15,9 +15,9 @@
*/
package com.android.tv.tests.jank;
-import android.support.test.filters.MediumTest;
import android.support.test.jank.GfxMonitor;
import android.support.test.jank.JankTest;
+import androidx.test.filters.MediumTest;
import com.android.tv.testing.uihelper.MenuHelper;
/** Jank tests for the program guide. */
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 57d38ba9..da2eb9cb 100644
--- a/tests/jank/src/com/android/tv/tests/jank/ProgramGuideJankTest.java
+++ b/tests/jank/src/com/android/tv/tests/jank/ProgramGuideJankTest.java
@@ -17,10 +17,10 @@ package com.android.tv.tests.jank;
import static com.android.tv.testing.uihelper.UiDeviceAsserts.assertWaitForCondition;
-import android.support.test.filters.MediumTest;
import android.support.test.jank.GfxMonitor;
import android.support.test.jank.JankTest;
import android.support.test.uiautomator.Until;
+import androidx.test.filters.MediumTest;
import com.android.tv.R;
import com.android.tv.testing.uihelper.ByResource;
import com.android.tv.testing.uihelper.Constants;
diff --git a/tests/unit/Android.mk b/tests/unit/Android.mk
index a425bcfe..5ea7ccd8 100644
--- a/tests/unit/Android.mk
+++ b/tests/unit/Android.mk
@@ -8,7 +8,7 @@ LOCAL_MODULE_TAGS := tests
LOCAL_SRC_FILES := $(call all-java-files-under, src)
LOCAL_STATIC_JAVA_LIBRARIES := \
- android-support-test \
+ androidx.test.runner \
mockito-target \
tv-test-common \
diff --git a/tests/unit/AndroidManifest.xml b/tests/unit/AndroidManifest.xml
index 9134a1c1..c7d2f528 100644
--- a/tests/unit/AndroidManifest.xml
+++ b/tests/unit/AndroidManifest.xml
@@ -18,10 +18,10 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.tv.tests" >
- <uses-sdk android:targetSdkVersion="26" android:minSdkVersion="23" />
+ <uses-sdk android:targetSdkVersion="27" android:minSdkVersion="23" />
<instrumentation
- android:name="android.support.test.runner.AndroidJUnitRunner"
+ android:name="androidx.test.runner.AndroidJUnitRunner"
android:label="Live Channel Unit Tests"
android:targetPackage="com.android.tv" />
diff --git a/tests/unit/src/com/android/tv/CurrentPositionMediatorTest.java b/tests/unit/src/com/android/tv/CurrentPositionMediatorTest.java
index abadde31..4b85eaae 100644
--- a/tests/unit/src/com/android/tv/CurrentPositionMediatorTest.java
+++ b/tests/unit/src/com/android/tv/CurrentPositionMediatorTest.java
@@ -20,9 +20,9 @@ import static com.android.tv.TimeShiftManager.INVALID_TIME;
import static com.android.tv.TimeShiftManager.REQUEST_TIMEOUT_MS;
import static com.google.common.truth.Truth.assertWithMessage;
-import android.support.test.annotation.UiThreadTest;
-import android.support.test.filters.MediumTest;
-import android.support.test.runner.AndroidJUnit4;
+import androidx.test.annotation.UiThreadTest;
+import androidx.test.filters.MediumTest;
+import androidx.test.runner.AndroidJUnit4;
import com.android.tv.testing.activities.BaseMainActivityTestCase;
import org.junit.Before;
import org.junit.Test;
diff --git a/tests/unit/src/com/android/tv/MainActivityTest.java b/tests/unit/src/com/android/tv/MainActivityTest.java
index c5df21a9..f6223ec6 100644
--- a/tests/unit/src/com/android/tv/MainActivityTest.java
+++ b/tests/unit/src/com/android/tv/MainActivityTest.java
@@ -15,14 +15,14 @@
*/
package com.android.tv;
-import static android.support.test.InstrumentationRegistry.getInstrumentation;
+import static androidx.test.InstrumentationRegistry.getInstrumentation;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
-import android.support.test.filters.MediumTest;
-import android.support.test.runner.AndroidJUnit4;
import android.view.View;
import android.widget.TextView;
+import androidx.test.filters.MediumTest;
+import androidx.test.runner.AndroidJUnit4;
import com.android.tv.data.api.Channel;
import com.android.tv.testing.activities.BaseMainActivityTestCase;
import com.android.tv.testing.testinput.TvTestInputConstants;
diff --git a/tests/unit/src/com/android/tv/TimeShiftManagerTest.java b/tests/unit/src/com/android/tv/TimeShiftManagerTest.java
index cb523045..7adee385 100644
--- a/tests/unit/src/com/android/tv/TimeShiftManagerTest.java
+++ b/tests/unit/src/com/android/tv/TimeShiftManagerTest.java
@@ -24,8 +24,8 @@ import static com.android.tv.TimeShiftManager.TIME_SHIFT_ACTION_ID_PLAY;
import static com.android.tv.TimeShiftManager.TIME_SHIFT_ACTION_ID_REWIND;
import static com.google.common.truth.Truth.assertWithMessage;
-import android.support.test.filters.MediumTest;
-import android.support.test.runner.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+import androidx.test.runner.AndroidJUnit4;
import com.android.tv.testing.activities.BaseMainActivityTestCase;
import org.junit.Before;
import org.junit.Test;
diff --git a/tests/unit/src/com/android/tv/data/ChannelDataManagerTest.java b/tests/unit/src/com/android/tv/data/ChannelDataManagerTest.java
index 96c1f7a1..71ccaf35 100644
--- a/tests/unit/src/com/android/tv/data/ChannelDataManagerTest.java
+++ b/tests/unit/src/com/android/tv/data/ChannelDataManagerTest.java
@@ -16,8 +16,8 @@
package com.android.tv.data;
-import static android.support.test.InstrumentationRegistry.getInstrumentation;
-import static android.support.test.InstrumentationRegistry.getTargetContext;
+import static androidx.test.InstrumentationRegistry.getInstrumentation;
+import static androidx.test.InstrumentationRegistry.getTargetContext;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
@@ -25,14 +25,14 @@ import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
+import android.content.res.AssetFileDescriptor;
import android.database.ContentObserver;
import android.database.Cursor;
import android.media.tv.TvContract;
import android.media.tv.TvContract.Channels;
import android.net.Uri;
import android.os.AsyncTask;
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
+import android.os.Bundle;
import android.test.MoreAsserts;
import android.test.mock.MockContentProvider;
import android.test.mock.MockContentResolver;
@@ -40,10 +40,13 @@ import android.test.mock.MockCursor;
import android.text.TextUtils;
import android.util.Log;
import android.util.SparseArray;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
import com.android.tv.data.api.Channel;
import com.android.tv.testing.constants.Constants;
import com.android.tv.testing.data.ChannelInfo;
import com.android.tv.util.TvInputManagerHelper;
+import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -98,9 +101,20 @@ public class ChannelDataManagerTest {
Mockito.mock(TvInputManagerHelper.class);
Mockito.when(mockHelper.hasTvInputInfo(Matchers.anyString()))
.thenReturn(true);
+ Context mockContext = Mockito.mock(Context.class);
+ Mockito.when(mockContext.getContentResolver())
+ .thenReturn(mContentResolver);
+ Mockito.when(mockContext.checkSelfPermission(Matchers.anyString()))
+ .thenAnswer(
+ invocation -> {
+ Object[] args = invocation.getArguments();
+ return getTargetContext()
+ .checkSelfPermission(((String) args[0]));
+ });
+
mChannelDataManager =
new ChannelDataManager(
- getTargetContext(),
+ mockContext,
mockHelper,
AsyncTask.SERIAL_EXECUTOR,
mContentResolver);
@@ -417,6 +431,15 @@ public class ChannelDataManagerTest {
}
}
+ @Override
+ public AssetFileDescriptor openTypedAssetFile(Uri url, String mimeType, Bundle opts) {
+ try {
+ return getTargetContext().getContentResolver().openAssetFileDescriptor(url, "r");
+ } catch (FileNotFoundException e) {
+ return null;
+ }
+ }
+
/**
* Implementation of {@link ContentProvider#query}. This assumes that {@link
* ChannelDataManager} queries channels with empty {@code selection}. (i.e. channels are
diff --git a/tests/unit/src/com/android/tv/data/ChannelImplTest.java b/tests/unit/src/com/android/tv/data/ChannelImplTest.java
index b791a7e4..86cfab66 100644
--- a/tests/unit/src/com/android/tv/data/ChannelImplTest.java
+++ b/tests/unit/src/com/android/tv/data/ChannelImplTest.java
@@ -25,8 +25,8 @@ import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
import com.android.tv.data.api.Channel;
import com.android.tv.testing.ComparatorTester;
import com.android.tv.util.TvInputManagerHelper;
diff --git a/tests/unit/src/com/android/tv/data/TvInputNewComparatorTest.java b/tests/unit/src/com/android/tv/data/TvInputNewComparatorTest.java
deleted file mode 100644
index 8e892cce..00000000
--- a/tests/unit/src/com/android/tv/data/TvInputNewComparatorTest.java
+++ /dev/null
@@ -1,98 +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.data;
-
-import android.content.pm.ResolveInfo;
-import android.media.tv.TvInputInfo;
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
-import android.util.Pair;
-import com.android.tv.testing.ComparatorTester;
-import com.android.tv.testing.utils.TestUtils;
-import com.android.tv.util.SetupUtils;
-import com.android.tv.util.TvInputManagerHelper;
-import java.util.Comparator;
-import java.util.LinkedHashMap;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Matchers;
-import org.mockito.Mockito;
-import org.mockito.invocation.InvocationOnMock;
-import org.mockito.stubbing.Answer;
-
-/** Test for {@link TvInputNewComparator} */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class TvInputNewComparatorTest {
- @Test
- public void testComparator() throws Exception {
- LinkedHashMap<String, Pair<Boolean, Boolean>> inputIdToNewInput = new LinkedHashMap<>();
- inputIdToNewInput.put("2_new_input", new Pair<>(true, false));
- inputIdToNewInput.put("4_new_input", new Pair<>(true, false));
- inputIdToNewInput.put("4_old_input", new Pair<>(false, false));
- inputIdToNewInput.put("0_old_input", new Pair<>(false, true));
- inputIdToNewInput.put("1_old_input", new Pair<>(false, true));
- inputIdToNewInput.put("3_old_input", new Pair<>(false, true));
-
- SetupUtils setupUtils = Mockito.mock(SetupUtils.class);
- Mockito.when(setupUtils.isNewInput(Matchers.anyString()))
- .thenAnswer(
- new Answer<Boolean>() {
- @Override
- public Boolean answer(InvocationOnMock invocation) throws Throwable {
- String inputId = (String) invocation.getArguments()[0];
- return inputIdToNewInput.get(inputId).first;
- }
- });
- Mockito.when(setupUtils.isSetupDone(Matchers.anyString()))
- .thenAnswer(
- new Answer<Boolean>() {
- @Override
- public Boolean answer(InvocationOnMock invocation) throws Throwable {
- String inputId = (String) invocation.getArguments()[0];
- return inputIdToNewInput.get(inputId).second;
- }
- });
- TvInputManagerHelper inputManager = Mockito.mock(TvInputManagerHelper.class);
- Mockito.when(inputManager.getDefaultTvInputInfoComparator())
- .thenReturn(
- new Comparator<TvInputInfo>() {
- @Override
- public int compare(TvInputInfo lhs, TvInputInfo rhs) {
- return lhs.getId().compareTo(rhs.getId());
- }
- });
- TvInputNewComparator comparator = new TvInputNewComparator(setupUtils, inputManager);
- ComparatorTester<TvInputInfo> comparatorTester =
- ComparatorTester.withoutEqualsTest(comparator);
- ResolveInfo resolveInfo = TestUtils.createResolveInfo("test", "test");
- for (String id : inputIdToNewInput.keySet()) {
- // Put mock resolveInfo to prevent NPE in {@link TvInputInfo#toString}
- TvInputInfo info1 =
- TestUtils.createTvInputInfo(
- resolveInfo, id, "test1", TvInputInfo.TYPE_TUNER, false);
- TvInputInfo info2 =
- TestUtils.createTvInputInfo(
- resolveInfo, id, "test2", TvInputInfo.TYPE_DISPLAY_PORT, true);
- TvInputInfo info3 =
- TestUtils.createTvInputInfo(
- resolveInfo, id, "test", TvInputInfo.TYPE_HDMI, true);
- comparatorTester.addComparableGroup(info1, info2, info3);
- }
- comparatorTester.test();
- }
-}
diff --git a/tests/unit/src/com/android/tv/data/WatchedHistoryManagerTest.java b/tests/unit/src/com/android/tv/data/WatchedHistoryManagerTest.java
deleted file mode 100644
index 43bfde09..00000000
--- a/tests/unit/src/com/android/tv/data/WatchedHistoryManagerTest.java
+++ /dev/null
@@ -1,140 +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.data;
-
-import static android.support.test.InstrumentationRegistry.getTargetContext;
-import static com.google.common.truth.Truth.assertThat;
-
-import android.os.Looper;
-import android.support.test.filters.MediumTest;
-import android.support.test.runner.AndroidJUnit4;
-import com.android.tv.data.WatchedHistoryManager.WatchedRecord;
-import java.util.concurrent.TimeUnit;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-/**
- * Test for {@link com.android.tv.data.WatchedHistoryManagerTest}
- *
- * <p>This is a medium test because it load files which accessing SharedPreferences.
- */
-@MediumTest
-@RunWith(AndroidJUnit4.class)
-public class WatchedHistoryManagerTest {
- // Wait time for expected success.
- private static final int MAX_HISTORY_SIZE = 100;
-
- private WatchedHistoryManager mWatchedHistoryManager;
- private TestWatchedHistoryManagerListener mListener;
-
- @Before
- public void setUp() {
- if (Looper.myLooper() == null) {
- Looper.prepare();
- }
- mWatchedHistoryManager = new WatchedHistoryManager(getTargetContext(), MAX_HISTORY_SIZE);
- mListener = new TestWatchedHistoryManagerListener();
- mWatchedHistoryManager.setListener(mListener);
- }
-
- private void startAndWaitForComplete() throws InterruptedException {
- mWatchedHistoryManager.start();
- assertThat(mListener.mLoadFinished).isTrue();
- }
-
- @Test
- public void testIsLoaded() throws InterruptedException {
- startAndWaitForComplete();
- assertThat(mWatchedHistoryManager.isLoaded()).isTrue();
- }
-
- @Test
- public void testLogChannelViewStop() throws InterruptedException {
- startAndWaitForComplete();
- long fakeId = 100000000;
- long time = System.currentTimeMillis();
- long duration = TimeUnit.MINUTES.toMillis(10);
- ChannelImpl channel = new ChannelImpl.Builder().setId(fakeId).build();
- mWatchedHistoryManager.logChannelViewStop(channel, time, duration);
-
- WatchedRecord record = mWatchedHistoryManager.getRecord(0);
- WatchedRecord recordFromSharedPreferences =
- mWatchedHistoryManager.getRecordFromSharedPreferences(0);
- assertThat(fakeId).isEqualTo(record.channelId);
- assertThat(time - duration).isEqualTo(record.watchedStartTime);
- assertThat(duration).isEqualTo(record.duration);
- assertThat(recordFromSharedPreferences).isEqualTo(record);
- }
-
- @Test
- public void testCircularHistoryQueue() throws InterruptedException {
- startAndWaitForComplete();
- final long startChannelId = 100000000;
- long time = System.currentTimeMillis();
- long duration = TimeUnit.MINUTES.toMillis(10);
-
- int size = MAX_HISTORY_SIZE * 2;
- for (int i = 0; i < size; ++i) {
- ChannelImpl channel = new ChannelImpl.Builder().setId(startChannelId + i).build();
- mWatchedHistoryManager.logChannelViewStop(channel, time + duration * i, duration);
- }
- for (int i = 0; i < MAX_HISTORY_SIZE; ++i) {
- WatchedRecord record = mWatchedHistoryManager.getRecord(i);
- WatchedRecord recordFromSharedPreferences =
- mWatchedHistoryManager.getRecordFromSharedPreferences(i);
- assertThat(recordFromSharedPreferences).isEqualTo(record);
- assertThat(startChannelId + size - 1 - i).isEqualTo(record.channelId);
- }
- // Since the WatchedHistory is a circular queue, the value for 0 and maxHistorySize
- // are same.
- assertThat(mWatchedHistoryManager.getRecordFromSharedPreferences(MAX_HISTORY_SIZE))
- .isEqualTo(mWatchedHistoryManager.getRecordFromSharedPreferences(0));
- }
-
- @Test
- public void testWatchedRecordEquals() {
- assertThat(new WatchedRecord(1, 2, 3).equals(new WatchedRecord(1, 2, 3))).isTrue();
- assertThat(new WatchedRecord(1, 2, 3).equals(new WatchedRecord(1, 2, 4))).isFalse();
- assertThat(new WatchedRecord(1, 2, 3).equals(new WatchedRecord(1, 4, 3))).isFalse();
- assertThat(new WatchedRecord(1, 2, 3).equals(new WatchedRecord(4, 2, 3))).isFalse();
- }
-
- @Test
- public void testEncodeDecodeWatchedRecord() {
- long fakeId = 100000000;
- long time = System.currentTimeMillis();
- long duration = TimeUnit.MINUTES.toMillis(10);
- WatchedRecord record = new WatchedRecord(fakeId, time, duration);
- WatchedRecord sameRecord =
- mWatchedHistoryManager.decode(mWatchedHistoryManager.encode(record));
- assertThat(sameRecord).isEqualTo(record);
- }
-
- private static final class TestWatchedHistoryManagerListener
- implements WatchedHistoryManager.Listener {
- boolean mLoadFinished;
-
- @Override
- public void onLoadFinished() {
- mLoadFinished = true;
- }
-
- @Override
- public void onNewRecordAdded(WatchedRecord watchedRecord) {}
- }
-}
diff --git a/tests/unit/src/com/android/tv/dvr/recorder/DvrRecordingServiceTest.java b/tests/unit/src/com/android/tv/dvr/recorder/DvrRecordingServiceTest.java
index d510da32..546f074f 100644
--- a/tests/unit/src/com/android/tv/dvr/recorder/DvrRecordingServiceTest.java
+++ b/tests/unit/src/com/android/tv/dvr/recorder/DvrRecordingServiceTest.java
@@ -20,9 +20,9 @@ import static com.google.common.truth.Truth.assertThat;
import android.content.Intent;
import android.os.Build;
-import android.support.test.filters.SdkSuppress;
-import android.support.test.filters.SmallTest;
import android.test.ServiceTestCase;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
import com.android.tv.common.feature.CommonFeatures;
import com.android.tv.common.feature.TestableFeature;
import org.mockito.MockitoAnnotations;
diff --git a/tests/unit/src/com/android/tv/FeaturesTest.java b/tests/unit/src/com/android/tv/features/FeaturesTest.java
index e19f4b7c..e35758c3 100644
--- a/tests/unit/src/com/android/tv/FeaturesTest.java
+++ b/tests/unit/src/com/android/tv/features/FeaturesTest.java
@@ -14,12 +14,12 @@
* limitations under the License
*/
-package com.android.tv;
+package com.android.tv.features;
import static com.google.common.truth.Truth.assertThat;
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
diff --git a/tests/unit/src/com/android/tv/menu/MenuTest.java b/tests/unit/src/com/android/tv/menu/MenuTest.java
index 028a185d..e384c398 100644
--- a/tests/unit/src/com/android/tv/menu/MenuTest.java
+++ b/tests/unit/src/com/android/tv/menu/MenuTest.java
@@ -15,11 +15,11 @@
*/
package com.android.tv.menu;
-import static android.support.test.InstrumentationRegistry.getTargetContext;
+import static androidx.test.InstrumentationRegistry.getTargetContext;
import static com.google.common.truth.Truth.assertWithMessage;
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
import com.android.tv.menu.Menu.OnMenuVisibilityChangeListener;
import org.junit.Before;
import org.junit.Ignore;
diff --git a/tests/unit/src/com/android/tv/menu/TvOptionsRowAdapterTest.java b/tests/unit/src/com/android/tv/menu/TvOptionsRowAdapterTest.java
index 0f815a7a..5ecbdf02 100644
--- a/tests/unit/src/com/android/tv/menu/TvOptionsRowAdapterTest.java
+++ b/tests/unit/src/com/android/tv/menu/TvOptionsRowAdapterTest.java
@@ -15,15 +15,15 @@
*/
package com.android.tv.menu;
-import static android.support.test.InstrumentationRegistry.getInstrumentation;
+import static androidx.test.InstrumentationRegistry.getInstrumentation;
import static com.google.common.truth.Truth.assertWithMessage;
import static org.junit.Assert.fail;
import android.media.tv.TvTrackInfo;
import android.os.SystemClock;
-import android.support.test.filters.MediumTest;
-import android.support.test.runner.AndroidJUnit4;
import android.text.TextUtils;
+import androidx.test.filters.MediumTest;
+import androidx.test.runner.AndroidJUnit4;
import com.android.tv.testing.activities.BaseMainActivityTestCase;
import com.android.tv.testing.constants.Constants;
import com.android.tv.testing.testinput.ChannelState;
diff --git a/tests/unit/src/com/android/tv/recommendation/ChannelRecordTest.java b/tests/unit/src/com/android/tv/recommendation/ChannelRecordTest.java
index e63bdc3a..c217222f 100644
--- a/tests/unit/src/com/android/tv/recommendation/ChannelRecordTest.java
+++ b/tests/unit/src/com/android/tv/recommendation/ChannelRecordTest.java
@@ -16,11 +16,11 @@
package com.android.tv.recommendation;
-import static android.support.test.InstrumentationRegistry.getContext;
+import static androidx.test.InstrumentationRegistry.getContext;
import static com.google.common.truth.Truth.assertThat;
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
import com.android.tv.testing.utils.Utils;
import java.util.Random;
import java.util.concurrent.TimeUnit;
diff --git a/tests/unit/src/com/android/tv/recommendation/EvaluatorTestCase.java b/tests/unit/src/com/android/tv/recommendation/EvaluatorTestCase.java
index f62a5e05..9696d8b6 100644
--- a/tests/unit/src/com/android/tv/recommendation/EvaluatorTestCase.java
+++ b/tests/unit/src/com/android/tv/recommendation/EvaluatorTestCase.java
@@ -16,7 +16,7 @@
package com.android.tv.recommendation;
-import static android.support.test.InstrumentationRegistry.getContext;
+import static androidx.test.InstrumentationRegistry.getContext;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
diff --git a/tests/unit/src/com/android/tv/recommendation/FavoriteChannelEvaluatorTest.java b/tests/unit/src/com/android/tv/recommendation/FavoriteChannelEvaluatorTest.java
index e14320f0..773d3358 100644
--- a/tests/unit/src/com/android/tv/recommendation/FavoriteChannelEvaluatorTest.java
+++ b/tests/unit/src/com/android/tv/recommendation/FavoriteChannelEvaluatorTest.java
@@ -18,8 +18,8 @@ package com.android.tv.recommendation;
import static com.google.common.truth.Truth.assertThat;
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.junit.Test;
diff --git a/tests/unit/src/com/android/tv/recommendation/RecentChannelEvaluatorTest.java b/tests/unit/src/com/android/tv/recommendation/RecentChannelEvaluatorTest.java
index f8d6b220..15a3726a 100644
--- a/tests/unit/src/com/android/tv/recommendation/RecentChannelEvaluatorTest.java
+++ b/tests/unit/src/com/android/tv/recommendation/RecentChannelEvaluatorTest.java
@@ -18,8 +18,8 @@ package com.android.tv.recommendation;
import static com.google.common.truth.Truth.assertThat;
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
diff --git a/tests/unit/src/com/android/tv/recommendation/RecommenderTest.java b/tests/unit/src/com/android/tv/recommendation/RecommenderTest.java
index 812a3eb1..01208d27 100644
--- a/tests/unit/src/com/android/tv/recommendation/RecommenderTest.java
+++ b/tests/unit/src/com/android/tv/recommendation/RecommenderTest.java
@@ -16,12 +16,12 @@
package com.android.tv.recommendation;
-import static android.support.test.InstrumentationRegistry.getContext;
+import static androidx.test.InstrumentationRegistry.getContext;
import static com.google.common.truth.Truth.assertThat;
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
import android.test.MoreAsserts;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
import com.android.tv.data.api.Channel;
import com.android.tv.recommendation.RecommendationUtils.ChannelRecordSortedMapHelper;
import com.android.tv.testing.utils.Utils;
diff --git a/tests/unit/src/com/android/tv/recommendation/RoutineWatchEvaluatorTest.java b/tests/unit/src/com/android/tv/recommendation/RoutineWatchEvaluatorTest.java
index 39e6e9c5..d9149050 100644
--- a/tests/unit/src/com/android/tv/recommendation/RoutineWatchEvaluatorTest.java
+++ b/tests/unit/src/com/android/tv/recommendation/RoutineWatchEvaluatorTest.java
@@ -19,8 +19,8 @@ package com.android.tv.recommendation;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
import com.android.tv.data.Program;
import com.android.tv.recommendation.RoutineWatchEvaluator.ProgramTime;
import java.util.Calendar;
diff --git a/tests/unit/src/com/android/tv/util/MockTvSingletons.java b/tests/unit/src/com/android/tv/util/MockTvSingletons.java
index 6de1eb3e..fd4b43cf 100644
--- a/tests/unit/src/com/android/tv/util/MockTvSingletons.java
+++ b/tests/unit/src/com/android/tv/util/MockTvSingletons.java
@@ -17,16 +17,19 @@
package com.android.tv.util;
import android.content.Context;
-import android.content.Intent;
import com.android.tv.InputSessionManager;
import com.android.tv.MainActivityWrapper;
import com.android.tv.TvApplication;
import com.android.tv.TvSingletons;
import com.android.tv.analytics.Analytics;
import com.android.tv.analytics.Tracker;
-import com.android.tv.common.config.api.RemoteConfig;
import com.android.tv.common.experiments.ExperimentLoader;
+import com.android.tv.common.flags.impl.DefaultBackendKnobsFlags;
+import com.android.tv.common.flags.impl.DefaultCloudEpgFlags;
+import com.android.tv.common.flags.impl.DefaultConcurrentDvrPlaybackFlags;
+import com.android.tv.common.flags.impl.DefaultUiFlags;
import com.android.tv.common.recording.RecordingStorageStatusManager;
+import com.android.tv.common.singletons.HasSingletons;
import com.android.tv.common.util.Clock;
import com.android.tv.data.ChannelDataManager;
import com.android.tv.data.PreviewDataManager;
@@ -40,15 +43,21 @@ import com.android.tv.dvr.DvrWatchedPositionManager;
import com.android.tv.dvr.recorder.RecordingScheduler;
import com.android.tv.perf.PerformanceMonitor;
import com.android.tv.testing.FakeClock;
-import com.android.tv.tuner.TunerInputController;
+import com.android.tv.tunerinputcontroller.BuiltInTunerManager;
+import com.google.common.base.Optional;
import java.util.concurrent.Executor;
import javax.inject.Provider;
/** Mock {@link TvSingletons} class. */
-public class MockTvSingletons implements TvSingletons {
+public class MockTvSingletons implements TvSingletons, HasSingletons<TvSingletons> {
public final FakeClock fakeClock = FakeClock.createWithCurrentTime();
private final TvApplication mApp;
+ private final DefaultBackendKnobsFlags mBackendFlags = new DefaultBackendKnobsFlags();
+ private final DefaultCloudEpgFlags mCloudEpgFlags = new DefaultCloudEpgFlags();
+ private final DefaultUiFlags mUiFlags = new DefaultUiFlags();
+ private final DefaultConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags =
+ new DefaultConcurrentDvrPlaybackFlags();
private PerformanceMonitor mPerformanceMonitor;
public MockTvSingletons(Context context) {
@@ -154,8 +163,8 @@ public class MockTvSingletons implements TvSingletons {
}
@Override
- public TunerInputController getTunerInputController() {
- return mApp.getTunerInputController();
+ public Optional<BuiltInTunerManager> getBuiltInTunerManager() {
+ return mApp.getBuiltInTunerManager();
}
@Override
@@ -174,16 +183,6 @@ public class MockTvSingletons implements TvSingletons {
}
@Override
- public RemoteConfig getRemoteConfig() {
- return mApp.getRemoteConfig();
- }
-
- @Override
- public Intent getTunerSetupIntent(Context context) {
- return mApp.getTunerSetupIntent(context);
- }
-
- @Override
public boolean isRunningInMainProcess() {
return mApp.isRunningInMainProcess();
}
@@ -198,12 +197,37 @@ public class MockTvSingletons implements TvSingletons {
}
@Override
- public String getEmbeddedTunerInputId() {
- return "com.android.tv/.tuner.tvinput.LiveTvTunerTvInputService";
+ public DefaultCloudEpgFlags getCloudEpgFlags() {
+ return mCloudEpgFlags;
+ }
+
+ @Override
+ public DefaultUiFlags getUiFlags() {
+ return mUiFlags;
}
@Override
public Executor getDbExecutor() {
return mApp.getDbExecutor();
}
+
+ @Override
+ public DefaultBackendKnobsFlags getBackendKnobs() {
+ return mBackendFlags;
+ }
+
+ @Override
+ public BuildType getBuildType() {
+ return BuildType.ENG;
+ }
+
+ @Override
+ public DefaultConcurrentDvrPlaybackFlags getConcurrentDvrPlaybackFlags() {
+ return mConcurrentDvrPlaybackFlags;
+ }
+
+ @Override
+ public TvSingletons singletons() {
+ return this;
+ }
}
diff --git a/tests/unit/src/com/android/tv/util/TvInputManagerHelperTest.java b/tests/unit/src/com/android/tv/util/TvInputManagerHelperTest.java
index 6dfed64a..7e35d76b 100644
--- a/tests/unit/src/com/android/tv/util/TvInputManagerHelperTest.java
+++ b/tests/unit/src/com/android/tv/util/TvInputManagerHelperTest.java
@@ -16,12 +16,12 @@
package com.android.tv.util;
-import static android.support.test.InstrumentationRegistry.getContext;
+import static androidx.test.InstrumentationRegistry.getContext;
import android.content.pm.ResolveInfo;
import android.media.tv.TvInputInfo;
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
import com.android.tv.testing.ComparatorTester;
import com.android.tv.testing.utils.TestUtils;
import java.util.ArrayList;
diff --git a/tests/unit/src/com/android/tv/util/TvTrackInfoUtilsTest.java b/tests/unit/src/com/android/tv/util/TvTrackInfoUtilsTest.java
deleted file mode 100644
index d84a90d4..00000000
--- a/tests/unit/src/com/android/tv/util/TvTrackInfoUtilsTest.java
+++ /dev/null
@@ -1,127 +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 static com.android.tv.util.TvTrackInfoUtils.getBestTrackInfo;
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import android.media.tv.TvTrackInfo;
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
-import com.android.tv.testing.ComparatorTester;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.List;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-/** Tests for {@link com.android.tv.util.TvTrackInfoUtils}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class TvTrackInfoUtilsTest {
- private static final String UN_MATCHED_ID = "no matching ID";
-
- private static final TvTrackInfo INFO_1_EN_1 = create("1", "en", 1);
-
- private static final TvTrackInfo INFO_2_EN_5 = create("2", "en", 5);
-
- private static final TvTrackInfo INFO_3_FR_8 = create("3", "fr", 8);
-
- private static final TvTrackInfo INFO_4_NULL_2 = create("4", null, 2);
-
- private static final TvTrackInfo INFO_5_NULL_6 = create("5", null, 6);
-
- private static TvTrackInfo create(String id, String fr, int audioChannelCount) {
- return new TvTrackInfo.Builder(TvTrackInfo.TYPE_AUDIO, id)
- .setLanguage(fr)
- .setAudioChannelCount(audioChannelCount)
- .build();
- }
-
- private static final List<TvTrackInfo> ALL =
- Arrays.asList(INFO_1_EN_1, INFO_2_EN_5, INFO_3_FR_8, INFO_4_NULL_2, INFO_5_NULL_6);
- private static final List<TvTrackInfo> NULL_LANGUAGE_TRACKS =
- Arrays.asList(INFO_4_NULL_2, INFO_5_NULL_6);
-
- @Test
- public void testGetBestTrackInfo_empty() {
- TvTrackInfo result = getBestTrackInfo(Collections.emptyList(), UN_MATCHED_ID, "en", 1);
- assertWithMessage("best track ").that(result).isEqualTo(null);
- }
-
- @Test
- public void testGetBestTrackInfo_exactMatch() {
- TvTrackInfo result = getBestTrackInfo(ALL, "1", "en", 1);
- assertWithMessage("best track ").that(result).isEqualTo(INFO_1_EN_1);
- }
-
- @Test
- public void testGetBestTrackInfo_langAndChannelCountMatch() {
- TvTrackInfo result = getBestTrackInfo(ALL, UN_MATCHED_ID, "en", 5);
- assertWithMessage("best track ").that(result).isEqualTo(INFO_2_EN_5);
- }
-
- @Test
- public void testGetBestTrackInfo_languageOnlyMatch() {
- TvTrackInfo result = getBestTrackInfo(ALL, UN_MATCHED_ID, "fr", 1);
- assertWithMessage("best track ").that(result).isEqualTo(INFO_3_FR_8);
- }
-
- @Test
- public void testGetBestTrackInfo_channelCountOnlyMatchWithNullLanguage() {
- TvTrackInfo result = getBestTrackInfo(ALL, UN_MATCHED_ID, null, 8);
- assertWithMessage("best track ").that(result).isEqualTo(INFO_3_FR_8);
- }
-
- @Test
- public void testGetBestTrackInfo_noMatches() {
- TvTrackInfo result = getBestTrackInfo(ALL, UN_MATCHED_ID, "kr", 1);
- assertWithMessage("best track ").that(result).isEqualTo(INFO_1_EN_1);
- }
-
- @Test
- public void testGetBestTrackInfo_noMatchesWithNullLanguage() {
- TvTrackInfo result = getBestTrackInfo(ALL, UN_MATCHED_ID, null, 0);
- assertWithMessage("best track ").that(result).isEqualTo(INFO_1_EN_1);
- }
-
- @Test
- public void testGetBestTrackInfo_channelCountAndIdMatch() {
- TvTrackInfo result = getBestTrackInfo(NULL_LANGUAGE_TRACKS, "5", null, 6);
- assertWithMessage("best track ").that(result).isEqualTo(INFO_5_NULL_6);
- }
-
- @Test
- public void testComparator() {
- Comparator<TvTrackInfo> comparator = TvTrackInfoUtils.createComparator("1", "en", 1);
- ComparatorTester.withoutEqualsTest(comparator)
- // lang not match
- .addComparableGroup(
- create("1", "kr", 1),
- create("2", "kr", 2),
- create("1", "ja", 1),
- create("1", "ch", 1))
- // lang match not count match
- .addComparableGroup(
- create("2", "en", 2), create("3", "en", 3), create("1", "en", 2))
- // lang and count match
- .addComparableGroup(create("2", "en", 1), create("3", "en", 1))
- // all match
- .addComparableGroup(create("1", "en", 1), create("1", "en", 1))
- .test();
- }
-}
diff --git a/tests/unit/src/com/android/tv/util/images/ImageCacheTest.java b/tests/unit/src/com/android/tv/util/images/ImageCacheTest.java
index b7715c4a..41722135 100644
--- a/tests/unit/src/com/android/tv/util/images/ImageCacheTest.java
+++ b/tests/unit/src/com/android/tv/util/images/ImageCacheTest.java
@@ -20,8 +20,8 @@ import static com.android.tv.util.images.BitmapUtils.createScaledBitmapInfo;
import static com.google.common.truth.Truth.assertWithMessage;
import android.graphics.Bitmap;
-import android.support.test.filters.MediumTest;
-import android.support.test.runner.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+import androidx.test.runner.AndroidJUnit4;
import com.android.tv.util.images.BitmapUtils.ScaledBitmapInfo;
import org.junit.Before;
import org.junit.Test;
diff --git a/tests/unit/src/com/android/tv/util/images/ScaledBitmapInfoTest.java b/tests/unit/src/com/android/tv/util/images/ScaledBitmapInfoTest.java
index 005775b6..1bb650fb 100644
--- a/tests/unit/src/com/android/tv/util/images/ScaledBitmapInfoTest.java
+++ b/tests/unit/src/com/android/tv/util/images/ScaledBitmapInfoTest.java
@@ -18,8 +18,8 @@ package com.android.tv.util.images;
import static com.google.common.truth.Truth.assertWithMessage;
import android.graphics.Bitmap;
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
import com.android.tv.util.images.BitmapUtils.ScaledBitmapInfo;
import org.junit.Test;
import org.junit.runner.RunWith;
diff --git a/tuner/Android.bp b/tuner/Android.bp
new file mode 100644
index 00000000..215a1e53
--- /dev/null
+++ b/tuner/Android.bp
@@ -0,0 +1,48 @@
+//
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+android_library {
+ name: "live-tv-tuner",
+ srcs: ["src/**/*.java"],
+ sdk_version: "system_current",
+ resource_dirs: ["res"],
+ libs: [
+ "tv-auto-value-jar",
+ "tv-auto-factory-jar",
+ "android-support-annotations",
+ "tv-error-prone-annotations-jar",
+ "tv-guava-android-jar",
+ "tv-javax-annotations-jar",
+ "jsr330",
+ "tv-lib-dagger",
+ "tv-lib-exoplayer",
+ "tv-lib-exoplayer-v2-core",
+ "live-tv-tuner-proto",
+ "android-support-compat",
+ "android-support-core-ui",
+ "android-support-v7-palette",
+ "android-support-v7-recyclerview",
+ "android-support-v17-leanback",
+ "androidx.tvprovider_tvprovider",
+ "tv-lib-dagger-android",
+ "tv-common",
+ ],
+ plugins: [
+ "tv-auto-value",
+ "tv-auto-factory",
+ ],
+ min_sdk_version: "23",
+}
diff --git a/tuner/Android.mk b/tuner/Android.mk
deleted file mode 100644
index aedda3c1..00000000
--- a/tuner/Android.mk
+++ /dev/null
@@ -1,44 +0,0 @@
-LOCAL_PATH:= $(call my-dir)
-include $(CLEAR_VARS)
-
-# Include all java and proto files.
-LOCAL_SRC_FILES := \
- $(call all-java-files-under, src) \
- $(call all-proto-files-under, proto)
-
-
-LOCAL_MODULE := live-tv-tuner
-LOCAL_MODULE_CLASS := STATIC_JAVA_LIBRARIES
-LOCAL_MODULE_TAGS := optional
-LOCAL_SDK_VERSION := system_current
-
-LOCAL_USE_AAPT2 := true
-
-LOCAL_PROTOC_OPTIMIZE_TYPE := nano
-LOCAL_PROTOC_FLAGS := --proto_path=$(LOCAL_PATH)/proto/
-LOCAL_PROTO_JAVA_OUTPUT_PARAMS := enum_style=java
-
-LOCAL_RESOURCE_DIR := \
- $(LOCAL_PATH)/res \
-
-LOCAL_JAVA_LIBRARIES := \
- android-support-annotations \
- lib-exoplayer \
- lib-exoplayer-v2-core \
-
-LOCAL_SHARED_ANDROID_LIBRARIES := \
- android-support-compat \
- android-support-core-ui \
- android-support-tv-provider \
- android-support-v7-palette \
- android-support-v7-recyclerview \
- android-support-v17-leanback \
- android-support-tv-provider \
- tv-common \
-
-LOCAL_MIN_SDK_VERSION := 23
-
-include $(LOCAL_PATH)/buildconfig.mk
-
-include $(BUILD_STATIC_JAVA_LIBRARY)
-
diff --git a/tuner/AndroidManifest.xml b/tuner/AndroidManifest.xml
index af80f692..fd217717 100644
--- a/tuner/AndroidManifest.xml
+++ b/tuner/AndroidManifest.xml
@@ -15,8 +15,10 @@
~ limitations under the License
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
package="com.android.tv.tuner"
android:versionCode="1">
- <uses-sdk android:targetSdkVersion="26" android:minSdkVersion="21"/>
- <application />
+ <uses-sdk android:targetSdkVersion="27" android:minSdkVersion="23"/>
+ <application tools:replace="android:appComponentFactory"
+ android:appComponentFactory="android.support.v4.app.CoreComponentFactory" />
</manifest>
diff --git a/tuner/BuildConfig.java.in b/tuner/BuildConfig.java.in
deleted file mode 100644
index 85967fad..00000000
--- a/tuner/BuildConfig.java.in
+++ /dev/null
@@ -1,8 +0,0 @@
-/* This file is auto generated. Do not modify. */
-package com.android.tv.tuner;
-
-public final class BuildConfig {
- public static final boolean DEBUG = %DEBUG%;
- public static final boolean ENG = %ENG%;
- private BuildConfig() {}
-} \ No newline at end of file
diff --git a/tuner/SampleDvbTuner/AndroidManifest.xml b/tuner/SampleDvbTuner/AndroidManifest.xml
index 740989ad..5ad927e3 100755
--- a/tuner/SampleDvbTuner/AndroidManifest.xml
+++ b/tuner/SampleDvbTuner/AndroidManifest.xml
@@ -19,7 +19,7 @@
<uses-sdk
android:minSdkVersion="23"
- android:targetSdkVersion="26" />
+ android:targetSdkVersion="27" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
@@ -50,12 +50,10 @@
<application
android:name="com.android.tv.tuner.sample.dvb.app.SampleDvbTuner"
+ android:appComponentFactory="android.support.v4.app.CoreComponentFactory"
android:icon="@mipmap/ic_launcher"
android:label="@string/sample_dvb_tuner_app_name" >
- <activity
- android:name="com.google.android.gms.common.api.GoogleApiActivity"
- android:exported="false"
- android:theme="@android:style/Theme.Translucent.NoTitleBar" />
+
<activity
android:name="com.android.tv.tuner.sample.dvb.setup.SampleDvbTunerSetupActivity"
@@ -73,7 +71,7 @@
android:name="com.android.tv.tuner.sample.dvb.tvinput.SampleDvbTunerTvInputService"
android:label="@string/sample_dvb_tuner_app_name"
android:permission="android.permission.BIND_TV_INPUT"
- android:process="com.google.android.tv.tuner.sample.dvb.tvinput" >
+ android:process="com.android.tv.tuner.sample.dvb.tvinput" >
<intent-filter>
<action android:name="android.media.tv.TvInputService" />
</intent-filter>
diff --git a/tuner/SampleDvbTuner/ResourceManifest.xml b/tuner/SampleDvbTuner/ResourceManifest.xml
deleted file mode 100644
index e13f9584..00000000
--- a/tuner/SampleDvbTuner/ResourceManifest.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright (C) 2018 The Android Open Source Project
- ~
- ~ Licensed under the Apache License, Version 2.0 (the "License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the License at
- ~
- ~ http://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distributed under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License.
- -->
-
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="com.android.tv.tuner.sample.dvb" xmlns:tools="http://schemas.android.com/tools">
-
- <uses-sdk android:targetSdkVersion="26" android:minSdkVersion="23"/>
-
- <!-- Only used for resources-->
-</manifest>
diff --git a/tuner/SampleDvbTuner/build.gradle b/tuner/SampleDvbTuner/build.gradle
new file mode 100644
index 00000000..657a4258
--- /dev/null
+++ b/tuner/SampleDvbTuner/build.gradle
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+/*
+ * Experimental gradle configuration. This file may not be up to date.
+ */
+
+buildscript {
+ repositories {
+ mavenCentral()
+ google()
+ jcenter()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:3.1.4'
+ classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.5'
+ }
+}
+apply plugin: 'com.android.application'
+apply plugin: 'com.google.protobuf'
+android {
+ compileSdkVersion 26
+ buildToolsVersion '28.0.2'
+
+ dexOptions {
+ preDexLibraries = false
+ additionalParameters=['--core-library']
+ javaMaxHeapSize "6g"
+ }
+ android {
+ defaultConfig {
+ resConfigs "en"
+ }
+ }
+ defaultConfig {
+ minSdkVersion 23
+ targetSdkVersion 26
+ versionCode 1
+ versionName "1.0"
+ }
+ buildTypes {
+ debug {
+ minifyEnabled false
+ }
+ }
+ compileOptions() {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+ sourceSets {
+ main {
+ res.srcDirs = ['res']
+ java.srcDirs = ['src', '../../partner_support/src']
+ manifest.srcFile 'AndroidManifest.xml'
+ }
+ }
+}
+
+repositories {
+ mavenCentral()
+ jcenter()
+ google()
+}
+
+final String SUPPORT_LIBS_VERSION = '26.1.0'
+dependencies {
+ implementation 'com.google.android.exoplayer:exoplayer-core:2.9.0'
+ implementation 'com.google.android.exoplayer:exoplayer:r1.5.16'
+ implementation "com.android.support:palette-v7:${SUPPORT_LIBS_VERSION}"
+ implementation "com.android.support:leanback-v17:${SUPPORT_LIBS_VERSION}"
+ implementation "com.android.support:support-tv-provider:${SUPPORT_LIBS_VERSION}"
+ /*Not building with latest one (1.6.2)*/
+ annotationProcessor 'com.google.auto.value:auto-value:1.5.4'
+ implementation 'com.google.auto.value:auto-value:1.5.4'
+ implementation project(':common')
+ implementation project(':tuner')
+}
diff --git a/tuner/SampleDvbTuner/res/mipmap-hdpi/ic_launcher.png b/tuner/SampleDvbTuner/res/mipmap-hdpi/ic_launcher.png
index b5c51706..9735fec5 100644
--- a/tuner/SampleDvbTuner/res/mipmap-hdpi/ic_launcher.png
+++ b/tuner/SampleDvbTuner/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/tuner/SampleDvbTuner/res/mipmap-mdpi/ic_launcher.png b/tuner/SampleDvbTuner/res/mipmap-mdpi/ic_launcher.png
index 66297216..3bb94802 100644
--- a/tuner/SampleDvbTuner/res/mipmap-mdpi/ic_launcher.png
+++ b/tuner/SampleDvbTuner/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/tuner/SampleDvbTuner/res/mipmap-xhdpi/ic_launcher.png b/tuner/SampleDvbTuner/res/mipmap-xhdpi/ic_launcher.png
index f259d1c9..c1c9c73f 100644
--- a/tuner/SampleDvbTuner/res/mipmap-xhdpi/ic_launcher.png
+++ b/tuner/SampleDvbTuner/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/tuner/SampleDvbTuner/res/mipmap-xxhdpi/ic_launcher.png b/tuner/SampleDvbTuner/res/mipmap-xxhdpi/ic_launcher.png
index 421cd08b..0556c2c1 100644
--- a/tuner/SampleDvbTuner/res/mipmap-xxhdpi/ic_launcher.png
+++ b/tuner/SampleDvbTuner/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/tuner/SampleDvbTuner/res/mipmap-xxxhdpi/ic_launcher.png b/tuner/SampleDvbTuner/res/mipmap-xxxhdpi/ic_launcher.png
index 91be3220..652fc456 100644
--- a/tuner/SampleDvbTuner/res/mipmap-xxxhdpi/ic_launcher.png
+++ b/tuner/SampleDvbTuner/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/tuner/SampleDvbTuner/settings.gradle b/tuner/SampleDvbTuner/settings.gradle
new file mode 100644
index 00000000..13cb90f8
--- /dev/null
+++ b/tuner/SampleDvbTuner/settings.gradle
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+/*
+ * Experimental gradle configuration. This file may not be up to date.
+ */
+
+include ':common'
+include ':tuner'
+project(":common").projectDir = file("../../common")
+project(":tuner").projectDir = file(".././")
diff --git a/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/AndroidManifest.xml b/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/AndroidManifest.xml
index adb8e309..dc042286 100644
--- a/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/AndroidManifest.xml
+++ b/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/AndroidManifest.xml
@@ -36,6 +36,10 @@
<uses-feature android:name="android.software.leanback" android:required="true" />
<uses-feature android:name="android.software.live_tv" android:required="true" />
<uses-feature android:name="android.hardware.touchscreen" android:required="false"/>
- <uses-sdk android:targetSdkVersion="26" android:minSdkVersion="23"/>
- <application />
+ <uses-sdk android:targetSdkVersion="27" android:minSdkVersion="23"/>
+ <application
+ android:name=".app.SampleDvbTuner"
+ android:icon="@mipmap/ic_launcher"
+ android:label="@string/sample_dvb_tuner_app_name"
+ />
</manifest>
diff --git a/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/app/SampleDvbTuner.java b/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/app/SampleDvbTuner.java
index 15e90437..568e3c98 100644
--- a/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/app/SampleDvbTuner.java
+++ b/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/app/SampleDvbTuner.java
@@ -17,30 +17,39 @@
package com.android.tv.tuner.sample.dvb.app;
import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
import android.media.tv.TvContract;
import com.android.tv.common.BaseApplication;
-import com.android.tv.common.actions.InputSetupActionUtils;
-import com.android.tv.common.config.DefaultConfigManager;
-import com.android.tv.common.config.api.RemoteConfig;
-import com.android.tv.common.util.CommonUtils;
+import com.android.tv.common.singletons.HasSingletons;
+import com.android.tv.tuner.modules.TunerSingletonsModule;
+import com.android.tv.tuner.sample.dvb.singletons.SampleDvbSingletons;
import com.android.tv.tuner.sample.dvb.tvinput.SampleDvbTunerTvInputService;
-import com.android.tv.tuner.setup.LiveTvTunerSetupActivity;
+import com.android.tv.tuner.tvinput.factory.TunerSessionFactory;
+import com.android.tv.tuner.tvinput.factory.TunerSessionFactoryImpl;
+import dagger.android.AndroidInjector;
+import com.android.tv.common.flags.CloudEpgFlags;
+import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags;
+import javax.inject.Inject;
/** The top level application for Sample DVB Tuner. */
-public class SampleDvbTuner extends BaseApplication {
+public class SampleDvbTuner extends BaseApplication
+ implements SampleDvbSingletons, HasSingletons<SampleDvbSingletons> {
+
private String mEmbeddedInputId;
- private RemoteConfig mRemoteConfig;
+ @Inject CloudEpgFlags mCloudEpgFlags;
+ @Inject ConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags;
+ @Inject TunerSessionFactoryImpl mTunerSessionFactory;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ }
@Override
- public Intent getTunerSetupIntent(Context context) {
- // Make an intent to launch the setup activity of TV tuner input.
- Intent intent =
- CommonUtils.createSetupIntent(
- new Intent(context, LiveTvTunerSetupActivity.class), mEmbeddedInputId);
- intent.putExtra(InputSetupActionUtils.EXTRA_INPUT_ID, mEmbeddedInputId);
- return intent;
+ protected AndroidInjector<SampleDvbTuner> applicationInjector() {
+ return DaggerSampleDvbTunerComponent.builder()
+ .sampleDvbTunerModule(new SampleDvbTunerModule(this))
+ .tunerSingletonsModule(new TunerSingletonsModule(this))
+ .build();
}
@Override
@@ -54,11 +63,26 @@ public class SampleDvbTuner extends BaseApplication {
}
@Override
- public RemoteConfig getRemoteConfig() {
- if (mRemoteConfig == null) {
- // No need to synchronize this, it does not hurt to create two and throw one away.
- mRemoteConfig = DefaultConfigManager.createInstance(this).getRemoteConfig();
- }
- return mRemoteConfig;
+ public CloudEpgFlags getCloudEpgFlags() {
+ return mCloudEpgFlags;
+ }
+
+ @Override
+ public BuildType getBuildType() {
+ return BuildType.ENG;
+ }
+
+ @Override
+ public ConcurrentDvrPlaybackFlags getConcurrentDvrPlaybackFlags() {
+ return mConcurrentDvrPlaybackFlags;
+ }
+
+ @Override
+ public SampleDvbSingletons singletons() {
+ return this;
+ }
+
+ public TunerSessionFactory getTunerSessionFactory() {
+ return mTunerSessionFactory;
}
}
diff --git a/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/app/SampleDvbTunerComponent.java b/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/app/SampleDvbTunerComponent.java
new file mode 100644
index 00000000..e6c80ea9
--- /dev/null
+++ b/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/app/SampleDvbTunerComponent.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.tuner.sample.dvb.app;
+
+import dagger.Component;
+import dagger.android.AndroidInjectionModule;
+import dagger.android.AndroidInjector;
+import javax.inject.Singleton;
+
+/** Dagger component for {@link SampleDvbTuner}. */
+@Singleton
+@Component(modules = {AndroidInjectionModule.class, SampleDvbTunerModule.class})
+public interface SampleDvbTunerComponent extends AndroidInjector<SampleDvbTuner> {}
diff --git a/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/app/SampleDvbTunerModule.java b/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/app/SampleDvbTunerModule.java
new file mode 100644
index 00000000..4da3ca9d
--- /dev/null
+++ b/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/app/SampleDvbTunerModule.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.tuner.sample.dvb.app;
+
+import com.android.tv.common.flags.impl.DefaultFlagsModule;
+import com.android.tv.tuner.api.TunerFactory;
+import com.android.tv.tuner.builtin.BuiltInTunerHalFactory;
+import com.android.tv.tuner.modules.TunerModule;
+import com.android.tv.tuner.sample.dvb.setup.SampleDvbTunerSetupActivity;
+import com.android.tv.tuner.sample.dvb.tvinput.SampleDvbTunerTvInputService;
+import com.android.tv.tuner.tvinput.factory.TunerSessionFactory;
+import dagger.Module;
+import dagger.Provides;
+
+/** Dagger module for {@link SampleDvbTuner}. */
+@Module(
+ includes = {
+ DefaultFlagsModule.class,
+ SampleDvbTunerTvInputService.Module.class,
+ SampleDvbTunerSetupActivity.Module.class,
+ TunerModule.class,
+ })
+class SampleDvbTunerModule {
+ private final SampleDvbTuner mSampleDvbTuner;
+
+ SampleDvbTunerModule(SampleDvbTuner sampleDvbTuner) {
+ mSampleDvbTuner = sampleDvbTuner;
+ }
+
+ @Provides
+ public TunerSessionFactory providesTunerSessionFactory() {
+ return mSampleDvbTuner.getTunerSessionFactory();
+ }
+
+ @Provides
+ TunerFactory providesTunerFactory() {
+ return BuiltInTunerHalFactory.INSTANCE;
+ }
+}
diff --git a/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/setup/SampleDvbTunerSetupActivity.java b/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/setup/SampleDvbTunerSetupActivity.java
index 54b3a9e7..f9ef29c4 100644
--- a/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/setup/SampleDvbTunerSetupActivity.java
+++ b/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/setup/SampleDvbTunerSetupActivity.java
@@ -20,6 +20,7 @@ import android.app.FragmentManager;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Intent;
+import android.content.pm.PackageManager;
import android.media.tv.TvInputInfo;
import android.os.AsyncTask;
import android.os.Bundle;
@@ -29,26 +30,28 @@ import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;
import android.view.KeyEvent;
-import com.android.tv.common.BaseApplication;
import com.android.tv.common.feature.CommonFeatures;
+import com.android.tv.common.singletons.HasSingletons;
import com.android.tv.common.ui.setup.SetupFragment;
import com.android.tv.common.ui.setup.SetupMultiPaneFragment;
import com.android.tv.common.util.PostalCodeUtils;
-import com.android.tv.tuner.TunerHal;
import com.android.tv.tuner.sample.dvb.R;
import com.android.tv.tuner.setup.BaseTunerSetupActivity;
import com.android.tv.tuner.setup.ConnectionTypeFragment;
import com.android.tv.tuner.setup.LineupFragment;
+import com.android.tv.tuner.setup.LocationFragment;
import com.android.tv.tuner.setup.PostalCodeFragment;
import com.android.tv.tuner.setup.ScanFragment;
import com.android.tv.tuner.setup.ScanResultFragment;
import com.android.tv.tuner.setup.WelcomeFragment;
+import com.android.tv.tuner.singletons.TunerSingletons;
import com.google.android.tv.partner.support.EpgContract;
import com.google.android.tv.partner.support.EpgInput;
import com.google.android.tv.partner.support.EpgInputs;
import com.google.android.tv.partner.support.Lineup;
import com.google.android.tv.partner.support.Lineups;
import com.google.android.tv.partner.support.TunerSetupUtils;
+import dagger.android.ContributesAndroidInjector;
import java.util.ArrayList;
import java.util.List;
@@ -72,23 +75,19 @@ public class SampleDvbTunerSetupActivity extends BaseTunerSetupActivity {
private EpgInput epgInput;
private String postalCode;
private final Handler handler = new Handler();
- private final Runnable cancelFetchLineupTaskRunnable =
- new Runnable() {
- @Override
- public void run() {
- cancelFetchLineup();
- }
- };
+ private final Runnable cancelFetchLineupTaskRunnable = this::cancelFetchLineup;
private String embeddedInputId;
@Override
protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
if (DEBUG) {
Log.d(TAG, "onCreate");
}
- embeddedInputId = BaseApplication.getSingletons(this).getEmbeddedTunerInputId();
+ embeddedInputId =
+ HasSingletons.get(TunerSingletons.class, getApplicationContext())
+ .getEmbeddedTunerInputId();
new QueryEpgInputTask(embeddedInputId).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
- super.onCreate(savedInstanceState);
}
@Override
@@ -96,7 +95,7 @@ public class SampleDvbTunerSetupActivity extends BaseTunerSetupActivity {
new AsyncTask<Void, Void, Integer>() {
@Override
protected Integer doInBackground(Void... arg0) {
- return TunerHal.getTunerTypeAndCount(SampleDvbTunerSetupActivity.this).first;
+ return mTunerFactory.getTunerTypeAndCount(SampleDvbTunerSetupActivity.this).first;
}
@Override
@@ -125,10 +124,16 @@ public class SampleDvbTunerSetupActivity extends BaseTunerSetupActivity {
break;
default:
String postalCode = PostalCodeUtils.getLastPostalCode(this);
- if (mNeedToShowPostalCodeFragment
- || (CommonFeatures.ENABLE_CLOUD_EPG_REGION.isEnabled(
+ boolean needLocation =
+ CommonFeatures.ENABLE_CLOUD_EPG_REGION.isEnabled(
getApplicationContext())
- && TextUtils.isEmpty(postalCode))) {
+ && TextUtils.isEmpty(postalCode);
+ if (needLocation
+ && checkSelfPermission(
+ android.Manifest.permission.ACCESS_COARSE_LOCATION)
+ != PackageManager.PERMISSION_GRANTED) {
+ showLocationFragment();
+ } else if (mNeedToShowPostalCodeFragment || needLocation) {
// 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.
@@ -144,6 +149,26 @@ public class SampleDvbTunerSetupActivity extends BaseTunerSetupActivity {
break;
}
return true;
+ case LocationFragment.ACTION_CATEGORY:
+ switch (actionId) {
+ case LocationFragment.ACTION_ALLOW_PERMISSION:
+ String postalCode =
+ params == null
+ ? null
+ : params.getString(LocationFragment.KEY_POSTAL_CODE);
+ if (postalCode == null) {
+ showPostalCodeFragment();
+ } else {
+ this.postalCode = postalCode;
+ restartFetchLineupTask();
+ showConnectionTypeFragment();
+ }
+ break;
+ default:
+ cancelFetchLineup();
+ showConnectionTypeFragment();
+ }
+ return true;
case PostalCodeFragment.ACTION_CATEGORY:
lineups = null;
selectedLineup = null;
@@ -220,8 +245,7 @@ public class SampleDvbTunerSetupActivity extends BaseTunerSetupActivity {
case ScanResultFragment.ACTION_CATEGORY:
switch (actionId) {
case SetupMultiPaneFragment.ACTION_DONE:
- new SampleDvbTunerSetupActivity.InsertOrModifyEpgInputTask(
- selectedLineup, embeddedInputId)
+ new InsertOrModifyEpgInputTask(selectedLineup, embeddedInputId)
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
break;
default:
@@ -340,7 +364,9 @@ public class SampleDvbTunerSetupActivity extends BaseTunerSetupActivity {
private void restartFetchLineupTask() {
if (!CommonFeatures.ENABLE_CLOUD_EPG_REGION.isEnabled(getApplicationContext())
- || TextUtils.isEmpty(postalCode)) {
+ || TextUtils.isEmpty(postalCode)
+ || checkSelfPermission(android.Manifest.permission.ACCESS_COARSE_LOCATION)
+ != PackageManager.PERMISSION_GRANTED) {
return;
}
if (fetchLineupTask != null) {
@@ -441,6 +467,16 @@ public class SampleDvbTunerSetupActivity extends BaseTunerSetupActivity {
}
}
+ /**
+ * Exports {@link SampleDvbTunerSetupActivity} for Dagger codegen to create the appropriate
+ * injector.
+ */
+ @dagger.Module
+ public abstract static class Module {
+ @ContributesAndroidInjector
+ abstract SampleDvbTunerSetupActivity contributeSampleDvbTunerSetupActivityInjector();
+ }
+
private class QueryEpgInputTask extends AsyncTask<Void, Void, EpgInput> {
private final String inputId;
diff --git a/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/singletons/SampleDvbSingletons.java b/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/singletons/SampleDvbSingletons.java
new file mode 100644
index 00000000..22a67905
--- /dev/null
+++ b/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/singletons/SampleDvbSingletons.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.sample.dvb.singletons;
+
+import com.android.tv.common.BaseSingletons;
+import com.android.tv.tuner.singletons.TunerSingletons;
+
+/** Singletons for SampleDvbTuner. */
+public interface SampleDvbSingletons extends BaseSingletons, TunerSingletons {}
diff --git a/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/tvinput/SampleDvbTunerTvInputService.java b/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/tvinput/SampleDvbTunerTvInputService.java
index ae15affc..a31faa81 100644
--- a/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/tvinput/SampleDvbTunerTvInputService.java
+++ b/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/tvinput/SampleDvbTunerTvInputService.java
@@ -16,6 +16,18 @@
package com.android.tv.tuner.sample.dvb.tvinput;
import com.android.tv.tuner.tvinput.BaseTunerTvInputService;
+import dagger.android.ContributesAndroidInjector;
/** Sample DVB Tuner {@link android.media.tv.TvInputService}. */
-public class SampleDvbTunerTvInputService extends BaseTunerTvInputService {}
+public class SampleDvbTunerTvInputService extends BaseTunerTvInputService {
+
+ /**
+ * Exports {@link SampleDvbTunerTvInputService} for Dagger codegen to create the appropriate
+ * injector.
+ */
+ @dagger.Module
+ public abstract static class Module {
+ @ContributesAndroidInjector
+ abstract SampleDvbTunerTvInputService contributesSampleDvbTunerTvInputServiceInjector();
+ }
+}
diff --git a/tuner/SampleNetworkTuner/AndroidManifest.xml b/tuner/SampleNetworkTuner/AndroidManifest.xml
new file mode 100755
index 00000000..0ec9afca
--- /dev/null
+++ b/tuner/SampleNetworkTuner/AndroidManifest.xml
@@ -0,0 +1,90 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2018 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.tv.tuner.sample.network" >
+
+ <uses-sdk
+ android:minSdkVersion="23"
+ android:targetSdkVersion="27" />
+
+ <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+ <uses-permission android:name="android.permission.READ_CONTENT_RATING_SYSTEMS" />
+ <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+ <uses-permission android:name="android.permission.READ_TV_LISTINGS" />
+ <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
+ <uses-permission android:name="com.android.providers.tv.permission.READ_EPG_DATA" />
+ <uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA" />
+
+ <!-- Permissions/feature for USB tuner -->
+ <uses-permission android:name="android.permission.DVB_DEVICE" />
+
+ <uses-feature
+ android:name="android.hardware.usb.host"
+ android:required="false" />
+
+ <!-- Limit only for Android TV -->
+ <uses-feature
+ android:name="android.software.leanback"
+ android:required="true" />
+ <uses-feature
+ android:name="android.software.live_tv"
+ android:required="true" />
+ <uses-feature
+ android:name="android.hardware.touchscreen"
+ android:required="false" />
+
+ <application
+ android:name="com.android.tv.tuner.sample.network.app.SampleNetworkTuner"
+ android:appComponentFactory="android.support.v4.app.CoreComponentFactory"
+ android:icon="@mipmap/ic_launcher"
+ android:label="@string/sample_network_tuner_app_name" >
+
+
+ <activity
+ android:name="com.android.tv.tuner.sample.network.setup.SampleNetworkTunerSetupActivity"
+ android:configChanges="keyboard|keyboardHidden"
+ android:exported="true"
+ android:label="@string/sample_network_tuner_app_name"
+ android:launchMode="singleInstance"
+ android:theme="@style/Theme.Setup.GuidedStep" >
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ </intent-filter>
+ </activity>
+
+ <service
+ android:name="com.android.tv.tuner.sample.network.tvinput.SampleNetworkTunerTvInputService"
+ android:label="@string/sample_network_tuner_app_name"
+ android:permission="android.permission.BIND_TV_INPUT"
+ android:process="com.android.tv.tuner.sample.network.tvinput" >
+ <intent-filter>
+ <action android:name="android.media.tv.TvInputService" />
+ </intent-filter>
+
+ <meta-data
+ android:name="android.media.tv.input"
+ android:resource="@xml/sample_network_tvinputservice" />
+ </service>
+ <service
+ android:name="com.android.tv.tuner.tvinput.TunerStorageCleanUpService"
+ android:exported="false"
+ android:permission="android.permission.BIND_JOB_SERVICE"
+ android:process="com.android.tv.tuner" />
+ </application>
+
+</manifest> \ No newline at end of file
diff --git a/tuner/SampleNetworkTuner/build.gradle b/tuner/SampleNetworkTuner/build.gradle
new file mode 100644
index 00000000..657a4258
--- /dev/null
+++ b/tuner/SampleNetworkTuner/build.gradle
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+/*
+ * Experimental gradle configuration. This file may not be up to date.
+ */
+
+buildscript {
+ repositories {
+ mavenCentral()
+ google()
+ jcenter()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:3.1.4'
+ classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.5'
+ }
+}
+apply plugin: 'com.android.application'
+apply plugin: 'com.google.protobuf'
+android {
+ compileSdkVersion 26
+ buildToolsVersion '28.0.2'
+
+ dexOptions {
+ preDexLibraries = false
+ additionalParameters=['--core-library']
+ javaMaxHeapSize "6g"
+ }
+ android {
+ defaultConfig {
+ resConfigs "en"
+ }
+ }
+ defaultConfig {
+ minSdkVersion 23
+ targetSdkVersion 26
+ versionCode 1
+ versionName "1.0"
+ }
+ buildTypes {
+ debug {
+ minifyEnabled false
+ }
+ }
+ compileOptions() {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+ sourceSets {
+ main {
+ res.srcDirs = ['res']
+ java.srcDirs = ['src', '../../partner_support/src']
+ manifest.srcFile 'AndroidManifest.xml'
+ }
+ }
+}
+
+repositories {
+ mavenCentral()
+ jcenter()
+ google()
+}
+
+final String SUPPORT_LIBS_VERSION = '26.1.0'
+dependencies {
+ implementation 'com.google.android.exoplayer:exoplayer-core:2.9.0'
+ implementation 'com.google.android.exoplayer:exoplayer:r1.5.16'
+ implementation "com.android.support:palette-v7:${SUPPORT_LIBS_VERSION}"
+ implementation "com.android.support:leanback-v17:${SUPPORT_LIBS_VERSION}"
+ implementation "com.android.support:support-tv-provider:${SUPPORT_LIBS_VERSION}"
+ /*Not building with latest one (1.6.2)*/
+ annotationProcessor 'com.google.auto.value:auto-value:1.5.4'
+ implementation 'com.google.auto.value:auto-value:1.5.4'
+ implementation project(':common')
+ implementation project(':tuner')
+}
diff --git a/tuner/SampleNetworkTuner/res/mipmap-hdpi/ic_launcher.png b/tuner/SampleNetworkTuner/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 00000000..e0d8e84a
--- /dev/null
+++ b/tuner/SampleNetworkTuner/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/tuner/SampleNetworkTuner/res/mipmap-mdpi/ic_launcher.png b/tuner/SampleNetworkTuner/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 00000000..5e99cdf5
--- /dev/null
+++ b/tuner/SampleNetworkTuner/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/tuner/SampleNetworkTuner/res/mipmap-xhdpi/ic_launcher.png b/tuner/SampleNetworkTuner/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 00000000..2f6336fb
--- /dev/null
+++ b/tuner/SampleNetworkTuner/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/tuner/SampleNetworkTuner/res/mipmap-xxhdpi/ic_launcher.png b/tuner/SampleNetworkTuner/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 00000000..d14775be
--- /dev/null
+++ b/tuner/SampleNetworkTuner/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/tuner/SampleNetworkTuner/res/mipmap-xxxhdpi/ic_launcher.png b/tuner/SampleNetworkTuner/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 00000000..ab4e8dfd
--- /dev/null
+++ b/tuner/SampleNetworkTuner/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/tuner/SampleNetworkTuner/res/values/strings.xml b/tuner/SampleNetworkTuner/res/values/strings.xml
new file mode 100644
index 00000000..47a4c284
--- /dev/null
+++ b/tuner/SampleNetworkTuner/res/values/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of this application. It appears in TV app UI, as one of available TV inputs. -->
+ <string name="sample_network_tuner_app_name" translatable="false">Sample Network Tuner</string>
+</resources> \ No newline at end of file
diff --git a/tuner/SampleNetworkTuner/res/xml/sample_network_tvinputservice.xml b/tuner/SampleNetworkTuner/res/xml/sample_network_tvinputservice.xml
new file mode 100644
index 00000000..7e6083db
--- /dev/null
+++ b/tuner/SampleNetworkTuner/res/xml/sample_network_tvinputservice.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<!--
+/**
+ * Copyright (c) 2014, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+-->
+
+<tv-input xmlns:android="http://schemas.android.com/apk/res/android"
+ android:setupActivity="com.android.tv.tuner.sample.network.setup.SampleNetworkTunerSetupActivity"
+ android:canRecord="true"
+ android:tunerCount="1" />
diff --git a/tuner/SampleNetworkTuner/settings.gradle b/tuner/SampleNetworkTuner/settings.gradle
new file mode 100644
index 00000000..13cb90f8
--- /dev/null
+++ b/tuner/SampleNetworkTuner/settings.gradle
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+/*
+ * Experimental gradle configuration. This file may not be up to date.
+ */
+
+include ':common'
+include ':tuner'
+project(":common").projectDir = file("../../common")
+project(":tuner").projectDir = file(".././")
diff --git a/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/AndroidManifest.xml b/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/AndroidManifest.xml
new file mode 100644
index 00000000..dddd8a4b
--- /dev/null
+++ b/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/AndroidManifest.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2018 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.tv.tuner.sample.network" xmlns:tools="http://schemas.android.com/tools">
+
+ <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+ <uses-permission android:name="android.permission.READ_CONTENT_RATING_SYSTEMS" />
+ <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+ <uses-permission android:name="android.permission.READ_TV_LISTINGS" />
+ <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
+ <uses-permission android:name="com.android.providers.tv.permission.READ_EPG_DATA" />
+ <uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA" />
+
+ <!-- Permissions/feature for USB tuner -->
+ <uses-permission android:name="android.permission.DVB_DEVICE" />
+ <uses-feature android:name="android.hardware.usb.host" android:required="false" />
+
+ <!-- Limit only for Android TV -->
+ <uses-feature android:name="android.software.leanback" android:required="true" />
+ <uses-feature android:name="android.software.live_tv" android:required="true" />
+ <uses-feature android:name="android.hardware.touchscreen" android:required="false"/>
+ <uses-sdk android:targetSdkVersion="27" android:minSdkVersion="23"/>
+ <application
+ android:name=".app.SampleNetworkTuner"
+ android:icon="@mipmap/ic_launcher"
+ android:label="@string/sample_network_tuner_app_name"
+ />
+</manifest>
diff --git a/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/README.md b/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/README.md
new file mode 100644
index 00000000..9df86c53
--- /dev/null
+++ b/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/README.md
@@ -0,0 +1,37 @@
+# SampleNetworkTuner
+
+SampleNetworkTuner is the reference DVB Tuner. Partners should copy these files
+to their own directory and modify as needed.
+
+## Prerequisites
+
+* A DVB Tuner
+ * A Nexus player with a USB Tuner attached will work.
+* system privileged app
+ * The DVB_DEVICE permission requires the app to be a privileged system app
+
+## First install
+
+#### Root
+
+```bash
+adb root
+adb remount
+```
+
+### modify privapp-permissions-atv.xml
+
+Edit system/etc/permissions/privapp-permissions-atv.xml
+
+```xml
+<privapp-permissions package="com.android.tv.tuner.sample.network">
+ <permission name="android.permission.DVB_DEVICE"/>
+</privapp-permissions>
+```
+
+### Push to system/priv-app
+
+```bash
+adb shell mkdir /system/priv-app/SampleNetworkTuner
+adb push <path to apk> /system/priv-app/SampleNetworkTuner/SampleNetworkTuner.apk
+```
diff --git a/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/app/SampleNetworkTuner.java b/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/app/SampleNetworkTuner.java
new file mode 100644
index 00000000..eb5b2ad4
--- /dev/null
+++ b/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/app/SampleNetworkTuner.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.sample.network.app;
+
+import android.content.ComponentName;
+import android.media.tv.TvContract;
+import com.android.tv.common.BaseApplication;
+import com.android.tv.common.singletons.HasSingletons;
+import com.android.tv.tuner.modules.TunerSingletonsModule;
+import com.android.tv.tuner.sample.network.singletons.SampleNetworkSingletons;
+import com.android.tv.tuner.sample.network.tvinput.SampleNetworkTunerTvInputService;
+import com.android.tv.tuner.tvinput.factory.TunerSessionFactory;
+import com.android.tv.tuner.tvinput.factory.TunerSessionFactoryImpl;
+import dagger.android.AndroidInjector;
+import com.android.tv.common.flags.CloudEpgFlags;
+import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags;
+import javax.inject.Inject;
+
+/** The top level application for Sample DVB Tuner. */
+public class SampleNetworkTuner extends BaseApplication
+ implements SampleNetworkSingletons, HasSingletons<SampleNetworkSingletons> {
+
+ private String mEmbeddedInputId;
+ @Inject CloudEpgFlags mCloudEpgFlags;
+ @Inject ConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags;
+ @Inject TunerSessionFactoryImpl mTunerSessionFactory;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ }
+
+ @Override
+ protected AndroidInjector<SampleNetworkTuner> applicationInjector() {
+ return DaggerSampleNetworkTunerComponent.builder()
+ .sampleNetworkTunerModule(new SampleNetworkTunerModule(this))
+ .tunerSingletonsModule(new TunerSingletonsModule(this))
+ .build();
+ }
+
+ @Override
+ public synchronized String getEmbeddedTunerInputId() {
+ if (mEmbeddedInputId == null) {
+ mEmbeddedInputId =
+ TvContract.buildInputId(
+ new ComponentName(this, SampleNetworkTunerTvInputService.class));
+ }
+ return mEmbeddedInputId;
+ }
+
+ @Override
+ public CloudEpgFlags getCloudEpgFlags() {
+ return mCloudEpgFlags;
+ }
+
+ @Override
+ public BuildType getBuildType() {
+ return BuildType.ENG;
+ }
+
+ @Override
+ public ConcurrentDvrPlaybackFlags getConcurrentDvrPlaybackFlags() {
+ return mConcurrentDvrPlaybackFlags;
+ }
+
+ @Override
+ public SampleNetworkSingletons singletons() {
+ return this;
+ }
+
+ public TunerSessionFactory getTunerSessionFactory() {
+ return mTunerSessionFactory;
+ }
+}
diff --git a/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/app/SampleNetworkTunerComponent.java b/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/app/SampleNetworkTunerComponent.java
new file mode 100644
index 00000000..b10105b3
--- /dev/null
+++ b/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/app/SampleNetworkTunerComponent.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.tuner.sample.network.app;
+
+import dagger.Component;
+import dagger.android.AndroidInjectionModule;
+import dagger.android.AndroidInjector;
+import javax.inject.Singleton;
+
+/** Dagger component for {@link SampleNetworkTuner}. */
+@Singleton
+@Component(modules = {AndroidInjectionModule.class, SampleNetworkTunerModule.class})
+public interface SampleNetworkTunerComponent extends AndroidInjector<SampleNetworkTuner> {}
diff --git a/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/app/SampleNetworkTunerModule.java b/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/app/SampleNetworkTunerModule.java
new file mode 100644
index 00000000..d974e20a
--- /dev/null
+++ b/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/app/SampleNetworkTunerModule.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.tuner.sample.network.app;
+
+import com.android.tv.common.flags.impl.DefaultFlagsModule;
+import com.android.tv.tuner.api.TunerFactory;
+import com.android.tv.tuner.builtin.BuiltInTunerHalFactory;
+import com.android.tv.tuner.modules.TunerModule;
+import com.android.tv.tuner.sample.network.setup.SampleNetworkTunerSetupActivity;
+import com.android.tv.tuner.sample.network.tvinput.SampleNetworkTunerTvInputService;
+import com.android.tv.tuner.tvinput.factory.TunerSessionFactory;
+import dagger.Module;
+import dagger.Provides;
+
+/** Dagger module for {@link SampleNetworkTuner}. */
+@Module(
+ includes = {
+ DefaultFlagsModule.class,
+ SampleNetworkTunerTvInputService.Module.class,
+ SampleNetworkTunerSetupActivity.Module.class,
+ TunerModule.class,
+ })
+class SampleNetworkTunerModule {
+ private final SampleNetworkTuner mSampleNetworkTuner;
+
+ SampleNetworkTunerModule(SampleNetworkTuner sampleNetworkTuner) {
+ mSampleNetworkTuner = sampleNetworkTuner;
+ }
+
+ @Provides
+ public TunerSessionFactory providesTunerSessionFactory() {
+ return mSampleNetworkTuner.getTunerSessionFactory();
+ }
+
+ @Provides
+ TunerFactory providesTunerFactory() {
+ return BuiltInTunerHalFactory.INSTANCE;
+ }
+}
diff --git a/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/setup/SampleNetworkTunerSetupActivity.java b/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/setup/SampleNetworkTunerSetupActivity.java
new file mode 100644
index 00000000..fd783c4f
--- /dev/null
+++ b/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/setup/SampleNetworkTunerSetupActivity.java
@@ -0,0 +1,500 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.sample.network.setup;
+
+import android.app.FragmentManager;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.media.tv.TvInputInfo;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Pair;
+import android.view.KeyEvent;
+import com.android.tv.common.feature.CommonFeatures;
+import com.android.tv.common.singletons.HasSingletons;
+import com.android.tv.common.ui.setup.SetupFragment;
+import com.android.tv.common.ui.setup.SetupMultiPaneFragment;
+import com.android.tv.common.util.PostalCodeUtils;
+import com.android.tv.tuner.sample.network.R;
+import com.android.tv.tuner.setup.BaseTunerSetupActivity;
+import com.android.tv.tuner.setup.ConnectionTypeFragment;
+import com.android.tv.tuner.setup.LineupFragment;
+import com.android.tv.tuner.setup.LocationFragment;
+import com.android.tv.tuner.setup.PostalCodeFragment;
+import com.android.tv.tuner.setup.ScanFragment;
+import com.android.tv.tuner.setup.ScanResultFragment;
+import com.android.tv.tuner.setup.WelcomeFragment;
+import com.android.tv.tuner.singletons.TunerSingletons;
+import com.google.android.tv.partner.support.EpgContract;
+import com.google.android.tv.partner.support.EpgInput;
+import com.google.android.tv.partner.support.EpgInputs;
+import com.google.android.tv.partner.support.Lineup;
+import com.google.android.tv.partner.support.Lineups;
+import com.google.android.tv.partner.support.TunerSetupUtils;
+import dagger.android.ContributesAndroidInjector;
+import java.util.ArrayList;
+import java.util.List;
+
+/** An activity that serves Live TV tuner setup process. */
+public class SampleNetworkTunerSetupActivity extends BaseTunerSetupActivity {
+ private static final String TAG = "SampleNetworkTunerSetupActivity";
+ private static final boolean DEBUG = false;
+
+ private static final int FETCH_LINEUP_TIMEOUT_MS = 10000; // 10 seconds
+ private static final int FETCH_LINEUP_RETRY_TIMEOUT_MS = 20000; // 20 seconds
+ private static final String OTAD_PREFIX = "OTAD";
+ private static final String STRING_BROADCAST_DIGITAL = "Broadcast Digital";
+
+ private LineupFragment currentLineupFragment;
+
+ private List<String> channelNumbers;
+ private List<Lineup> lineups;
+ private Lineup selectedLineup;
+ private List<Pair<Lineup, Integer>> lineupMatchCountPair;
+ private FetchLineupTask fetchLineupTask;
+ private EpgInput epgInput;
+ private String postalCode;
+ private final Handler handler = new Handler();
+ private final Runnable cancelFetchLineupTaskRunnable = this::cancelFetchLineup;
+ private String embeddedInputId;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (DEBUG) {
+ Log.d(TAG, "onCreate");
+ }
+ embeddedInputId =
+ HasSingletons.get(TunerSingletons.class, getApplicationContext())
+ .getEmbeddedTunerInputId();
+ new QueryEpgInputTask(embeddedInputId).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+
+ @Override
+ protected void executeGetTunerTypeAndCountAsyncTask() {
+ new AsyncTask<Void, Void, Integer>() {
+ @Override
+ protected Integer doInBackground(Void... arg0) {
+ return mTunerFactory.getTunerTypeAndCount(SampleNetworkTunerSetupActivity.this)
+ .first;
+ }
+
+ @Override
+ protected void onPostExecute(Integer result) {
+ if (!SampleNetworkTunerSetupActivity.this.isDestroyed()) {
+ mTunerType = result;
+ if (result == null) {
+ finish();
+ } else if (!mActivityStopped) {
+ showInitialFragment();
+ } else {
+ mPendingShowInitialFragment = true;
+ }
+ }
+ }
+ }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+
+ @Override
+ protected boolean executeAction(String category, int actionId, Bundle params) {
+ switch (category) {
+ case WelcomeFragment.ACTION_CATEGORY:
+ switch (actionId) {
+ case SetupMultiPaneFragment.ACTION_DONE:
+ super.executeAction(category, actionId, params);
+ break;
+ default:
+ String postalCode = PostalCodeUtils.getLastPostalCode(this);
+ boolean needLocation =
+ CommonFeatures.ENABLE_CLOUD_EPG_REGION.isEnabled(
+ getApplicationContext())
+ && TextUtils.isEmpty(postalCode);
+ if (needLocation
+ && checkSelfPermission(
+ android.Manifest.permission.ACCESS_COARSE_LOCATION)
+ != PackageManager.PERMISSION_GRANTED) {
+ showLocationFragment();
+ } else if (mNeedToShowPostalCodeFragment || needLocation) {
+ // 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 {
+ lineups = null;
+ selectedLineup = null;
+ this.postalCode = postalCode;
+ restartFetchLineupTask();
+ showConnectionTypeFragment();
+ }
+ break;
+ }
+ return true;
+ case LocationFragment.ACTION_CATEGORY:
+ switch (actionId) {
+ case LocationFragment.ACTION_ALLOW_PERMISSION:
+ String postalCode =
+ params == null
+ ? null
+ : params.getString(LocationFragment.KEY_POSTAL_CODE);
+ if (postalCode == null) {
+ showPostalCodeFragment();
+ } else {
+ this.postalCode = postalCode;
+ restartFetchLineupTask();
+ showConnectionTypeFragment();
+ }
+ break;
+ default:
+ cancelFetchLineup();
+ showConnectionTypeFragment();
+ }
+ return true;
+ case PostalCodeFragment.ACTION_CATEGORY:
+ lineups = null;
+ selectedLineup = null;
+ switch (actionId) {
+ case SetupMultiPaneFragment.ACTION_DONE:
+ String postalCode = params.getString(PostalCodeFragment.KEY_POSTAL_CODE);
+ if (postalCode != null) {
+ this.postalCode = postalCode;
+ restartFetchLineupTask();
+ }
+ // fall through
+ case SetupMultiPaneFragment.ACTION_SKIP:
+ showConnectionTypeFragment();
+ break;
+ default: // fall out
+ }
+ return true;
+ case ConnectionTypeFragment.ACTION_CATEGORY:
+ channelNumbers = null;
+ lineupMatchCountPair = null;
+ return super.executeAction(category, actionId, params);
+ case ScanFragment.ACTION_CATEGORY:
+ switch (actionId) {
+ case ScanFragment.ACTION_CANCEL:
+ getFragmentManager().popBackStack();
+ return true;
+ case ScanFragment.ACTION_FINISH:
+ clearTunerHal();
+ channelNumbers =
+ params.getStringArrayList(ScanFragment.KEY_CHANNEL_NUMBERS);
+ selectedLineup = null;
+ if (CommonFeatures.ENABLE_CLOUD_EPG_REGION.isEnabled(
+ getApplicationContext())
+ && channelNumbers != null
+ && !channelNumbers.isEmpty()
+ && !TextUtils.isEmpty(this.postalCode)) {
+ showLineupFragment();
+ } else {
+ showScanResultFragment();
+ }
+ return true;
+ default: // fall out
+ }
+ break;
+ case LineupFragment.ACTION_CATEGORY:
+ switch (actionId) {
+ case LineupFragment.ACTION_SKIP:
+ selectedLineup = null;
+ currentLineupFragment = null;
+ showScanResultFragment();
+ break;
+ case LineupFragment.ACTION_ID_RETRY:
+ currentLineupFragment.onRetry();
+ restartFetchLineupTask();
+ handler.postDelayed(
+ cancelFetchLineupTaskRunnable, FETCH_LINEUP_RETRY_TIMEOUT_MS);
+ break;
+ default:
+ if (actionId >= 0 && actionId < lineupMatchCountPair.size()) {
+ if (DEBUG) {
+ if (selectedLineup != null) {
+ Log.d(
+ TAG,
+ "Lineup " + selectedLineup.getName() + " is selected.");
+ }
+ }
+ selectedLineup = lineupMatchCountPair.get(actionId).first;
+ }
+ currentLineupFragment = null;
+ showScanResultFragment();
+ break;
+ }
+ return true;
+ case ScanResultFragment.ACTION_CATEGORY:
+ switch (actionId) {
+ case SetupMultiPaneFragment.ACTION_DONE:
+ new InsertOrModifyEpgInputTask(selectedLineup, embeddedInputId)
+ .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ break;
+ default:
+ // scan again
+ if (lineups == null || lineups.isEmpty()) {
+ lineups = null;
+ restartFetchLineupTask();
+ }
+ super.executeAction(category, actionId, params);
+ break;
+ }
+ return true;
+ default: // fall out
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_BACK) {
+ FragmentManager manager = getFragmentManager();
+ int count = manager.getBackStackEntryCount();
+ if (count > 0) {
+ String lastTag = manager.getBackStackEntryAt(count - 1).getName();
+ if (LineupFragment.class.getCanonicalName().equals(lastTag) && count >= 2) {
+ // Pops fragment including ScanFragment.
+ manager.popBackStack(
+ manager.getBackStackEntryAt(count - 2).getName(),
+ FragmentManager.POP_BACK_STACK_INCLUSIVE);
+ return true;
+ }
+ if (ScanResultFragment.class.getCanonicalName().equals(lastTag) && count >= 2) {
+ String secondLastTag = manager.getBackStackEntryAt(count - 2).getName();
+ if (ScanFragment.class.getCanonicalName().equals(secondLastTag)) {
+ // Pops fragment including ScanFragment.
+ manager.popBackStack(
+ secondLastTag, FragmentManager.POP_BACK_STACK_INCLUSIVE);
+ return true;
+ }
+ if (LineupFragment.class.getCanonicalName().equals(secondLastTag)) {
+ currentLineupFragment =
+ (LineupFragment) manager.findFragmentByTag(secondLastTag);
+ if (lineups == null || lineups.isEmpty()) {
+ lineups = null;
+ restartFetchLineupTask();
+ }
+ }
+ } else if (ScanFragment.class.getCanonicalName().equals(lastTag)) {
+ mLastScanFragment.finishScan(true);
+ return true;
+ }
+ }
+ }
+ return super.onKeyUp(keyCode, event);
+ }
+
+ private void showLineupFragment() {
+ if (lineupMatchCountPair == null && lineups != null) {
+ lineupMatchCountPair = TunerSetupUtils.lineupChannelMatchCount(lineups, channelNumbers);
+ }
+ currentLineupFragment = new LineupFragment();
+ currentLineupFragment.setArguments(getArgsForLineupFragment());
+ currentLineupFragment.setShortDistance(
+ SetupFragment.FRAGMENT_ENTER_TRANSITION | SetupFragment.FRAGMENT_RETURN_TRANSITION);
+ handler.removeCallbacksAndMessages(null);
+ showFragment(currentLineupFragment, true);
+ handler.postDelayed(cancelFetchLineupTaskRunnable, FETCH_LINEUP_TIMEOUT_MS);
+ }
+
+ private Bundle getArgsForLineupFragment() {
+ Bundle args = new Bundle();
+ if (lineupMatchCountPair == null) {
+ return args;
+ }
+ ArrayList<String> lineupNames = new ArrayList<>(lineupMatchCountPair.size());
+ ArrayList<Integer> matchNumbers = new ArrayList<>(lineupMatchCountPair.size());
+ int defaultLineupIndex = 0;
+ for (Pair<Lineup, Integer> pair : lineupMatchCountPair) {
+ Lineup lineup = pair.first;
+ String name;
+ if (!TextUtils.isEmpty(lineup.getName())) {
+ name = lineup.getName();
+ } else {
+ name = lineup.getId();
+ }
+ if (name.equals(OTAD_PREFIX + postalCode) || name.equals(STRING_BROADCAST_DIGITAL)) {
+ // rename OTA / antenna lineups
+ name = getString(R.string.ut_lineup_name_antenna);
+ }
+ lineupNames.add(name);
+ matchNumbers.add(pair.second);
+ if (epgInput != null && TextUtils.equals(lineup.getId(), epgInput.getLineupId())) {
+ // The last index is the current one.
+ defaultLineupIndex = lineupNames.size() - 1;
+ }
+ }
+ args.putStringArrayList(LineupFragment.KEY_LINEUP_NAMES, lineupNames);
+ args.putIntegerArrayList(LineupFragment.KEY_MATCH_NUMBERS, matchNumbers);
+ args.putInt(LineupFragment.KEY_DEFAULT_LINEUP, defaultLineupIndex);
+ return args;
+ }
+
+ private void cancelFetchLineup() {
+ if (fetchLineupTask == null) {
+ return;
+ }
+ AsyncTask.Status status = fetchLineupTask.getStatus();
+ if (status == AsyncTask.Status.RUNNING || status == AsyncTask.Status.PENDING) {
+ fetchLineupTask.cancel(true);
+ fetchLineupTask = null;
+ if (currentLineupFragment != null) {
+ currentLineupFragment.onLineupNotFound();
+ }
+ }
+ }
+
+ private void restartFetchLineupTask() {
+ if (!CommonFeatures.ENABLE_CLOUD_EPG_REGION.isEnabled(getApplicationContext())
+ || TextUtils.isEmpty(postalCode)
+ || checkSelfPermission(android.Manifest.permission.ACCESS_COARSE_LOCATION)
+ != PackageManager.PERMISSION_GRANTED) {
+ return;
+ }
+ if (fetchLineupTask != null) {
+ fetchLineupTask.cancel(true);
+ }
+ handler.removeCallbacksAndMessages(null);
+ fetchLineupTask = new FetchLineupTask(getContentResolver(), postalCode);
+ fetchLineupTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+
+ private class FetchLineupTask extends AsyncTask<Void, Void, List<Lineup>> {
+ private final ContentResolver contentResolver;
+ private final String postalCode;
+
+ FetchLineupTask(ContentResolver contentResolver, String postalCode) {
+ this.contentResolver = contentResolver;
+ this.postalCode = postalCode;
+ }
+
+ @Override
+ protected List<Lineup> doInBackground(Void... args) {
+ if (contentResolver == null || TextUtils.isEmpty(postalCode)) {
+ return new ArrayList<>();
+ }
+ return new ArrayList<>(Lineups.query(contentResolver, postalCode));
+ }
+
+ @Override
+ protected void onPostExecute(List<Lineup> lineups) {
+ if (DEBUG) {
+ if (lineups != null) {
+ Log.d(TAG, "FetchLineupTask fetched " + lineups.size() + " lineups");
+ } else {
+ Log.d(TAG, "FetchLineupTask returned null");
+ }
+ }
+ SampleNetworkTunerSetupActivity.this.lineups = lineups;
+ if (currentLineupFragment != null) {
+ if (lineups == null || lineups.isEmpty()) {
+ currentLineupFragment.onLineupNotFound();
+ } else {
+ lineupMatchCountPair =
+ TunerSetupUtils.lineupChannelMatchCount(
+ SampleNetworkTunerSetupActivity.this.lineups, channelNumbers);
+ currentLineupFragment.onLineupFound(getArgsForLineupFragment());
+ }
+ }
+ }
+ }
+
+ private class InsertOrModifyEpgInputTask extends AsyncTask<Void, Void, Void> {
+ private final Lineup lineup;
+ private final String inputId;
+
+ InsertOrModifyEpgInputTask(@Nullable Lineup lineup, String inputId) {
+ this.lineup = lineup;
+ this.inputId = inputId;
+ }
+
+ @Override
+ protected Void doInBackground(Void... args) {
+ if (lineup == null
+ || (SampleNetworkTunerSetupActivity.this.epgInput != null
+ && TextUtils.equals(
+ lineup.getId(),
+ SampleNetworkTunerSetupActivity.this.epgInput.getLineupId()))) {
+ return null;
+ }
+ ContentValues values = new ContentValues();
+ values.put(EpgContract.EpgInputs.COLUMN_INPUT_ID, inputId);
+ values.put(EpgContract.EpgInputs.COLUMN_LINEUP_ID, lineup.getId());
+
+ ContentResolver contentResolver = getContentResolver();
+ if (SampleNetworkTunerSetupActivity.this.epgInput != null) {
+ values.put(
+ EpgContract.EpgInputs.COLUMN_ID,
+ SampleNetworkTunerSetupActivity.this.epgInput.getId());
+ EpgInputs.update(contentResolver, EpgInput.createEpgChannel(values));
+ return null;
+ }
+ EpgInput epgInput = EpgInputs.queryEpgInput(contentResolver, inputId);
+ if (epgInput == null) {
+ contentResolver.insert(EpgContract.EpgInputs.CONTENT_URI, values);
+ } else {
+ values.put(EpgContract.EpgInputs.COLUMN_ID, epgInput.getId());
+ EpgInputs.update(contentResolver, EpgInput.createEpgChannel(values));
+ }
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Void result) {
+ Intent data = new Intent();
+ data.putExtra(TvInputInfo.EXTRA_INPUT_ID, inputId);
+ data.putExtra(EpgContract.EXTRA_USE_CLOUD_EPG, true);
+ setResult(RESULT_OK, data);
+ finish();
+ }
+ }
+
+ /**
+ * Exports {@link SampleNetworkTunerSetupActivity} for Dagger codegen to create the appropriate
+ * injector.
+ */
+ @dagger.Module
+ public abstract static class Module {
+ @ContributesAndroidInjector
+ abstract SampleNetworkTunerSetupActivity
+ contributeSampleNetworkTunerSetupActivityInjector();
+ }
+
+ private class QueryEpgInputTask extends AsyncTask<Void, Void, EpgInput> {
+ private final String inputId;
+
+ QueryEpgInputTask(String inputId) {
+ this.inputId = inputId;
+ }
+
+ @Override
+ protected EpgInput doInBackground(Void... args) {
+ ContentResolver contentResolver = getContentResolver();
+ return EpgInputs.queryEpgInput(contentResolver, inputId);
+ }
+
+ @Override
+ protected void onPostExecute(EpgInput result) {
+ epgInput = result;
+ }
+ }
+}
diff --git a/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/singletons/SampleNetworkSingletons.java b/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/singletons/SampleNetworkSingletons.java
new file mode 100644
index 00000000..00a6e27a
--- /dev/null
+++ b/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/singletons/SampleNetworkSingletons.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.sample.network.singletons;
+
+import com.android.tv.common.BaseSingletons;
+import com.android.tv.tuner.singletons.TunerSingletons;
+
+/** Singletons for SampleNetworkTuner. */
+public interface SampleNetworkSingletons extends BaseSingletons, TunerSingletons {}
diff --git a/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/tvinput/SampleNetworkTunerTvInputService.java b/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/tvinput/SampleNetworkTunerTvInputService.java
new file mode 100644
index 00000000..de5ff229
--- /dev/null
+++ b/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/tvinput/SampleNetworkTunerTvInputService.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.tuner.sample.network.tvinput;
+
+import com.android.tv.tuner.tvinput.BaseTunerTvInputService;
+import dagger.android.ContributesAndroidInjector;
+
+/** Sample DVB Tuner {@link android.media.tv.TvInputService}. */
+public class SampleNetworkTunerTvInputService extends BaseTunerTvInputService {
+
+ /**
+ * Exports {@link SampleNetworkTunerTvInputService} for Dagger codegen to create the appropriate
+ * injector.
+ */
+ @dagger.Module
+ public abstract static class Module {
+ @ContributesAndroidInjector
+ abstract SampleNetworkTunerTvInputService
+ contributesSampleNetworkTunerTvInputServiceInjector();
+ }
+}
diff --git a/tuner/build.gradle b/tuner/build.gradle
new file mode 100644
index 00000000..0f40a29b
--- /dev/null
+++ b/tuner/build.gradle
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+/*
+ * Experimental gradle configuration. This file may not be up to date.
+ */
+
+apply plugin: 'com.android.library'
+apply plugin: 'com.google.protobuf'
+buildscript {
+ repositories {
+ mavenCentral()
+ google()
+ jcenter()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:3.1.4'
+ classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.5'
+ }
+}
+android {
+ compileSdkVersion 26
+ buildToolsVersion '28.0.2'
+
+ dexOptions {
+ preDexLibraries = false
+ additionalParameters = ['--core-library']
+ javaMaxHeapSize "6g"
+ }
+
+ android {
+ defaultConfig {
+ resConfigs "en"
+ }
+ }
+
+ defaultConfig {
+ minSdkVersion 23
+ targetSdkVersion 26
+ versionCode 1
+ versionName "1.0"
+ }
+ buildTypes {
+ debug {
+ minifyEnabled false
+ }
+ release {
+ minifyEnabled true
+ }
+ }
+ compileOptions() {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
+ sourceSets {
+ main {
+ res.srcDirs = ['res']
+ java.srcDirs = ['src']
+ manifest.srcFile 'AndroidManifest.xml'
+ proto {
+ srcDir 'proto/'
+ }
+ }
+ }
+}
+
+repositories {
+ mavenCentral()
+ google()
+ jcenter()
+}
+
+final String SUPPORT_LIBS_VERSION = '26.1.0'
+dependencies {
+ implementation 'com.google.android.exoplayer:exoplayer-core:2.9.0'
+ implementation 'com.google.android.exoplayer:exoplayer:r1.5.16'
+ implementation "com.android.support:support-tv-provider:${SUPPORT_LIBS_VERSION}"
+ implementation "com.android.support:appcompat-v7:${SUPPORT_LIBS_VERSION}"
+ implementation "com.android.support:leanback-v17:${SUPPORT_LIBS_VERSION}"
+ implementation 'com.google.guava:guava:26.0-android'
+ implementation 'com.google.protobuf.nano:protobuf-javanano:3.2.0rc2'
+ implementation project(':common')
+}
+protobuf {
+ // Configure the protoc executable
+ protoc {
+ artifact = 'com.google.protobuf:protoc:3.1.0'
+
+ generateProtoTasks {
+ all().each {
+ task -> task.builtins {
+ remove java
+ javanano {
+ option "enum_style=java"
+ }
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/tuner/buildconfig.mk b/tuner/buildconfig.mk
deleted file mode 100644
index cece7f2b..00000000
--- a/tuner/buildconfig.mk
+++ /dev/null
@@ -1,39 +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.
-#
-
-# Emulate gradles BuildConfig.java
-
-ifeq "$(TARGET_BUILD_VARIANT)" "eng"
- BC_DEBUG_STATUS := true
-else ifeq "$(TARGET_BUILD_VARIANT)" "userdebug"
- BC_DEBUG_STATUS := true
-else
- BC_DEBUG_STATUS := false
-endif
-
-ifeq "$(TARGET_BUILD_VARIANT)" "eng"
- BC_ENG_STATUS := true
-else
- BC_ENG_STATUS := false
-endif
-
-gen := $(local-generated-sources-dir)/$(TARGET_BUILD_VARIANT)/BuildConfig.java
-$(gen): PRIVATE_CUSTOM_TOOL = sed -e \
- 's/%DEBUG%/$(BC_DEBUG_STATUS)/;s/%ENG%/$(BC_ENG_STATUS)/' \
- $< > $@
-$(gen) : $(LOCAL_PATH)/BuildConfig.java.in
- $(transform-generated-source)
-LOCAL_GENERATED_SOURCES += $(gen) \ No newline at end of file
diff --git a/tuner/proto/Android.bp b/tuner/proto/Android.bp
new file mode 100644
index 00000000..67f35f82
--- /dev/null
+++ b/tuner/proto/Android.bp
@@ -0,0 +1,27 @@
+//
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+java_library {
+ name: "live-tv-tuner-proto",
+ srcs: ["*.proto"],
+ sdk_version: "system_current",
+ proto: {
+ type: "nano",
+ output_params: ["enum_style=java"],
+ canonical_path_from_root: false,
+ },
+ min_sdk_version: "23",
+}
diff --git a/tuner/proto/channel.proto b/tuner/proto/channel.proto
index 1f994522..ff372ad5 100644
--- a/tuner/proto/channel.proto
+++ b/tuner/proto/channel.proto
@@ -21,10 +21,15 @@ package com.android.tv.tuner.data;
option java_package = "com.android.tv.tuner.data";
option java_outer_classname = "Channel";
+
+// AOSP_Comment_Out import "third_party/android/nanoproto/nano_descriptor.proto";
+
import "track.proto";
// Holds information about a channel used in the tuners.
message TunerChannelProto {
+// AOSP_Comment_Out option (proto2.nano.message_as_lite) = false;
+
optional TunerType type = 1;
optional string short_name = 2;
optional string long_name = 3;
@@ -60,6 +65,8 @@ message TunerChannelProto {
// Enum describing the types of tuner.
enum TunerType {
+// AOSP_Comment_Out option (proto2.nano.enum_as_lite) = false;
+
TYPE_TUNER = 0;
TYPE_FILE = 1;
TYPE_NETWORK = 2;
@@ -67,6 +74,8 @@ enum TunerType {
// Enum describing the types of video stream.
enum VideoStreamType {
+// AOSP_Comment_Out option (proto2.nano.enum_as_lite) = false;
+
// ISO/IEC 11172 Video (MPEG-1)
MPEG1 = 0x01;
// ISO/IEC 13818-2 (MPEG-2) Video
@@ -81,6 +90,8 @@ enum VideoStreamType {
// Enum describing the types of audio stream.
enum AudioStreamType {
+// AOSP_Comment_Out option (proto2.nano.enum_as_lite) = false;
+
// ISO/IEC 11172 Audio (MPEG-1)
MPEG1AUDIO = 0x03;
// ISO/IEC 13818-3 Audio (MPEG-2)
@@ -98,6 +109,8 @@ enum AudioStreamType {
// Enum describing ATSC service types
// See ATSC Code Points Registry.
enum AtscServiceType {
+// AOSP_Comment_Out option (proto2.nano.enum_as_lite) = false;
+
SERVICE_TYPE_ATSC_RESERVED = 0x0;
SERVICE_TYPE_ANALOG_TELEVISION_CHANNELS = 0x1;
SERVICE_TYPE_ATSC_DIGITAL_TELEVISION = 0x2;
diff --git a/tuner/proto/track.proto b/tuner/proto/track.proto
index fe60fed5..11ca784d 100644
--- a/tuner/proto/track.proto
+++ b/tuner/proto/track.proto
@@ -18,11 +18,15 @@ syntax = "proto2";
package com.android.tv.tuner.data;
+// AOSP_Comment_Out import "third_party/android/nanoproto/nano_descriptor.proto";
+
option java_package = "com.android.tv.tuner.data";
option java_outer_classname = "Track";
// Represents a AC3 audio track.
message AtscAudioTrack {
+// AOSP_Comment_Out option (proto2.nano.message_as_lite) = false;
+
optional string language = 1;
optional AudioType audio_type = 2;
optional int32 index = 3;
@@ -32,6 +36,8 @@ message AtscAudioTrack {
// Enum describing the types of a audio track.
// See ISO/IEC 138181-1:2000(e) Table 2-53.
enum AudioType {
+// AOSP_Comment_Out option (proto2.nano.enum_as_lite) = false;
+
AUDIOTYPE_UNDEFINED = 0;
AUDIOTYPE_CLEAN_EFFECTS = 1;
AUDIOTYPE_HEARING_IMPAIRED = 2;
@@ -41,6 +47,8 @@ message AtscAudioTrack {
// Represents a CEA-708 caption track.
message AtscCaptionTrack {
+// AOSP_Comment_Out option (proto2.nano.message_as_lite) = false;
+
optional string language = 1;
optional int32 service_number = 2;
optional bool easy_reader = 3;
diff --git a/tuner/res/values/strings.xml b/tuner/res/values/strings.xml
index 58d7214c..96aca8a2 100644
--- a/tuner/res/values/strings.xml
+++ b/tuner/res/values/strings.xml
@@ -210,10 +210,40 @@
<!-- Title of postal/zip code input guided step fragment [CHAR LIMIT=30] -->
<string name="postal_code_guidance_title">Enter your ZIP Code.</string>
<!-- Description of postal/zip code input guided step fragment [CHAR LIMIT=NONE] -->
- <string name="postal_code_guidance_description">Live TV app will use the ZIP Code to provide a complete program guide for the TV channels.</string>
+ <string name="postal_code_guidance_description">
+ Please enter your ZIP code.
+ <xlgiff id="break">\n</xlgiff>
+ Your ZIP code is stored on device locally and sent to Google servers when updating your
+ program information. The ZIP code sent is never associated with your account or stored in
+ Google servers.
+ <xlgiff id="break">\n</xlgiff>
+ If you do not provide your ZIP code, your TV program guide will be limited.</string>
+ <!-- Description prefix of postal/zip code input guided step fragment when failed to get location automatically. [CHAR LIMIT=NONE] -->
+ <string name="postal_code_guidance_description_get_location_failed">
+ <xliff:g id="app_name">Live TV</xliff:g> cannot determine your location.</string>
<!-- Description of postal/zip code input edit text view to prompt users entering ZIP Code [CHAR LIMIT=30] -->
<string name="postal_code_action_description">Enter your ZIP Code</string>
<!-- Warning message shown in description field of postal/zip code input edit text view when user enters an invalid ZIP Code and presses Done [CHAR LIMIT=30] -->
<string name="postal_code_invalid_warning">Invalid ZIP Code</string>
+ <!-- Title of location rationale guided step fragment [CHAR LIMIT=30] -->
+ <string name="location_guidance_title">Location</string>
+ <!-- Description of location rationale guided step fragment [CHAR LIMIT=NONE] -->
+ <string name="location_guidance_description">
+ <xliff:g id="app_name">Live TV</xliff:g> uses your ZIP code to provide a complete
+ program guide for your TV channels.
+ <xlgiff id="break">\n</xlgiff>
+ Your ZIP code is stored on device locally and sent to Google servers when updating your
+ program information. The ZIP code sent is never associated with your account or stored in
+ Google servers.
+ <xlgiff id="break">\n</xlgiff>
+ If you do not grant permission or do not manually enter your ZIP code, your TV program guide
+ will be limited.</string>
+ <!-- Grant location permission -->
+ <string name="location_choices_allow_permission">Grant Permission</string>
+ <!-- Enter postal code -->
+ <string name="location_choices_enter_zip_code">Enter Zip Code</string>
+ <!-- Message to show users when getting location information -->
+ <string name="location_choices_getting_location">Getting Location</string>
+
</resources>
diff --git a/tuner/src/com/android/tv/tuner/DvbTunerHal.java b/tuner/src/com/android/tv/tuner/DvbTunerHal.java
index 4375fc32..c802ebbb 100644
--- a/tuner/src/com/android/tv/tuner/DvbTunerHal.java
+++ b/tuner/src/com/android/tv/tuner/DvbTunerHal.java
@@ -19,6 +19,7 @@ package com.android.tv.tuner;
import android.content.Context;
import android.os.ParcelFileDescriptor;
import android.util.Log;
+import com.android.tv.common.compat.TvInputConstantCompat;
import com.android.tv.tuner.DvbDeviceAccessor.DvbDeviceInfoWrapper;
import java.util.List;
import java.util.SortedSet;
@@ -26,13 +27,18 @@ import java.util.TreeSet;
/** A class to handle a hardware Linux DVB API supported tuner device. */
public class DvbTunerHal extends TunerHal {
+ private static final String TAG = "DvbTunerHal";
+ private static final boolean DEBUG = false;
private static final Object sLock = new Object();
// @GuardedBy("sLock")
private static final SortedSet<DvbDeviceInfoWrapper> sUsedDvbDevices = new TreeSet<>();
+ // The minimum delta for updating signal strength when valid
+ private static final int SIGNAL_STRENGTH_MINIMUM_DELTA = 2;
private final DvbDeviceAccessor mDvbDeviceAccessor;
private DvbDeviceInfoWrapper mDvbDeviceInfo;
+ private int mSignalStrength = TvInputConstantCompat.SIGNAL_STRENGTH_NOT_USED;
public DvbTunerHal(Context context) {
super(context);
@@ -40,7 +46,7 @@ public class DvbTunerHal extends TunerHal {
}
@Override
- protected boolean openFirstAvailable() {
+ public boolean openFirstAvailable() {
List<DvbDeviceInfoWrapper> deviceInfoList = mDvbDeviceAccessor.getDvbDeviceList();
if (deviceInfoList == null || deviceInfoList.isEmpty()) {
Log.e(TAG, "There's no dvb device attached");
@@ -115,12 +121,12 @@ public class DvbTunerHal extends TunerHal {
}
@Override
- protected boolean isDeviceOpen() {
+ public boolean isDeviceOpen() {
return (mDvbDeviceInfo != null);
}
@Override
- protected long getDeviceId() {
+ public long getDeviceId() {
if (mDvbDeviceInfo != null) {
return mDvbDeviceInfo.getId();
}
@@ -174,4 +180,45 @@ public class DvbTunerHal extends TunerHal {
return 0;
}
}
+
+ @Override
+ public int getSignalStrength() {
+ int signalStrength;
+ signalStrength = nativeGetSignalStrength(getDeviceId());
+ if (signalStrength == -3) {
+ mSignalStrength = signalStrength;
+ return TvInputConstantCompat.SIGNAL_STRENGTH_NOT_USED;
+ }
+ if (signalStrength > 65535 || signalStrength < 0) {
+ mSignalStrength = signalStrength;
+ return TvInputConstantCompat.SIGNAL_STRENGTH_ERROR;
+ }
+ signalStrength = getCurvedSignalStrength(signalStrength);
+ return updatingSignal(signalStrength);
+ }
+
+ /**
+ * This method curves the raw signal strength from tuner when it's between 0 - 65535 inclusive.
+ */
+ private int getCurvedSignalStrength(int signalStrength) {
+ /** When value < 80% of 65535, it will be recognized as level 0. */
+ if (signalStrength < 65535 * 0.8) {
+ return 0;
+ }
+ /** When value is between 80% to 100% of 65535, it will be linearly mapped to 0 - 100%. */
+ return (int) (5 * (signalStrength * 100.0 / 65535) - 400);
+ }
+
+ /**
+ * This method is for noise canceling. If the delta between current and previous strength is
+ * less than {@link #SIGNAL_STRENGTH_MINIMUM_DELTA}, previous signal strength will be returned.
+ * Otherwise current signal strength will be updated and returned.
+ */
+ private int updatingSignal(int signal) {
+ int delta = Math.abs(signal - mSignalStrength);
+ if (delta > SIGNAL_STRENGTH_MINIMUM_DELTA) {
+ mSignalStrength = signal;
+ }
+ return mSignalStrength;
+ }
}
diff --git a/tuner/src/com/android/tv/tuner/TunerFeatures.java b/tuner/src/com/android/tv/tuner/TunerFeatures.java
deleted file mode 100644
index e682e636..00000000
--- a/tuner/src/com/android/tv/tuner/TunerFeatures.java
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.tv.tuner;
-
-import static com.android.tv.common.feature.FeatureUtils.OFF;
-
-import android.content.Context;
-import android.text.TextUtils;
-import android.util.Log;
-import com.android.tv.common.BaseApplication;
-import com.android.tv.common.config.api.RemoteConfig;
-import com.android.tv.common.feature.CommonFeatures;
-import com.android.tv.common.feature.Feature;
-import com.android.tv.common.feature.Model;
-import com.android.tv.common.feature.PropertyFeature;
-import com.android.tv.common.util.CommonUtils;
-import com.android.tv.common.util.LocationUtils;
-import java.util.Locale;
-
-/**
- * List of {@link Feature} for Tuner.
- *
- * <p>Remove the {@code Feature} once it is launched.
- */
-public class TunerFeatures extends CommonFeatures {
- private static final String TAG = "TunerFeatures";
- private static final boolean DEBUG = false;
-
- /** 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 (CommonUtils.isDeveloper()) {
- // Network tuner will be enabled for developers.
- return true;
- }
- return Locale.US
- .getCountry()
- .equalsIgnoreCase(LocationUtils.getCurrentCountry(context));
- }
- };
-
- /**
- * USE_SW_CODEC_FOR_SD
- *
- * <p>Prefer software based codec for SD channels.
- */
- public static final Feature USE_SW_CODEC_FOR_SD =
- PropertyFeature.create(
- "use_sw_codec_for_sd",
- false
- );
-
- /** Use AC3 software decode. */
- public static final Feature AC3_SOFTWARE_DECODE =
- new Feature() {
- private final String[] SUPPORTED_REGIONS = {};
-
- private Boolean mEnabled;
-
- @Override
- public boolean isEnabled(Context context) {
- if (mEnabled == null) {
- if (mEnabled == null) {
- // We will not cache the result of fallback solution.
- String country = LocationUtils.getCurrentCountry(context);
- for (int i = 0; i < SUPPORTED_REGIONS.length; ++i) {
- if (SUPPORTED_REGIONS[i].equalsIgnoreCase(country)) {
- return true;
- }
- }
- if (DEBUG) Log.d(TAG, "AC3 flag false after country check");
- return false;
- }
- }
- if (DEBUG) Log.d(TAG, "AC3 flag " + mEnabled);
- return mEnabled;
- }
- };
-
- /** Enable Dvb parsers and listeners. */
- public static final Feature ENABLE_FILE_DVB = OFF;
-
- private TunerFeatures() {}
-}
diff --git a/tuner/src/com/android/tv/tuner/TunerHal.java b/tuner/src/com/android/tv/tuner/TunerHal.java
index 5801406b..dce4f4c4 100644
--- a/tuner/src/com/android/tv/tuner/TunerHal.java
+++ b/tuner/src/com/android/tv/tuner/TunerHal.java
@@ -17,87 +17,25 @@
package com.android.tv.tuner;
import android.content.Context;
-import android.support.annotation.IntDef;
-import android.support.annotation.StringDef;
-import android.support.annotation.WorkerThread;
import android.util.Log;
-import android.util.Pair;
import com.android.tv.common.BuildConfig;
-import com.android.tv.common.customization.CustomizationManager;
-
-
+import com.android.tv.common.compat.TvInputConstantCompat;
+import com.android.tv.tuner.api.Tuner;
import com.android.tv.common.annotation.UsedByNative;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
import java.util.Objects;
/** A base class to handle a hardware tuner device. */
-public abstract class TunerHal implements AutoCloseable {
- protected static final String TAG = "TunerHal";
- protected static final boolean DEBUG = false;
-
- @IntDef({FILTER_TYPE_OTHER, FILTER_TYPE_AUDIO, FILTER_TYPE_VIDEO, FILTER_TYPE_PCR})
- @Retention(RetentionPolicy.SOURCE)
- public @interface FilterType {}
-
- public static final int FILTER_TYPE_OTHER = 0;
- public static final int FILTER_TYPE_AUDIO = 1;
- public static final int FILTER_TYPE_VIDEO = 2;
- public static final int FILTER_TYPE_PCR = 3;
-
- @StringDef({MODULATION_8VSB, MODULATION_QAM256})
- @Retention(RetentionPolicy.SOURCE)
- public @interface ModulationType {}
-
- public static final String MODULATION_8VSB = "8VSB";
- public static final String MODULATION_QAM256 = "QAM256";
-
- @IntDef({
- DELIVERY_SYSTEM_UNDEFINED,
- DELIVERY_SYSTEM_ATSC,
- DELIVERY_SYSTEM_DVBC,
- DELIVERY_SYSTEM_DVBS,
- DELIVERY_SYSTEM_DVBS2,
- DELIVERY_SYSTEM_DVBT,
- DELIVERY_SYSTEM_DVBT2
- })
- @Retention(RetentionPolicy.SOURCE)
- public @interface DeliverySystemType {}
+public abstract class TunerHal implements Tuner {
+ private static final String TAG = "TunerHal";
- public static final int DELIVERY_SYSTEM_UNDEFINED = 0;
- public static final int DELIVERY_SYSTEM_ATSC = 1;
- public static final int DELIVERY_SYSTEM_DVBC = 2;
- public static final int DELIVERY_SYSTEM_DVBS = 3;
- public static final int DELIVERY_SYSTEM_DVBS2 = 4;
- public static final int DELIVERY_SYSTEM_DVBT = 5;
- public static final int DELIVERY_SYSTEM_DVBT2 = 6;
+ private static final int PID_PAT = 0;
+ private static final int PID_ATSC_SI_BASE = 0x1ffb;
+ private static final int PID_DVB_SDT = 0x0011;
+ private static final int PID_DVB_EIT = 0x0012;
+ private static final int DEFAULT_VSB_TUNE_TIMEOUT_MS = 2000;
+ private static final int DEFAULT_QAM_TUNE_TIMEOUT_MS = 4000; // Some device takes time for
- @IntDef({TUNER_TYPE_BUILT_IN, TUNER_TYPE_USB, TUNER_TYPE_NETWORK})
- @Retention(RetentionPolicy.SOURCE)
- public @interface TunerType {}
-
- public static final int TUNER_TYPE_BUILT_IN = 1;
- public static final int TUNER_TYPE_USB = 2;
- public static final int TUNER_TYPE_NETWORK = 3;
-
- protected static final int PID_PAT = 0;
- protected static final int PID_ATSC_SI_BASE = 0x1ffb;
- protected static final int PID_DVB_SDT = 0x0011;
- protected static final int PID_DVB_EIT = 0x0012;
- protected static final int DEFAULT_VSB_TUNE_TIMEOUT_MS = 2000;
- protected static final int DEFAULT_QAM_TUNE_TIMEOUT_MS = 4000; // Some device takes time for
- // QAM256 tuning.
- @IntDef({
- BUILT_IN_TUNER_TYPE_LINUX_DVB
- })
- @Retention(RetentionPolicy.SOURCE)
- private @interface BuiltInTunerType {}
-
- private static final int BUILT_IN_TUNER_TYPE_LINUX_DVB = 1;
-
- private static Integer sBuiltInTunerType;
-
- protected @DeliverySystemType int mDeliverySystemType;
+ @DeliverySystemType private int mDeliverySystemType;
private boolean mIsStreaming;
private int mFrequency;
private String mModulation;
@@ -108,66 +46,6 @@ public abstract class TunerHal implements AutoCloseable {
}
}
- /**
- * Creates a TunerHal instance.
- *
- * @param context context for creating the TunerHal instance
- * @return the TunerHal instance
- */
- @WorkerThread
- public static synchronized TunerHal createInstance(Context context) {
- TunerHal tunerHal = null;
- if (DvbTunerHal.getNumberOfDevices(context) > 0) {
- if (DEBUG) Log.d(TAG, "Use DvbTunerHal");
- tunerHal = new DvbTunerHal(context);
- }
- return tunerHal != null && tunerHal.openFirstAvailable() ? tunerHal : null;
- }
-
- /** Gets the number of tuner devices currently present. */
- @WorkerThread
- public static Pair<Integer, Integer> getTunerTypeAndCount(Context context) {
- if (useBuiltInTuner(context)) {
- if (getBuiltInTunerType(context) == BUILT_IN_TUNER_TYPE_LINUX_DVB) {
- return new Pair<>(TUNER_TYPE_BUILT_IN, DvbTunerHal.getNumberOfDevices(context));
- }
- } else {
- int usbTunerCount = DvbTunerHal.getNumberOfDevices(context);
- if (usbTunerCount > 0) {
- return new Pair<>(TUNER_TYPE_USB, usbTunerCount);
- }
- }
- return new Pair<>(null, 0);
- }
-
- /** Check a delivery system is for DVB or not. */
- public static boolean isDvbDeliverySystem(@DeliverySystemType int deliverySystemType) {
- return deliverySystemType == DELIVERY_SYSTEM_DVBC
- || deliverySystemType == DELIVERY_SYSTEM_DVBS
- || deliverySystemType == DELIVERY_SYSTEM_DVBS2
- || deliverySystemType == DELIVERY_SYSTEM_DVBT
- || deliverySystemType == DELIVERY_SYSTEM_DVBT2;
- }
-
- /**
- * Returns if tuner input service would use built-in tuners instead of USB tuners or network
- * tuners.
- */
- public static boolean useBuiltInTuner(Context context) {
- return getBuiltInTunerType(context) != 0;
- }
-
- private static @BuiltInTunerType int getBuiltInTunerType(Context context) {
- if (sBuiltInTunerType == null) {
- sBuiltInTunerType = 0;
- if (CustomizationManager.hasLinuxDvbBuiltInTuner(context)
- && DvbTunerHal.getNumberOfDevices(context) > 0) {
- sBuiltInTunerType = BUILT_IN_TUNER_TYPE_LINUX_DVB;
- }
- }
- return sBuiltInTunerType;
- }
-
protected TunerHal(Context context) {
mIsStreaming = false;
mFrequency = -1;
@@ -188,6 +66,7 @@ public abstract class TunerHal implements AutoCloseable {
* Returns {@code true} if this tuner HAL can be reused to save tuning time between channels of
* the same frequency.
*/
+ @Override
public boolean isReusable() {
return true;
}
@@ -201,18 +80,6 @@ public abstract class TunerHal implements AutoCloseable {
protected native void nativeFinalize(long deviceId);
/**
- * Acquires the first available tuner device. If there is a tuner device that is available, the
- * tuner device will be locked to the current instance.
- *
- * @return {@code true} if the operation was successful, {@code false} otherwise
- */
- protected abstract boolean openFirstAvailable();
-
- protected abstract boolean isDeviceOpen();
-
- protected abstract long getDeviceId();
-
- /**
* Sets the tuner channel. This should be called after acquiring a tuner device.
*
* @param frequency a frequency of the channel to tune to
@@ -221,6 +88,7 @@ public abstract class TunerHal implements AutoCloseable {
* use channelNumber instead of frequency for tune.
* @return {@code true} if the operation was successful, {@code false} otherwise
*/
+ @Override
public synchronized boolean tune(
int frequency, @ModulationType String modulation, String channelNumber) {
if (!isDeviceOpen()) {
@@ -237,7 +105,7 @@ public abstract class TunerHal implements AutoCloseable {
if (mFrequency == frequency && Objects.equals(mModulation, modulation)) {
addPidFilter(PID_PAT, FILTER_TYPE_OTHER);
addPidFilter(PID_ATSC_SI_BASE, FILTER_TYPE_OTHER);
- if (isDvbDeliverySystem(mDeliverySystemType)) {
+ if (Tuner.isDvbDeliverySystem(mDeliverySystemType)) {
addPidFilter(PID_DVB_SDT, FILTER_TYPE_OTHER);
addPidFilter(PID_DVB_EIT, FILTER_TYPE_OTHER);
}
@@ -251,7 +119,7 @@ public abstract class TunerHal implements AutoCloseable {
if (nativeTune(getDeviceId(), frequency, modulation, timeout_ms)) {
addPidFilter(PID_PAT, FILTER_TYPE_OTHER);
addPidFilter(PID_ATSC_SI_BASE, FILTER_TYPE_OTHER);
- if (isDvbDeliverySystem(mDeliverySystemType)) {
+ if (Tuner.isDvbDeliverySystem(mDeliverySystemType)) {
addPidFilter(PID_DVB_SDT, FILTER_TYPE_OTHER);
addPidFilter(PID_DVB_EIT, FILTER_TYPE_OTHER);
}
@@ -273,6 +141,7 @@ public abstract class TunerHal implements AutoCloseable {
* @param filterType a type of pid. Must be one of (FILTER_TYPE_XXX)
* @return {@code true} if the operation was successful, {@code false} otherwise
*/
+ @Override
public synchronized boolean addPidFilter(int pid, @FilterType int filterType) {
if (!isDeviceOpen()) {
Log.e(TAG, "There's no available device");
@@ -293,10 +162,13 @@ public abstract class TunerHal implements AutoCloseable {
protected native int nativeGetDeliverySystemType(long deviceId);
+ protected native int nativeGetSignalStrength(long deviceId);
+
/**
* Stops current tuning. The tuner device and pid filters will be reset by this call and make
* the tuner ready to accept another tune request.
*/
+ @Override
public synchronized void stopTune() {
if (isDeviceOpen()) {
if (mIsStreaming) {
@@ -309,10 +181,12 @@ public abstract class TunerHal implements AutoCloseable {
mModulation = null;
}
+ @Override
public void setHasPendingTune(boolean hasPendingTune) {
nativeSetHasPendingTune(getDeviceId(), hasPendingTune);
}
+ @Override
public int getDeliverySystemType() {
return mDeliverySystemType;
}
@@ -320,9 +194,9 @@ public abstract class TunerHal implements AutoCloseable {
protected native void nativeStopTune(long deviceId);
/**
- * This method must be called after {@link TunerHal#tune} and before {@link TunerHal#stopTune}.
- * Writes at most maxSize TS frames in a buffer provided by the user. The frames employ MPEG
- * encoding.
+ * This method must be called after {@link #tune(int, String, String)} and before {@link
+ * #stopTune()}. Writes at most maxSize TS frames in a buffer provided by the user. The frames
+ * employ MPEG encoding.
*
* @param javaBuffer a buffer to write the video data in
* @param javaBufferSize the max amount of bytes to write in this buffer. Usually this number
@@ -330,6 +204,7 @@ public abstract class TunerHal implements AutoCloseable {
* @return the amount of bytes written in the buffer. Note that this value could be 0 if no new
* frames have been obtained since the last call.
*/
+ @Override
public synchronized int readTsStream(byte[] javaBuffer, int javaBufferSize) {
if (isDeviceOpen()) {
return nativeWriteInBuffer(getDeviceId(), javaBuffer, javaBufferSize);
@@ -338,6 +213,21 @@ public abstract class TunerHal implements AutoCloseable {
}
}
+ /**
+ * This method gets signal strength for currently tuned channel.
+ * Each specific tuner should implement its own method.
+ *
+ * @return {@link TvInputConstantCompat#SIGNAL_STRENGTH_NOT_USED
+ * when signal check is not supported from tuner.
+ * {@link TvInputConstantCompat#SIGNAL_STRENGTH_ERROR}
+ * when signal returned is not valid.
+ * 0 - 100 representing strength from low to high. Curve raw data if necessary.
+ */
+ @Override
+ public int getSignalStrength() {
+ return TvInputConstantCompat.SIGNAL_STRENGTH_NOT_USED;
+ }
+
protected native int nativeWriteInBuffer(long deviceId, byte[] javaBuffer, int javaBufferSize);
/**
diff --git a/tuner/src/com/android/tv/tuner/api/ChannelScanListener.java b/tuner/src/com/android/tv/tuner/api/ChannelScanListener.java
new file mode 100644
index 00000000..e0319a27
--- /dev/null
+++ b/tuner/src/com/android/tv/tuner/api/ChannelScanListener.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.tuner.api;
+
+import com.android.tv.tuner.data.TunerChannel;
+
+/** Listener for detecting TV channels. */
+public interface ChannelScanListener {
+
+ /**
+ * Fired when new information of an TV channel arrives.
+ *
+ * @param channel an TV channel
+ * @param channelArrivedAtFirstTime tells whether this channel arrived at first time
+ */
+ void onChannelDetected(TunerChannel channel, boolean channelArrivedAtFirstTime);
+}
diff --git a/tuner/src/com/android/tv/tuner/api/ScanChannel.java b/tuner/src/com/android/tv/tuner/api/ScanChannel.java
new file mode 100644
index 00000000..56e5493c
--- /dev/null
+++ b/tuner/src/com/android/tv/tuner/api/ScanChannel.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.tuner.api;
+
+import com.android.tv.tuner.data.nano.Channel;
+
+/** Channel information gathered from a <em>scan</em> */
+public final class ScanChannel {
+ public final int type;
+ public final int frequency;
+ public final String modulation;
+ public final String filename;
+ /**
+ * Radio frequency (channel) number specified at
+ * https://en.wikipedia.org/wiki/North_American_television_frequencies This can be {@code null}
+ * for cases like cable signal.
+ */
+ public final Integer radioFrequencyNumber;
+
+ public static ScanChannel forTuner(
+ int frequency, String modulation, Integer radioFrequencyNumber) {
+ return new ScanChannel(
+ Channel.TunerType.TYPE_TUNER, frequency, modulation, null, radioFrequencyNumber);
+ }
+
+ public static ScanChannel forFile(int frequency, String filename) {
+ return new ScanChannel(Channel.TunerType.TYPE_FILE, frequency, "file:", filename, null);
+ }
+
+ private ScanChannel(
+ int type,
+ int frequency,
+ String modulation,
+ String filename,
+ Integer radioFrequencyNumber) {
+ this.type = type;
+ this.frequency = frequency;
+ this.modulation = modulation;
+ this.filename = filename;
+ this.radioFrequencyNumber = radioFrequencyNumber;
+ }
+}
diff --git a/tuner/src/com/android/tv/tuner/api/Tuner.java b/tuner/src/com/android/tv/tuner/api/Tuner.java
new file mode 100644
index 00000000..6f7e9d94
--- /dev/null
+++ b/tuner/src/com/android/tv/tuner/api/Tuner.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.tuner.api;
+
+import android.support.annotation.IntDef;
+import android.support.annotation.StringDef;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** A interface a hardware tuner device. */
+public interface Tuner extends AutoCloseable {
+
+ int FILTER_TYPE_OTHER = 0;
+ int FILTER_TYPE_AUDIO = 1;
+ int FILTER_TYPE_VIDEO = 2;
+ int FILTER_TYPE_PCR = 3;
+ String MODULATION_8VSB = "8VSB";
+ String MODULATION_QAM256 = "QAM256";
+ int DELIVERY_SYSTEM_UNDEFINED = 0;
+ int DELIVERY_SYSTEM_ATSC = 1;
+ int DELIVERY_SYSTEM_DVBC = 2;
+ int DELIVERY_SYSTEM_DVBS = 3;
+ int DELIVERY_SYSTEM_DVBS2 = 4;
+ int DELIVERY_SYSTEM_DVBT = 5;
+ int DELIVERY_SYSTEM_DVBT2 = 6;
+ int TUNER_TYPE_BUILT_IN = 1;
+ int TUNER_TYPE_USB = 2;
+ int TUNER_TYPE_NETWORK = 3;
+ int BUILT_IN_TUNER_TYPE_LINUX_DVB = 1;
+
+ /** Check a delivery system is for DVB or not. */
+ static boolean isDvbDeliverySystem(@DeliverySystemType int deliverySystemType) {
+ return deliverySystemType == DELIVERY_SYSTEM_DVBC
+ || deliverySystemType == DELIVERY_SYSTEM_DVBS
+ || deliverySystemType == DELIVERY_SYSTEM_DVBS2
+ || deliverySystemType == DELIVERY_SYSTEM_DVBT
+ || deliverySystemType == DELIVERY_SYSTEM_DVBT2;
+ }
+
+ boolean isReusable();
+
+ /**
+ * Acquires the first available tuner device. If there is a tuner device that is available, the
+ * tuner device will be locked to the current instance.
+ *
+ * @return {@code true} if the operation was successful, {@code false} otherwise
+ */
+ boolean openFirstAvailable();
+
+ boolean isDeviceOpen();
+
+ long getDeviceId();
+
+ boolean tune(int frequency, @ModulationType String modulation, String channelNumber);
+
+ boolean addPidFilter(int pid, @FilterType int filterType);
+
+ void stopTune();
+
+ void setHasPendingTune(boolean hasPendingTune);
+
+ int getDeliverySystemType();
+
+ int readTsStream(byte[] javaBuffer, int javaBufferSize);
+
+ int getSignalStrength();
+
+ /** Filter type */
+ @IntDef({FILTER_TYPE_OTHER, FILTER_TYPE_AUDIO, FILTER_TYPE_VIDEO, FILTER_TYPE_PCR})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface FilterType {}
+
+ /** Modulation Type */
+ @StringDef({MODULATION_8VSB, MODULATION_QAM256})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ModulationType {}
+
+ /** Delivery System Type */
+ @IntDef({
+ DELIVERY_SYSTEM_UNDEFINED,
+ DELIVERY_SYSTEM_ATSC,
+ DELIVERY_SYSTEM_DVBC,
+ DELIVERY_SYSTEM_DVBS,
+ DELIVERY_SYSTEM_DVBS2,
+ DELIVERY_SYSTEM_DVBT,
+ DELIVERY_SYSTEM_DVBT2
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface DeliverySystemType {}
+
+ /** Tuner Type */
+ @IntDef({TUNER_TYPE_BUILT_IN, TUNER_TYPE_USB, TUNER_TYPE_NETWORK})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface TunerType {}
+
+ /** Built in tuner type */
+ @IntDef({
+ BUILT_IN_TUNER_TYPE_LINUX_DVB
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface BuiltInTunerType {}
+}
diff --git a/tuner/src/com/android/tv/tuner/api/TunerFactory.java b/tuner/src/com/android/tv/tuner/api/TunerFactory.java
new file mode 100644
index 00000000..bc29c7c9
--- /dev/null
+++ b/tuner/src/com/android/tv/tuner/api/TunerFactory.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.api;
+
+import android.content.Context;
+import android.support.annotation.WorkerThread;
+import android.util.Pair;
+
+/** Factory for {@link Tuner}. */
+public interface TunerFactory {
+ @WorkerThread
+ Tuner createInstance(Context context);
+
+ boolean useBuiltInTuner(Context context);
+
+ @WorkerThread
+ Pair<Integer, Integer> getTunerTypeAndCount(Context context);
+}
diff --git a/tuner/src/com/android/tv/tuner/builtin/BuiltInTunerHalFactory.java b/tuner/src/com/android/tv/tuner/builtin/BuiltInTunerHalFactory.java
new file mode 100644
index 00000000..9a0be740
--- /dev/null
+++ b/tuner/src/com/android/tv/tuner/builtin/BuiltInTunerHalFactory.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.builtin;
+
+import android.content.Context;
+import android.support.annotation.WorkerThread;
+import android.util.Log;
+import android.util.Pair;
+import com.android.tv.common.customization.CustomizationManager;
+import com.android.tv.common.feature.Model;
+import com.android.tv.tuner.DvbTunerHal;
+import com.android.tv.tuner.api.Tuner;
+import com.android.tv.tuner.api.TunerFactory;
+
+
+/** TunerHal factory that creates all built in tuner types. */
+public final class BuiltInTunerHalFactory implements TunerFactory {
+ private static final String TAG = "BuiltInTunerHalFactory";
+ private static final boolean DEBUG = false;
+
+ private Integer mBuiltInTunerType;
+
+ public static final TunerFactory INSTANCE = new BuiltInTunerHalFactory();
+
+ private BuiltInTunerHalFactory() {}
+
+ @Tuner.BuiltInTunerType
+ private int getBuiltInTunerType(Context context) {
+ if (mBuiltInTunerType == null) {
+ mBuiltInTunerType = 0;
+ if (CustomizationManager.hasLinuxDvbBuiltInTuner(context)
+ && DvbTunerHal.getNumberOfDevices(context) > 0) {
+ mBuiltInTunerType = Tuner.BUILT_IN_TUNER_TYPE_LINUX_DVB;
+ }
+ }
+ return mBuiltInTunerType;
+ }
+
+ /**
+ * Creates a TunerHal instance.
+ *
+ * @param context context for creating the TunerHal instance
+ * @return the TunerHal instance
+ */
+ @Override
+ @WorkerThread
+ public synchronized Tuner createInstance(Context context) {
+ Tuner tunerHal = null;
+ if (DvbTunerHal.getNumberOfDevices(context) > 0) {
+ if (DEBUG) Log.d(TAG, "Use DvbTunerHal");
+ tunerHal = new DvbTunerHal(context);
+ }
+ return tunerHal != null && tunerHal.openFirstAvailable() ? tunerHal : null;
+ }
+
+ /**
+ * Returns if tuner input service would use built-in tuners instead of USB tuners or network
+ * tuners.
+ */
+ @Override
+ public boolean useBuiltInTuner(Context context) {
+ return getBuiltInTunerType(context) != 0;
+ }
+
+ /** Gets the number of tuner devices currently present. */
+ @Override
+ @WorkerThread
+ public Pair<Integer, Integer> getTunerTypeAndCount(Context context) {
+ if (useBuiltInTuner(context)) {
+ if (getBuiltInTunerType(context) == Tuner.BUILT_IN_TUNER_TYPE_LINUX_DVB) {
+ return new Pair<>(
+ Tuner.TUNER_TYPE_BUILT_IN, DvbTunerHal.getNumberOfDevices(context));
+ }
+ } else {
+ int usbTunerCount = DvbTunerHal.getNumberOfDevices(context);
+ if (usbTunerCount > 0) {
+ return new Pair<>(Tuner.TUNER_TYPE_USB, usbTunerCount);
+ }
+ }
+ return new Pair<>(null, 0);
+ }
+}
diff --git a/tuner/src/com/android/tv/tuner/cc/CaptionTrackRenderer.java b/tuner/src/com/android/tv/tuner/cc/CaptionTrackRenderer.java
index 84033240..4a1c7c1b 100644
--- a/tuner/src/com/android/tv/tuner/cc/CaptionTrackRenderer.java
+++ b/tuner/src/com/android/tv/tuner/cc/CaptionTrackRenderer.java
@@ -26,6 +26,7 @@ import com.android.tv.tuner.data.Cea708Data.CaptionPenColor;
import com.android.tv.tuner.data.Cea708Data.CaptionPenLocation;
import com.android.tv.tuner.data.Cea708Data.CaptionWindow;
import com.android.tv.tuner.data.Cea708Data.CaptionWindowAttr;
+import com.android.tv.tuner.data.Cea708Parser;
import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack;
import java.util.ArrayList;
diff --git a/tuner/src/com/android/tv/tuner/data/Cea708Data.java b/tuner/src/com/android/tv/tuner/data/Cea708Data.java
index 73a90181..bd1fc9b9 100644
--- a/tuner/src/com/android/tv/tuner/data/Cea708Data.java
+++ b/tuner/src/com/android/tv/tuner/data/Cea708Data.java
@@ -18,7 +18,6 @@ package com.android.tv.tuner.data;
import android.graphics.Color;
import android.support.annotation.NonNull;
-import com.android.tv.tuner.cc.Cea708Parser;
/** Collection of CEA-708 structures. */
public class Cea708Data {
diff --git a/tuner/src/com/android/tv/tuner/cc/Cea708Parser.java b/tuner/src/com/android/tv/tuner/data/Cea708Parser.java
index 4e080276..92834b27 100644
--- a/tuner/src/com/android/tv/tuner/cc/Cea708Parser.java
+++ b/tuner/src/com/android/tv/tuner/data/Cea708Parser.java
@@ -14,13 +14,12 @@
* limitations under the License.
*/
-package com.android.tv.tuner.cc;
+package com.android.tv.tuner.data;
import android.os.SystemClock;
import android.support.annotation.IntDef;
import android.util.Log;
import android.util.SparseIntArray;
-import com.android.tv.tuner.data.Cea708Data;
import com.android.tv.tuner.data.Cea708Data.CaptionColor;
import com.android.tv.tuner.data.Cea708Data.CaptionEvent;
import com.android.tv.tuner.data.Cea708Data.CaptionPenAttr;
diff --git a/tuner/src/com/android/tv/tuner/data/PsipData.java b/tuner/src/com/android/tv/tuner/data/PsipData.java
index 239009dc..d4af0934 100644
--- a/tuner/src/com/android/tv/tuner/data/PsipData.java
+++ b/tuner/src/com/android/tv/tuner/data/PsipData.java
@@ -22,7 +22,6 @@ import android.text.format.DateUtils;
import com.android.tv.common.util.StringUtils;
import com.android.tv.tuner.data.nano.Track.AtscAudioTrack;
import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack;
-import com.android.tv.tuner.ts.SectionParser;
import com.android.tv.tuner.util.ConvertUtils;
import java.util.ArrayList;
import java.util.HashMap;
diff --git a/tuner/src/com/android/tv/tuner/ts/SectionParser.java b/tuner/src/com/android/tv/tuner/data/SectionParser.java
index 27726c02..d3dba6ba 100644
--- a/tuner/src/com/android/tv/tuner/ts/SectionParser.java
+++ b/tuner/src/com/android/tv/tuner/data/SectionParser.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.tv.tuner.ts;
+package com.android.tv.tuner.data;
import android.media.tv.TvContentRating;
import android.media.tv.TvContract.Programs.Genres;
diff --git a/tuner/src/com/android/tv/tuner/exoplayer/Cea708TextTrackRenderer.java b/tuner/src/com/android/tv/tuner/exoplayer/Cea708TextTrackRenderer.java
index 1f48c45b..5c203305 100644
--- a/tuner/src/com/android/tv/tuner/exoplayer/Cea708TextTrackRenderer.java
+++ b/tuner/src/com/android/tv/tuner/exoplayer/Cea708TextTrackRenderer.java
@@ -17,8 +17,8 @@
package com.android.tv.tuner.exoplayer;
import android.util.Log;
-import com.android.tv.tuner.cc.Cea708Parser;
import com.android.tv.tuner.data.Cea708Data.CaptionEvent;
+import com.android.tv.tuner.data.Cea708Parser;
import com.google.android.exoplayer.ExoPlaybackException;
import com.google.android.exoplayer.MediaClock;
import com.google.android.exoplayer.MediaFormat;
diff --git a/tuner/src/com/android/tv/tuner/exoplayer/ExoPlayerSampleExtractor.java b/tuner/src/com/android/tv/tuner/exoplayer/ExoPlayerSampleExtractor.java
index e10a2991..e48cb03c 100644
--- a/tuner/src/com/android/tv/tuner/exoplayer/ExoPlayerSampleExtractor.java
+++ b/tuner/src/com/android/tv/tuner/exoplayer/ExoPlayerSampleExtractor.java
@@ -23,13 +23,14 @@ import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.os.SystemClock;
+import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.util.Pair;
import com.android.tv.tuner.exoplayer.audio.MpegTsDefaultAudioTrackRenderer;
import com.android.tv.tuner.exoplayer.buffer.BufferManager;
+import com.android.tv.tuner.exoplayer.buffer.PlaybackBufferListener;
import com.android.tv.tuner.exoplayer.buffer.RecordingSampleBuffer;
import com.android.tv.tuner.exoplayer.buffer.SimpleSampleBuffer;
-import com.android.tv.tuner.tvinput.PlaybackBufferListener;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.MediaFormatHolder;
import com.google.android.exoplayer.SampleHolder;
@@ -49,6 +50,8 @@ 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.google.android.exoplayer2.upstream.TransferListener;
+import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
@@ -69,6 +72,7 @@ public class ExoPlayerSampleExtractor implements SampleExtractor {
private final long mId;
private final Handler.Callback mSourceReaderWorker;
+ private final ConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags;
private BufferManager.SampleBuffer mSampleBuffer;
private Handler mSourceReaderHandler;
@@ -90,7 +94,8 @@ public class ExoPlayerSampleExtractor implements SampleExtractor {
final DataSource source,
BufferManager bufferManager,
PlaybackBufferListener bufferListener,
- boolean isRecording) {
+ boolean isRecording,
+ ConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlagsoncurrentDvrPlaybackFlags) {
this(
uri,
source,
@@ -98,10 +103,12 @@ public class ExoPlayerSampleExtractor implements SampleExtractor {
bufferListener,
isRecording,
Looper.myLooper(),
- new HandlerThread("SourceReaderThread"));
+ new HandlerThread("SourceReaderThread"),
+ concurrentDvrPlaybackFlagsoncurrentDvrPlaybackFlags);
}
@VisibleForTesting
+ @SuppressWarnings("MissingOverride")
public ExoPlayerSampleExtractor(
Uri uri,
DataSource source,
@@ -109,9 +116,11 @@ public class ExoPlayerSampleExtractor implements SampleExtractor {
PlaybackBufferListener bufferListener,
boolean isRecording,
Looper workerLooper,
- HandlerThread sourceReaderThread) {
+ HandlerThread sourceReaderThread,
+ ConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlags) {
// It'll be used as a timeshift file chunk name's prefix.
mId = System.currentTimeMillis();
+ mConcurrentDvrPlaybackFlags = concurrentDvrPlaybackFlags;
EventListener eventListener =
new EventListener() {
@@ -134,8 +143,19 @@ public class ExoPlayerSampleExtractor implements SampleExtractor {
// DataSource interface.
return new com.google.android.exoplayer2.upstream
.DataSource() {
+
+ private @Nullable Uri uri;
+
+ // TODO: uncomment once this is part of the public API.
+ // @Override
+ public void addTransferListener(
+ TransferListener transferListener) {
+ // Do nothing. Unsupported in V1.
+ }
+
@Override
public long open(DataSpec dataSpec) throws IOException {
+ this.uri = dataSpec.uri;
return source.open(
new com.google.android.exoplayer.upstream
.DataSpec(
@@ -156,13 +176,14 @@ public class ExoPlayerSampleExtractor implements SampleExtractor {
}
@Override
- public Uri getUri() {
- return null;
+ public @Nullable Uri getUri() {
+ return uri;
}
@Override
public void close() throws IOException {
source.close();
+ uri = null;
}
};
}
@@ -176,6 +197,7 @@ public class ExoPlayerSampleExtractor implements SampleExtractor {
bufferManager,
bufferListener,
false,
+ mConcurrentDvrPlaybackFlags,
RecordingSampleBuffer.BUFFER_REASON_RECORDING);
} else {
if (bufferManager == null) {
@@ -186,6 +208,7 @@ public class ExoPlayerSampleExtractor implements SampleExtractor {
bufferManager,
bufferListener,
true,
+ mConcurrentDvrPlaybackFlags,
RecordingSampleBuffer.BUFFER_REASON_LIVE_PLAYBACK);
}
}
@@ -204,6 +227,7 @@ public class ExoPlayerSampleExtractor implements SampleExtractor {
private static final int RETRY_INTERVAL_MS = 50;
private final MediaSource mSampleSource;
+ private final MediaSource.SourceInfoRefreshListener mSampleSourceListener;
private MediaPeriod mMediaPeriod;
private SampleStream[] mStreams;
private boolean[] mTrackMetEos;
@@ -215,17 +239,16 @@ public class ExoPlayerSampleExtractor implements SampleExtractor {
public SourceReaderWorker(MediaSource sampleSource) {
mSampleSource = sampleSource;
- mSampleSource.prepareSource(
- null,
- false,
- new MediaSource.Listener() {
+ mSampleSourceListener =
+ new MediaSource.SourceInfoRefreshListener() {
@Override
public void onSourceInfoRefreshed(
MediaSource source, Timeline timeline, Object manifest) {
// Dynamic stream change is not supported yet. b/28169263
// For now, this will cause EOS and playback reset.
}
- });
+ };
+ mSampleSource.prepareSource(null, false, mSampleSourceListener, null);
mDecoderInputBuffer =
new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL);
mSampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL);
@@ -283,11 +306,10 @@ public class ExoPlayerSampleExtractor implements SampleExtractor {
// 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);
+ selections[i] = new FixedTrackSelection(trackGroupArray.get(i), 0);
}
boolean[] retain = new boolean[trackGroupArray.length];
boolean[] reset = new boolean[trackGroupArray.length];
@@ -343,7 +365,9 @@ public class ExoPlayerSampleExtractor implements SampleExtractor {
mMediaPeriod =
mSampleSource.createPeriod(
new MediaSource.MediaPeriodId(0),
- new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE));
+ new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE)
+// AOSP_Comment_Out , 0
+ );
mMediaPeriod.prepare(this, 0);
try {
mMediaPeriod.maybeThrowPrepareError();
@@ -382,7 +406,7 @@ public class ExoPlayerSampleExtractor implements SampleExtractor {
case MSG_RELEASE:
if (mMediaPeriod != null) {
mSampleSource.releasePeriod(mMediaPeriod);
- mSampleSource.releaseSource();
+ mSampleSource.releaseSource(mSampleSourceListener);
mMediaPeriod = null;
}
cleanUp();
@@ -607,12 +631,7 @@ public class ExoPlayerSampleExtractor implements SampleExtractor {
final long lastExtractedPositionUs = getLastExtractedPositionUs();
if (mOnCompletionListenerHandler != null && mOnCompletionListener != null) {
mOnCompletionListenerHandler.post(
- new Runnable() {
- @Override
- public void run() {
- listener.onCompletion(result, lastExtractedPositionUs);
- }
- });
+ () -> listener.onCompletion(result, lastExtractedPositionUs));
}
}
}
diff --git a/tuner/src/com/android/tv/tuner/exoplayer/FileSampleExtractor.java b/tuner/src/com/android/tv/tuner/exoplayer/FileSampleExtractor.java
index e7224422..9749e4ba 100644
--- a/tuner/src/com/android/tv/tuner/exoplayer/FileSampleExtractor.java
+++ b/tuner/src/com/android/tv/tuner/exoplayer/FileSampleExtractor.java
@@ -18,12 +18,13 @@ package com.android.tv.tuner.exoplayer;
import android.os.Handler;
import com.android.tv.tuner.exoplayer.buffer.BufferManager;
+import com.android.tv.tuner.exoplayer.buffer.PlaybackBufferListener;
import com.android.tv.tuner.exoplayer.buffer.RecordingSampleBuffer;
-import com.android.tv.tuner.tvinput.PlaybackBufferListener;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.MediaFormatHolder;
import com.google.android.exoplayer.MediaFormatUtil;
import com.google.android.exoplayer.SampleHolder;
+import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
@@ -43,10 +44,15 @@ public class FileSampleExtractor implements SampleExtractor {
private final BufferManager mBufferManager;
private final PlaybackBufferListener mBufferListener;
private BufferManager.SampleBuffer mSampleBuffer;
+ private final ConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags;
- public FileSampleExtractor(BufferManager bufferManager, PlaybackBufferListener bufferListener) {
+ public FileSampleExtractor(
+ BufferManager bufferManager,
+ PlaybackBufferListener bufferListener,
+ ConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlags) {
mBufferManager = bufferManager;
mBufferListener = bufferListener;
+ mConcurrentDvrPlaybackFlags = concurrentDvrPlaybackFlags;
mTrackCount = -1;
}
@@ -74,6 +80,7 @@ public class FileSampleExtractor implements SampleExtractor {
mBufferManager,
mBufferListener,
true,
+ mConcurrentDvrPlaybackFlags,
RecordingSampleBuffer.BUFFER_REASON_RECORDED_PLAYBACK);
mSampleBuffer.init(ids, mTrackFormats);
return true;
diff --git a/tuner/src/com/android/tv/tuner/exoplayer/MpegTsPlayer.java b/tuner/src/com/android/tv/tuner/exoplayer/MpegTsPlayer.java
index a49cbfaf..6781c616 100644
--- a/tuner/src/com/android/tv/tuner/exoplayer/MpegTsPlayer.java
+++ b/tuner/src/com/android/tv/tuner/exoplayer/MpegTsPlayer.java
@@ -31,8 +31,8 @@ import com.android.tv.tuner.exoplayer.audio.MpegTsDefaultAudioTrackRenderer;
import com.android.tv.tuner.exoplayer.audio.MpegTsMediaCodecAudioTrackRenderer;
import com.android.tv.tuner.source.TsDataSource;
import com.android.tv.tuner.source.TsDataSourceManager;
-import com.android.tv.tuner.tvinput.EventDetector;
-import com.android.tv.tuner.tvinput.TunerDebug;
+import com.android.tv.tuner.ts.EventDetector.EventListener;
+import com.android.tv.tuner.tvinput.debug.TunerDebug;
import com.google.android.exoplayer.DummyTrackRenderer;
import com.google.android.exoplayer.ExoPlaybackException;
import com.google.android.exoplayer.ExoPlayer;
@@ -58,10 +58,7 @@ public class MpegTsPlayer
/** Interface definition for building specific track renderers. */
public interface RendererBuilder {
void buildRenderers(
- MpegTsPlayer mpegTsPlayer,
- DataSource dataSource,
- boolean hasSoftwareAudioDecoder,
- RendererBuilderCallback callback);
+ MpegTsPlayer mpegTsPlayer, DataSource dataSource, RendererBuilderCallback callback);
}
/** Interface definition for {@link RendererBuilder#buildRenderers} to notify the result. */
@@ -229,7 +226,7 @@ public class MpegTsPlayer
Context context,
TunerChannel channel,
boolean hasSoftwareAudioDecoder,
- EventDetector.EventListener eventListener) {
+ EventListener eventListener) {
TsDataSource source = null;
if (channel != null) {
source = mSourceManager.createDataSource(context, channel, eventListener);
@@ -246,7 +243,7 @@ public class MpegTsPlayer
}
mRendererBuildingState = RENDERER_BUILDING_STATE_BUILDING;
mBuilderCallback = new InternalRendererBuilderCallback();
- mRendererBuilder.buildRenderers(this, source, hasSoftwareAudioDecoder, mBuilderCallback);
+ mRendererBuilder.buildRenderers(this, source, mBuilderCallback);
return true;
}
diff --git a/tuner/src/com/android/tv/tuner/exoplayer/MpegTsRendererBuilder.java b/tuner/src/com/android/tv/tuner/exoplayer/MpegTsRendererBuilder.java
index 774285e9..e043907f 100644
--- a/tuner/src/com/android/tv/tuner/exoplayer/MpegTsRendererBuilder.java
+++ b/tuner/src/com/android/tv/tuner/exoplayer/MpegTsRendererBuilder.java
@@ -17,41 +17,48 @@
package com.android.tv.tuner.exoplayer;
import android.content.Context;
-import com.android.tv.tuner.TunerFeatures;
import com.android.tv.tuner.exoplayer.MpegTsPlayer.RendererBuilder;
import com.android.tv.tuner.exoplayer.MpegTsPlayer.RendererBuilderCallback;
import com.android.tv.tuner.exoplayer.audio.MpegTsDefaultAudioTrackRenderer;
import com.android.tv.tuner.exoplayer.buffer.BufferManager;
-import com.android.tv.tuner.tvinput.PlaybackBufferListener;
+import com.android.tv.tuner.exoplayer.buffer.PlaybackBufferListener;
import com.google.android.exoplayer.MediaCodecSelector;
import com.google.android.exoplayer.SampleSource;
import com.google.android.exoplayer.TrackRenderer;
import com.google.android.exoplayer.upstream.DataSource;
+import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags;
/** Builder for renderer objects for {@link MpegTsPlayer}. */
public class MpegTsRendererBuilder implements RendererBuilder {
private final Context mContext;
private final BufferManager mBufferManager;
private final PlaybackBufferListener mBufferListener;
+ private final ConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags;
public MpegTsRendererBuilder(
- Context context, BufferManager bufferManager, PlaybackBufferListener bufferListener) {
+ Context context,
+ BufferManager bufferManager,
+ PlaybackBufferListener bufferListener,
+ ConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlags) {
mContext = context;
mBufferManager = bufferManager;
mBufferListener = bufferListener;
+ mConcurrentDvrPlaybackFlags = concurrentDvrPlaybackFlags;
}
@Override
public void buildRenderers(
- MpegTsPlayer mpegTsPlayer,
- DataSource dataSource,
- boolean mHasSoftwareAudioDecoder,
- RendererBuilderCallback callback) {
+ MpegTsPlayer mpegTsPlayer, DataSource dataSource, RendererBuilderCallback callback) {
// Build the video and audio renderers.
SampleExtractor extractor =
dataSource == null
- ? new MpegTsSampleExtractor(mBufferManager, mBufferListener)
- : new MpegTsSampleExtractor(dataSource, mBufferManager, mBufferListener);
+ ? new MpegTsSampleExtractor(
+ mBufferManager, mBufferListener, mConcurrentDvrPlaybackFlags)
+ : new MpegTsSampleExtractor(
+ dataSource,
+ mBufferManager,
+ mBufferListener,
+ mConcurrentDvrPlaybackFlags);
SampleSource sampleSource = new MpegTsSampleSource(extractor);
MpegTsVideoTrackRenderer videoRenderer =
new MpegTsVideoTrackRenderer(
@@ -63,9 +70,7 @@ public class MpegTsRendererBuilder implements RendererBuilder {
sampleSource,
MediaCodecSelector.DEFAULT,
mpegTsPlayer.getMainHandler(),
- mpegTsPlayer,
- mHasSoftwareAudioDecoder,
- !TunerFeatures.AC3_SOFTWARE_DECODE.isEnabled(mContext));
+ mpegTsPlayer);
Cea708TextTrackRenderer textRenderer = new Cea708TextTrackRenderer(sampleSource);
TrackRenderer[] renderers = new TrackRenderer[MpegTsPlayer.RENDERER_COUNT];
diff --git a/tuner/src/com/android/tv/tuner/exoplayer/MpegTsSampleExtractor.java b/tuner/src/com/android/tv/tuner/exoplayer/MpegTsSampleExtractor.java
index 593b576e..582f18c5 100644
--- a/tuner/src/com/android/tv/tuner/exoplayer/MpegTsSampleExtractor.java
+++ b/tuner/src/com/android/tv/tuner/exoplayer/MpegTsSampleExtractor.java
@@ -19,14 +19,15 @@ package com.android.tv.tuner.exoplayer;
import android.net.Uri;
import android.os.Handler;
import com.android.tv.tuner.exoplayer.buffer.BufferManager;
+import com.android.tv.tuner.exoplayer.buffer.PlaybackBufferListener;
import com.android.tv.tuner.exoplayer.buffer.SamplePool;
-import com.android.tv.tuner.tvinput.PlaybackBufferListener;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.MediaFormatHolder;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.SampleSource;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.util.MimeTypes;
+import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
@@ -63,13 +64,22 @@ public final class MpegTsSampleExtractor implements SampleExtractor {
* @param source the {@link DataSource} to extract from
* @param bufferManager the manager for reading & writing samples backed by physical storage
* @param bufferListener the {@link PlaybackBufferListener} to notify buffer storage status
- * change
+ * @param concurrentDvrPlaybackFlags
*/
public MpegTsSampleExtractor(
- DataSource source, BufferManager bufferManager, PlaybackBufferListener bufferListener) {
+ DataSource source,
+ BufferManager bufferManager,
+ PlaybackBufferListener bufferListener,
+ ConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlags) {
+
mSampleExtractor =
new ExoPlayerSampleExtractor(
- Uri.EMPTY, source, bufferManager, bufferListener, false);
+ Uri.EMPTY,
+ source,
+ bufferManager,
+ bufferListener,
+ false,
+ concurrentDvrPlaybackFlags);
init();
}
@@ -81,8 +91,11 @@ public final class MpegTsSampleExtractor implements SampleExtractor {
* change
*/
public MpegTsSampleExtractor(
- BufferManager bufferManager, PlaybackBufferListener bufferListener) {
- mSampleExtractor = new FileSampleExtractor(bufferManager, bufferListener);
+ BufferManager bufferManager,
+ PlaybackBufferListener bufferListener,
+ ConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlags) {
+ mSampleExtractor =
+ new FileSampleExtractor(bufferManager, bufferListener, concurrentDvrPlaybackFlags);
init();
}
diff --git a/tuner/src/com/android/tv/tuner/exoplayer/MpegTsVideoTrackRenderer.java b/tuner/src/com/android/tv/tuner/exoplayer/MpegTsVideoTrackRenderer.java
index b136e235..c8a9c01b 100644
--- a/tuner/src/com/android/tv/tuner/exoplayer/MpegTsVideoTrackRenderer.java
+++ b/tuner/src/com/android/tv/tuner/exoplayer/MpegTsVideoTrackRenderer.java
@@ -19,7 +19,7 @@ import android.content.Context;
import android.media.MediaCodec;
import android.os.Handler;
import android.util.Log;
-import com.android.tv.tuner.TunerFeatures;
+import com.android.tv.tuner.features.TunerFeatures;
import com.google.android.exoplayer.DecoderInfo;
import com.google.android.exoplayer.ExoPlaybackException;
import com.google.android.exoplayer.MediaCodecSelector;
diff --git a/tuner/src/com/android/tv/tuner/exoplayer/audio/MpegTsDefaultAudioTrackRenderer.java b/tuner/src/com/android/tv/tuner/exoplayer/audio/MpegTsDefaultAudioTrackRenderer.java
index 944cfbcf..bab74c9d 100644
--- a/tuner/src/com/android/tv/tuner/exoplayer/audio/MpegTsDefaultAudioTrackRenderer.java
+++ b/tuner/src/com/android/tv/tuner/exoplayer/audio/MpegTsDefaultAudioTrackRenderer.java
@@ -21,7 +21,7 @@ import android.os.Build;
import android.os.Handler;
import android.os.SystemClock;
import android.util.Log;
-import com.android.tv.tuner.tvinput.TunerDebug;
+import com.android.tv.tuner.tvinput.debug.TunerDebug;
import com.google.android.exoplayer.CodecCounters;
import com.google.android.exoplayer.ExoPlaybackException;
import com.google.android.exoplayer.MediaClock;
@@ -106,8 +106,6 @@ public class MpegTsDefaultAudioTrackRenderer extends TrackRenderer implements Me
private final Handler mEventHandler;
private final AudioTrackMonitor mMonitor;
private final AudioClock mAudioClock;
- private final boolean mAc3Passthrough;
- private final boolean mSoftwareDecoderAvailable;
private MediaFormat mFormat;
private SampleHolder mSampleHolder;
@@ -137,9 +135,7 @@ public class MpegTsDefaultAudioTrackRenderer extends TrackRenderer implements Me
SampleSource source,
MediaCodecSelector selector,
Handler eventHandler,
- EventListener listener,
- boolean hasSoftwareAudioDecoder,
- boolean usePassthrough) {
+ EventListener listener) {
mSource = source.register();
mSelector = selector;
mEventHandler = eventHandler;
@@ -152,9 +148,6 @@ public class MpegTsDefaultAudioTrackRenderer extends TrackRenderer implements Me
mMonitor = new AudioTrackMonitor();
mAudioClock = new AudioClock();
mTracksIndex = new ArrayList<>();
- mAc3Passthrough = usePassthrough;
- // TODO reimplement ffmpeg decoder check for google3
- mSoftwareDecoderAvailable = false;
}
@Override
@@ -379,19 +372,6 @@ public class MpegTsDefaultAudioTrackRenderer extends TrackRenderer implements Me
}
}
- private MediaFormat convertMediaFormatToRaw(MediaFormat format) {
- return MediaFormat.createAudioFormat(
- format.trackId,
- MimeTypes.AUDIO_RAW,
- format.bitrate,
- format.maxInputSize,
- format.durationUs,
- format.channelCount,
- format.sampleRate,
- format.initializationData,
- format.language);
- }
-
private void onInputFormatChanged(MediaFormatHolder formatHolder) throws ExoPlaybackException {
String mimeType = formatHolder.format.mimeType;
mUseFrameworkDecoder = MediaCodecAudioDecoder.supportMimeType(mSelector, mimeType);
@@ -662,26 +642,14 @@ public class MpegTsDefaultAudioTrackRenderer extends TrackRenderer implements Me
if (mEventHandler == null || mEventListener == null) {
return;
}
- mEventHandler.post(
- new Runnable() {
- @Override
- public void run() {
- mEventListener.onAudioTrackInitializationError(e);
- }
- });
+ mEventHandler.post(() -> 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);
- }
- });
+ mEventHandler.post(() -> mEventListener.onAudioTrackWriteError(e));
}
@Override
diff --git a/tuner/src/com/android/tv/tuner/exoplayer/audio/MpegTsMediaCodecAudioTrackRenderer.java b/tuner/src/com/android/tv/tuner/exoplayer/audio/MpegTsMediaCodecAudioTrackRenderer.java
index b382545f..c655f779 100644
--- a/tuner/src/com/android/tv/tuner/exoplayer/audio/MpegTsMediaCodecAudioTrackRenderer.java
+++ b/tuner/src/com/android/tv/tuner/exoplayer/audio/MpegTsMediaCodecAudioTrackRenderer.java
@@ -69,13 +69,7 @@ public class MpegTsMediaCodecAudioTrackRenderer extends MediaCodecAudioTrackRend
private void notifyAudioTrackSetPlaybackParamsError(final IllegalArgumentException e) {
if (eventHandler != null && mListener != null) {
- eventHandler.post(
- new Runnable() {
- @Override
- public void run() {
- mListener.onAudioTrackSetPlaybackParamsError(e);
- }
- });
+ eventHandler.post(() -> mListener.onAudioTrackSetPlaybackParamsError(e));
}
}
diff --git a/tuner/src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java b/tuner/src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java
index 3e4ab103..c32540c1 100644
--- a/tuner/src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java
+++ b/tuner/src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java
@@ -284,6 +284,20 @@ public class BufferManager {
*/
void writeIndexFile(String trackName, SortedMap<Long, Pair<SampleChunk, Integer>> index)
throws IOException;
+
+ /**
+ * Writes to index file to storage.
+ *
+ * @param trackName track name
+ * @param size size of sample
+ * @param position position in micro seconds
+ * @param sampleChunk {@link SampleChunk} chunk to be added
+ * @param offset offset
+ * @throws IOException
+ */
+ void updateIndexFile(
+ String trackName, int size, long position, SampleChunk sampleChunk, int offset)
+ throws IOException;
}
private static class EvictChunkQueueMap {
@@ -368,7 +382,8 @@ public class BufferManager {
long positionUs,
SamplePool samplePool,
SampleChunk currentChunk,
- int currentOffset)
+ int currentOffset,
+ boolean updateIndexFile)
throws IOException {
if (!maybeEvictChunk()) {
throw new IOException("Not enough storage space");
@@ -386,9 +401,16 @@ public class BufferManager {
mSampleChunkCreator.createSampleChunk(
samplePool, file, positionUs, mChunkCallback);
map.put(positionUs, new Pair(sampleChunk, 0));
+ if (updateIndexFile) {
+ mStorageManager.updateIndexFile(id, map.size(), positionUs, sampleChunk, 0);
+ }
return sampleChunk;
} else {
map.put(positionUs, new Pair(currentChunk, currentOffset));
+ if (updateIndexFile) {
+ mStorageManager.updateIndexFile(
+ id, map.size(), positionUs, currentChunk, currentOffset);
+ }
return null;
}
}
@@ -587,6 +609,26 @@ public class BufferManager {
}
}
+ /**
+ * Writes track information for all tracks.
+ *
+ * @param audios list of audio track information
+ * @param videos list of audio track information
+ * @throws IOException
+ */
+ public void writeMetaFilesOnly(List<TrackFormat> audios, List<TrackFormat> videos)
+ throws IOException {
+ if (audios.isEmpty() && videos.isEmpty()) {
+ throw new IOException("No track information to save");
+ }
+ if (!audios.isEmpty()) {
+ mStorageManager.writeTrackInfoFiles(audios, true);
+ }
+ if (!videos.isEmpty()) {
+ mStorageManager.writeTrackInfoFiles(videos, false);
+ }
+ }
+
/** Releases all the resources. */
public void release() {
try {
diff --git a/tuner/src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java b/tuner/src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java
index 2a58ffcf..f19756ec 100644
--- a/tuner/src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java
+++ b/tuner/src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java
@@ -27,6 +27,7 @@ import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
+import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
@@ -388,4 +389,22 @@ public class DvrStorageManager implements BufferManager.StorageManager {
}
}
}
+
+ @Override
+ public void updateIndexFile(
+ String trackName, int size, long position, SampleChunk sampleChunk, int offset)
+ throws IOException {
+ File indexFile = new File(getBufferDir(), trackName + IDX_FILE_SUFFIX_V2);
+ if (!indexFile.exists()) {
+ indexFile.createNewFile();
+ }
+ RandomAccessFile accessFile = new RandomAccessFile(indexFile, "rw");
+ accessFile.seek(0);
+ accessFile.writeLong(size);
+ accessFile.seek(accessFile.length());
+ accessFile.writeLong(position);
+ accessFile.writeLong(sampleChunk.getStartPositionUs());
+ accessFile.writeInt(offset);
+ accessFile.close();
+ }
}
diff --git a/tuner/src/com/android/tv/tuner/tvinput/PlaybackBufferListener.java b/tuner/src/com/android/tv/tuner/exoplayer/buffer/PlaybackBufferListener.java
index 1628bcfb..046cfbe5 100644
--- a/tuner/src/com/android/tv/tuner/tvinput/PlaybackBufferListener.java
+++ b/tuner/src/com/android/tv/tuner/exoplayer/buffer/PlaybackBufferListener.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.tv.tuner.tvinput;
+package com.android.tv.tuner.exoplayer.buffer;
/** The listener for buffer events occurred during playback. */
public interface PlaybackBufferListener {
diff --git a/tuner/src/com/android/tv/tuner/exoplayer/buffer/RecordingSampleBuffer.java b/tuner/src/com/android/tv/tuner/exoplayer/buffer/RecordingSampleBuffer.java
index ebf00f59..d95642c2 100644
--- a/tuner/src/com/android/tv/tuner/exoplayer/buffer/RecordingSampleBuffer.java
+++ b/tuner/src/com/android/tv/tuner/exoplayer/buffer/RecordingSampleBuffer.java
@@ -22,12 +22,12 @@ import android.support.annotation.NonNull;
import android.util.Log;
import com.android.tv.tuner.exoplayer.MpegTsPlayer;
import com.android.tv.tuner.exoplayer.SampleExtractor;
-import com.android.tv.tuner.tvinput.PlaybackBufferListener;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.SampleSource;
import com.google.android.exoplayer.util.Assertions;
+import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -69,6 +69,7 @@ public class RecordingSampleBuffer
private final BufferManager mBufferManager;
private final PlaybackBufferListener mBufferListener;
private final @BufferReason int mBufferReason;
+ private final ConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags;
private int mTrackCount;
private boolean[] mTrackSelected;
@@ -103,15 +104,18 @@ public class RecordingSampleBuffer
* @param bufferManager the manager of {@link SampleChunk}
* @param bufferListener the listener for buffer I/O event
* @param enableTrickplay {@code true} when trickplay should be enabled
- * @param bufferReason the reason for caching samples {@link RecordingSampleBuffer.BufferReason}
+ * @param concurrentDvrPlaybackFlags
+ * @param bufferReason the reason for caching samples {@link BufferReason}
*/
public RecordingSampleBuffer(
BufferManager bufferManager,
PlaybackBufferListener bufferListener,
boolean enableTrickplay,
+ ConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlags,
@BufferReason int bufferReason) {
mBufferManager = bufferManager;
mBufferListener = bufferListener;
+ mConcurrentDvrPlaybackFlags = concurrentDvrPlaybackFlags;
if (bufferListener != null) {
bufferListener.onBufferStateChanged(enableTrickplay);
}
@@ -129,7 +133,13 @@ public class RecordingSampleBuffer
mReadSampleQueues = new ArrayList<>();
mSampleChunkIoHelper =
new SampleChunkIoHelper(
- ids, mediaFormats, mBufferReason, mBufferManager, mSamplePool, mIoCallback);
+ ids,
+ mediaFormats,
+ mBufferReason,
+ mBufferManager,
+ mSamplePool,
+ mIoCallback,
+ mConcurrentDvrPlaybackFlags);
for (int i = 0; i < mTrackCount; ++i) {
mReadSampleQueues.add(i, new SampleQueue(mSamplePool));
}
diff --git a/tuner/src/com/android/tv/tuner/exoplayer/buffer/SampleChunkIoHelper.java b/tuner/src/com/android/tv/tuner/exoplayer/buffer/SampleChunkIoHelper.java
index d95d0adb..f4d3bf8e 100644
--- a/tuner/src/com/android/tv/tuner/exoplayer/buffer/SampleChunkIoHelper.java
+++ b/tuner/src/com/android/tv/tuner/exoplayer/buffer/SampleChunkIoHelper.java
@@ -29,7 +29,9 @@ import com.android.tv.tuner.exoplayer.buffer.RecordingSampleBuffer.BufferReason;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.util.MimeTypes;
+import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags;
import java.io.IOException;
+import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
@@ -52,6 +54,7 @@ public class SampleChunkIoHelper implements Handler.Callback {
private static final int MSG_READ = 5;
private static final int MSG_WRITE = 6;
private static final int MSG_RELEASE = 7;
+ private static final int MSG_UPDATE_INDEX = 8;
private final long mSampleChunkDurationUs;
private final int mTrackCount;
@@ -61,6 +64,7 @@ public class SampleChunkIoHelper implements Handler.Callback {
private final BufferManager mBufferManager;
private final SamplePool mSamplePool;
private final IoCallback mIoCallback;
+ private final ConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags;
private Handler mIoHandler;
private final ConcurrentLinkedQueue<SampleHolder> mReadSampleBuffers[];
@@ -70,6 +74,8 @@ public class SampleChunkIoHelper implements Handler.Callback {
private final SampleChunk.IoState[] mReadIoStates;
private final SampleChunk.IoState[] mWriteIoStates;
private final Set<Integer> mSelectedTracks = new ArraySet<>();
+ private final long[] mReadChunkOffset;
+ private final long[] mReadChunkPositionUs;
private long mBufferDurationUs = 0;
private boolean mWriteEnded;
private boolean mErrorNotified;
@@ -115,6 +121,7 @@ public class SampleChunkIoHelper implements Handler.Callback {
* @param bufferManager manager of {@link SampleChunk} collections
* @param samplePool allocator for a sample
* @param ioCallback listeners for I/O events
+ * @param concurrentDvrPlaybackFlags
*/
public SampleChunkIoHelper(
List<String> ids,
@@ -122,7 +129,8 @@ public class SampleChunkIoHelper implements Handler.Callback {
@BufferReason int bufferReason,
BufferManager bufferManager,
SamplePool samplePool,
- IoCallback ioCallback) {
+ IoCallback ioCallback,
+ ConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlags) {
mTrackCount = ids.size();
mIds = ids;
mMediaFormats = mediaFormats;
@@ -130,11 +138,14 @@ public class SampleChunkIoHelper implements Handler.Callback {
mBufferManager = bufferManager;
mSamplePool = samplePool;
mIoCallback = ioCallback;
+ mConcurrentDvrPlaybackFlags = concurrentDvrPlaybackFlags;
mReadSampleBuffers = new ConcurrentLinkedQueue[mTrackCount];
mHandlerReadSampleBuffers = new ConcurrentLinkedQueue[mTrackCount];
mWriteIndexEndPositionUs = new long[mTrackCount];
mWriteChunkEndPositionUs = new long[mTrackCount];
+ mReadChunkOffset = new long[mTrackCount];
+ mReadChunkPositionUs = new long[mTrackCount];
mReadIoStates = new SampleChunk.IoState[mTrackCount];
mWriteIoStates = new SampleChunk.IoState[mTrackCount];
@@ -171,6 +182,29 @@ public class SampleChunkIoHelper implements Handler.Callback {
mIoHandler.sendMessage(mIoHandler.obtainMessage(MSG_OPEN_WRITE, i));
}
}
+
+ try {
+ if (mConcurrentDvrPlaybackFlags.enabled()
+ && mBufferReason == RecordingSampleBuffer.BUFFER_REASON_RECORDING
+ && mTrackCount > 0) {
+ // Saves meta information for recording.
+ List<BufferManager.TrackFormat> audios = new ArrayList<>(mTrackCount);
+ List<BufferManager.TrackFormat> videos = new ArrayList<>(mTrackCount);
+ for (int i = 0; i < mTrackCount; ++i) {
+ android.media.MediaFormat format =
+ mMediaFormats.get(i).getFrameworkMediaFormatV16();
+ format.setLong(android.media.MediaFormat.KEY_DURATION, mBufferDurationUs);
+ if (MimeTypes.isAudio(mMediaFormats.get(i).mimeType)) {
+ audios.add(new BufferManager.TrackFormat(mIds.get(i), format));
+ } else if (MimeTypes.isVideo(mMediaFormats.get(i).mimeType)) {
+ videos.add(new BufferManager.TrackFormat(mIds.get(i), format));
+ }
+ }
+ mBufferManager.writeMetaFilesOnly(audios, videos);
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Unable to write Meta files for DVR recording.", e);
+ }
}
/**
@@ -217,6 +251,18 @@ public class SampleChunkIoHelper implements Handler.Callback {
}
/**
+ * Update Index from the specified offset.
+ *
+ * @param index track index
+ * @param offset of the specified position
+ */
+ private void updateIndex(int index, long offset) {
+ IoParams params =
+ new IoParams(index, offset, null, null, null); // mReadSampleBuffers[index]);
+ mIoHandler.sendMessage(mIoHandler.obtainMessage(MSG_UPDATE_INDEX, params));
+ }
+
+ /**
* Closes read from the specified track.
*
* @param index track index
@@ -300,6 +346,9 @@ public class SampleChunkIoHelper implements Handler.Callback {
case MSG_RELEASE:
doRelease((ConditionVariable) message.obj);
return true;
+ case MSG_UPDATE_INDEX:
+ doUpdateIndex((IoParams) message.obj);
+ return true;
}
} catch (IOException e) {
mIoCallback.onIoError();
@@ -334,8 +383,15 @@ public class SampleChunkIoHelper implements Handler.Callback {
}
private void doOpenWrite(int index) throws IOException {
+ boolean updateIndexFile =
+ mConcurrentDvrPlaybackFlags.enabled()
+ && (mBufferReason == RecordingSampleBuffer.BUFFER_REASON_RECORDING)
+ && (MimeTypes.isVideo(mMediaFormats.get(index).mimeType)
+ || MimeTypes.isAudio(mMediaFormats.get(index).mimeType));
+
SampleChunk chunk =
- mBufferManager.createNewWriteFileIfNeeded(mIds.get(index), 0, mSamplePool, null, 0);
+ mBufferManager.createNewWriteFileIfNeeded(
+ mIds.get(index), 0, mSamplePool, null, 0, updateIndexFile);
mWriteIoStates[index].openWrite(chunk);
}
@@ -370,7 +426,16 @@ public class SampleChunkIoHelper implements Handler.Callback {
SampleHolder sample = mReadIoStates[index].read();
if (sample != null) {
mHandlerReadSampleBuffers[index].offer(sample);
+ if (mConcurrentDvrPlaybackFlags.enabled()) {
+ mReadChunkOffset[index] = mReadIoStates[index].getOffset();
+ mReadChunkPositionUs[index] = sample.timeUs;
+ }
} else {
+ if (mConcurrentDvrPlaybackFlags.enabled()
+ && mBufferReason == RecordingSampleBuffer.BUFFER_REASON_RECORDED_PLAYBACK) {
+ // Update Index, to load new Samples
+ updateIndex(index, mReadChunkOffset[index]);
+ }
// Read reached write but write is not finished yet --- wait a few moments to
// see if another sample is written.
mIoHandler.sendMessageDelayed(
@@ -379,6 +444,27 @@ public class SampleChunkIoHelper implements Handler.Callback {
}
}
+ public void doUpdateIndex(IoParams params) throws IOException {
+ int index = params.index;
+ mIoHandler.removeMessages(MSG_READ, index);
+ // Update Track from Storage to load new Samples
+ mBufferManager.loadTrackFromStorage(mIds.get(index), mSamplePool);
+ Pair<SampleChunk, Integer> readPosition =
+ mBufferManager.getReadFile(mIds.get(index), mReadChunkPositionUs[index]);
+ if (readPosition == null) {
+ String errorMessage =
+ "Chunk ID:"
+ + mIds.get(index)
+ + " pos:"
+ + mReadChunkPositionUs[index]
+ + "is not found";
+ SoftPreconditions.checkNotNull(readPosition, TAG, errorMessage);
+ throw new IOException(errorMessage);
+ }
+ mReadIoStates[index].openRead(readPosition.first, params.positionUs);
+ mIoHandler.sendMessage(mIoHandler.obtainMessage(MSG_READ, index));
+ }
+
private void doWrite(IoParams params) throws IOException {
try {
if (mWriteEnded) {
@@ -398,13 +484,22 @@ public class SampleChunkIoHelper implements Handler.Callback {
? null
: mWriteIoStates[params.index].getChunk();
int currentOffset = (int) mWriteIoStates[params.index].getOffset();
+ boolean updateIndexFile =
+ mConcurrentDvrPlaybackFlags.enabled()
+ && (mBufferReason
+ == RecordingSampleBuffer.BUFFER_REASON_RECORDING)
+ && (MimeTypes.isVideo(mMediaFormats.get(index).mimeType)
+ || MimeTypes.isAudio(
+ mMediaFormats.get(index).mimeType));
+
nextChunk =
mBufferManager.createNewWriteFileIfNeeded(
mIds.get(index),
mWriteIndexEndPositionUs[index],
mSamplePool,
currentChunk,
- currentOffset);
+ currentOffset,
+ updateIndexFile);
mWriteIndexEndPositionUs[index] =
((sample.timeUs / RecordingSampleBuffer.MIN_SEEK_DURATION_US) + 1)
* RecordingSampleBuffer.MIN_SEEK_DURATION_US;
diff --git a/tuner/src/com/android/tv/tuner/exoplayer/buffer/SimpleSampleBuffer.java b/tuner/src/com/android/tv/tuner/exoplayer/buffer/SimpleSampleBuffer.java
index 4c6260bf..843df7dc 100644
--- a/tuner/src/com/android/tv/tuner/exoplayer/buffer/SimpleSampleBuffer.java
+++ b/tuner/src/com/android/tv/tuner/exoplayer/buffer/SimpleSampleBuffer.java
@@ -20,7 +20,6 @@ import android.os.ConditionVariable;
import android.support.annotation.NonNull;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.tuner.exoplayer.SampleExtractor;
-import com.android.tv.tuner.tvinput.PlaybackBufferListener;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.SampleHolder;
diff --git a/tuner/src/com/android/tv/tuner/exoplayer/buffer/TrickplayStorageManager.java b/tuner/src/com/android/tv/tuner/exoplayer/buffer/TrickplayStorageManager.java
index b22b8af1..3721706d 100644
--- a/tuner/src/com/android/tv/tuner/exoplayer/buffer/TrickplayStorageManager.java
+++ b/tuner/src/com/android/tv/tuner/exoplayer/buffer/TrickplayStorageManager.java
@@ -142,4 +142,8 @@ public class TrickplayStorageManager implements BufferManager.StorageManager {
@Override
public void writeIndexFile(
String trackName, SortedMap<Long, Pair<SampleChunk, Integer>> index) {}
+
+ @Override
+ public void updateIndexFile(
+ String trackName, int size, long position, SampleChunk sampleChunk, int offset) {}
}
diff --git a/tuner/src/com/android/tv/tuner/exoplayer2/VideoRendererExoV2.java b/tuner/src/com/android/tv/tuner/exoplayer2/VideoRendererExoV2.java
new file mode 100644
index 00000000..12039002
--- /dev/null
+++ b/tuner/src/com/android/tv/tuner/exoplayer2/VideoRendererExoV2.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.tuner.exoplayer2;
+
+import android.content.Context;
+import android.os.Handler;
+import android.util.Log;
+import com.android.tv.tuner.features.TunerFeatures;
+import com.google.android.exoplayer2.ExoPlaybackException;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.mediacodec.MediaCodecInfo;
+import com.google.android.exoplayer2.mediacodec.MediaCodecSelector;
+import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.video.MediaCodecVideoRenderer;
+import com.google.android.exoplayer2.video.VideoRendererEventListener;
+import java.lang.reflect.Field;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Subclasses {@link MediaCodecVideoRenderer} to customize minor behaviors.
+ *
+ * <p>This class changes two behaviors from {@link MediaCodecVideoRenderer}:
+ *
+ * <ul>
+ * <li>Prefer software decoders for sub-HD streams.
+ * <li>Prevents the rendering of the first frame when audio can start playing before the first
+ * video key frame's presentation timestamp.
+ * </ul>
+ */
+public class VideoRendererExoV2 extends MediaCodecVideoRenderer {
+ private static final String TAG = "MpegTsVideoTrackRender";
+
+ private static final String SOFTWARE_DECODER_NAME_PREFIX = "OMX.google.";
+ private static final long ALLOWED_JOINING_TIME_MS = 5000;
+ private static final int DROPPED_FRAMES_NOTIFICATION_THRESHOLD = 10;
+ private static final int MIN_HD_HEIGHT = 720;
+ private static Field sRenderedFirstFrameField;
+
+ private final boolean mIsSwCodecEnabled;
+ private boolean mCodecIsSwPreferred;
+ private boolean mSetRenderedFirstFrame;
+
+ static {
+ // Remove the reflection below once b/31223646 is resolved.
+ try {
+ // TODO: Remove this workaround by using public notification mechanisms.
+ sRenderedFirstFrameField =
+ MediaCodecVideoRenderer.class.getDeclaredField("renderedFirstFrame");
+ sRenderedFirstFrameField.setAccessible(true);
+ } catch (NoSuchFieldException e) {
+ // Null-checking for {@code sRenderedFirstFrameField} will do the error handling.
+ }
+ }
+
+ /**
+ * Creates an instance.
+ *
+ * @param context A context.
+ * @param handler The handler to use when delivering events to {@code eventListener}. May be
+ * null if delivery of events is not required.
+ * @param listener The listener of events. May be null if delivery of events is not required.
+ */
+ public VideoRendererExoV2(
+ Context context, Handler handler, VideoRendererEventListener listener) {
+ super(
+ context,
+ MediaCodecSelector.DEFAULT,
+ ALLOWED_JOINING_TIME_MS,
+ handler,
+ listener,
+ DROPPED_FRAMES_NOTIFICATION_THRESHOLD);
+ mIsSwCodecEnabled = TunerFeatures.USE_SW_CODEC_FOR_SD.isEnabled(context);
+ }
+
+ @Override
+ protected List<MediaCodecInfo> getDecoderInfos(
+ MediaCodecSelector codecSelector, Format format, boolean requiresSecureDecoder)
+ throws DecoderQueryException {
+ List<MediaCodecInfo> decoderInfos =
+ super.getDecoderInfos(codecSelector, format, requiresSecureDecoder);
+ if (mIsSwCodecEnabled && mCodecIsSwPreferred) {
+ // If software decoders are preferred, we sort the returned list so that software
+ // decoders appear first.
+ Collections.sort(
+ decoderInfos,
+ (o1, o2) ->
+ // Negate the result to consider software decoders as lower in
+ // comparisons.
+ -Boolean.compare(
+ o1.name.startsWith(SOFTWARE_DECODER_NAME_PREFIX),
+ o2.name.startsWith(SOFTWARE_DECODER_NAME_PREFIX)));
+ }
+ return decoderInfos;
+ }
+
+ @Override
+ protected void onInputFormatChanged(Format format) throws ExoPlaybackException {
+ mCodecIsSwPreferred =
+ MimeTypes.VIDEO_MPEG2.equals(format.sampleMimeType)
+ && format.height < MIN_HD_HEIGHT;
+ super.onInputFormatChanged(format);
+ }
+
+ @Override
+ protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException {
+ super.onPositionReset(positionUs, joining);
+ // Disabling pre-rendering of the first frame in order to avoid a frozen picture when
+ // starting the playback. We do this only once, when the renderer is enabled at first, since
+ // we need to pre-render the frame in advance when we do trickplay backed by seeking.
+ if (!mSetRenderedFirstFrame) {
+ setRenderedFirstFrame(true);
+ mSetRenderedFirstFrame = true;
+ }
+ }
+
+ private void setRenderedFirstFrame(boolean renderedFirstFrame) {
+ if (sRenderedFirstFrameField != null) {
+ try {
+ sRenderedFirstFrameField.setBoolean(this, renderedFirstFrame);
+ } catch (IllegalAccessException e) {
+ Log.w(
+ TAG,
+ "renderedFirstFrame is not accessible. Playback may start with a frozen"
+ + " picture.");
+ }
+ }
+ }
+}
diff --git a/tuner/src/com/android/tv/tuner/features/TunerFeatures.java b/tuner/src/com/android/tv/tuner/features/TunerFeatures.java
new file mode 100644
index 00000000..6033a3a6
--- /dev/null
+++ b/tuner/src/com/android/tv/tuner/features/TunerFeatures.java
@@ -0,0 +1,59 @@
+/*
+ * 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.features;
+
+import static com.android.tv.common.feature.FeatureUtils.OFF;
+
+import com.android.tv.common.feature.CommonFeatures;
+import com.android.tv.common.feature.Feature;
+import com.android.tv.common.feature.Model;
+import com.android.tv.common.feature.PropertyFeature;
+import com.android.tv.common.feature.Sdk;
+
+/**
+ * List of {@link Feature} for Tuner.
+ *
+ * <p>Only for use in Tuners.
+ *
+ * <p>Remove the {@code Feature} once it is launched.
+ */
+public class TunerFeatures extends CommonFeatures {
+
+ /**
+ * USE_SW_CODEC_FOR_SD
+ *
+ * <p>Prefer software based codec for SD channels.
+ */
+ public static final Feature USE_SW_CODEC_FOR_SD =
+ PropertyFeature.create(
+ "use_sw_codec_for_sd",
+ false
+ );
+
+ /**
+ * Does the TvProvider on the installed device allow systems inserts to the programs table.
+ *
+ * <p>This is available in {@link Sdk#AT_LEAST_O} but vendors may choose to backport support to
+ * the TvProvider.
+ */
+ public static final Feature TVPROVIDER_ALLOWS_COLUMN_CREATION = Sdk.AT_LEAST_O;
+
+ /** Enable Dvb parsers and listeners. */
+ public static final Feature ENABLE_FILE_DVB = OFF;
+
+ private TunerFeatures() {}
+}
diff --git a/tuner/src/com/android/tv/tuner/layout/ScaledLayout.java b/tuner/src/com/android/tv/tuner/layout/ScaledLayout.java
index dd92b641..6d17be98 100644
--- a/tuner/src/com/android/tv/tuner/layout/ScaledLayout.java
+++ b/tuner/src/com/android/tv/tuner/layout/ScaledLayout.java
@@ -35,14 +35,11 @@ public class ScaledLayout extends ViewGroup {
private static final String TAG = "ScaledLayout";
private static final boolean DEBUG = false;
private static final Comparator<Rect> mRectTopLeftSorter =
- new Comparator<Rect>() {
- @Override
- public int compare(Rect lhs, Rect rhs) {
- if (lhs.top != rhs.top) {
- return lhs.top - rhs.top;
- } else {
- return lhs.left - rhs.left;
- }
+ (Rect lhs, Rect rhs) -> {
+ if (lhs.top != rhs.top) {
+ return lhs.top - rhs.top;
+ } else {
+ return lhs.left - rhs.left;
}
};
diff --git a/tuner/src/com/android/tv/tuner/livetuner/LiveTvTunerTvInputService.java b/tuner/src/com/android/tv/tuner/livetuner/LiveTvTunerTvInputService.java
index f741fdb0..92701db8 100644
--- a/tuner/src/com/android/tv/tuner/livetuner/LiveTvTunerTvInputService.java
+++ b/tuner/src/com/android/tv/tuner/livetuner/LiveTvTunerTvInputService.java
@@ -17,6 +17,18 @@
package com.android.tv.tuner.livetuner;
import com.android.tv.tuner.tvinput.BaseTunerTvInputService;
+import dagger.android.ContributesAndroidInjector;
/** Live TV embedded tuner. */
-public class LiveTvTunerTvInputService extends BaseTunerTvInputService {}
+public class LiveTvTunerTvInputService extends BaseTunerTvInputService {
+
+ /**
+ * Exports {@link LiveTvTunerTvInputService} for Dagger codegen to create the appropriate
+ * injector.
+ */
+ @dagger.Module
+ public abstract static class Module {
+ @ContributesAndroidInjector
+ abstract LiveTvTunerTvInputService contributesLiveTvTunerTvInputServiceInjector();
+ }
+}
diff --git a/tuner/src/com/android/tv/tuner/modules/TunerModule.java b/tuner/src/com/android/tv/tuner/modules/TunerModule.java
new file mode 100644
index 00000000..4843f383
--- /dev/null
+++ b/tuner/src/com/android/tv/tuner/modules/TunerModule.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.tuner.modules;
+
+import com.android.tv.tuner.source.TunerSourceModule;
+import dagger.Module;
+
+/** Dagger module for TV Tuners. */
+@Module(includes = {TunerSingletonsModule.class, TunerSourceModule.class})
+public class TunerModule {}
diff --git a/tuner/src/com/android/tv/tuner/modules/TunerSingletonsModule.java b/tuner/src/com/android/tv/tuner/modules/TunerSingletonsModule.java
new file mode 100644
index 00000000..b7fba8d2
--- /dev/null
+++ b/tuner/src/com/android/tv/tuner/modules/TunerSingletonsModule.java
@@ -0,0 +1,18 @@
+package com.android.tv.tuner.modules;
+
+import com.android.tv.tuner.singletons.TunerSingletons;
+import dagger.Module;
+
+/**
+ * Provides bindings for items provided by {@link TunerSingletons}.
+ *
+ * <p>Use this module to inject items directly instead of using {@code TunerSingletons}.
+ */
+@Module
+public class TunerSingletonsModule {
+ private final TunerSingletons mTunerSingletons;
+
+ public TunerSingletonsModule(TunerSingletons tunerSingletons) {
+ this.mTunerSingletons = tunerSingletons;
+ }
+}
diff --git a/tuner/src/com/android/tv/tuner/TunerPreferences.java b/tuner/src/com/android/tv/tuner/prefs/TunerPreferences.java
index 7b45b997..85e3a5ec 100644
--- a/tuner/src/com/android/tv/tuner/TunerPreferences.java
+++ b/tuner/src/com/android/tv/tuner/prefs/TunerPreferences.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.tv.tuner;
+package com.android.tv.tuner.prefs;
import android.content.Context;
import android.content.SharedPreferences;
diff --git a/tuner/src/com/android/tv/tuner/setup/BaseTunerSetupActivity.java b/tuner/src/com/android/tv/tuner/setup/BaseTunerSetupActivity.java
index 1be4e1c2..44f689bf 100644
--- a/tuner/src/com/android/tv/tuner/setup/BaseTunerSetupActivity.java
+++ b/tuner/src/com/android/tv/tuner/setup/BaseTunerSetupActivity.java
@@ -22,6 +22,7 @@ import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
+import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.graphics.Bitmap;
@@ -30,16 +31,13 @@ import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.MainThread;
-import android.support.annotation.NonNull;
import android.support.annotation.VisibleForTesting;
import android.support.annotation.WorkerThread;
import android.support.v4.app.NotificationCompat;
import android.text.TextUtils;
import android.util.Log;
import android.widget.Toast;
-import com.android.tv.common.BaseApplication;
import com.android.tv.common.SoftPreconditions;
-import com.android.tv.common.experiments.Experiments;
import com.android.tv.common.feature.CommonFeatures;
import com.android.tv.common.ui.setup.SetupActivity;
import com.android.tv.common.ui.setup.SetupFragment;
@@ -47,12 +45,14 @@ import com.android.tv.common.ui.setup.SetupMultiPaneFragment;
import com.android.tv.common.util.AutoCloseableUtils;
import com.android.tv.common.util.PostalCodeUtils;
import com.android.tv.tuner.R;
-import com.android.tv.tuner.TunerHal;
-import com.android.tv.tuner.TunerPreferences;
+import com.android.tv.tuner.api.Tuner;
+import com.android.tv.tuner.api.TunerFactory;
+import com.android.tv.tuner.prefs.TunerPreferences;
import java.util.concurrent.Executor;
+import javax.inject.Inject;
/** The base setup activity class for tuner. */
-public class BaseTunerSetupActivity extends SetupActivity {
+public abstract class BaseTunerSetupActivity extends SetupActivity {
private static final String TAG = "BaseTunerSetupActivity";
private static final boolean DEBUG = false;
@@ -86,27 +86,21 @@ public class BaseTunerSetupActivity extends SetupActivity {
protected String mPreviousPostalCode;
protected boolean mActivityStopped;
protected boolean mPendingShowInitialFragment;
+ @Inject protected TunerFactory mTunerFactory;
- private TunerHalFactory mTunerHalFactory;
+ private TunerHalCreator mTunerHalCreator;
@Override
protected void onCreate(Bundle savedInstanceState) {
if (DEBUG) {
Log.d(TAG, "onCreate");
}
+ super.onCreate(savedInstanceState);
mActivityStopped = false;
executeGetTunerTypeAndCountAsyncTask();
- mTunerHalFactory =
- new TunerHalFactory(getApplicationContext(), AsyncTask.THREAD_POOL_EXECUTOR);
- super.onCreate(savedInstanceState);
- // TODO: check {@link shouldShowRequestPermissionRationale}.
- if (checkSelfPermission(android.Manifest.permission.ACCESS_COARSE_LOCATION)
- != PackageManager.PERMISSION_GRANTED) {
- // No need to check the request result.
- requestPermissions(
- new String[] {android.Manifest.permission.ACCESS_COARSE_LOCATION},
- PERMISSIONS_REQUEST_ACCESS_COARSE_LOCATION);
- }
+ mTunerHalCreator =
+ new TunerHalCreator(
+ getApplicationContext(), AsyncTask.THREAD_POOL_EXECUTOR, mTunerFactory);
try {
// Updating postal code takes time, therefore we called it here for "warm-up".
mPreviousPostalCode = PostalCodeUtils.getLastPostalCode(this);
@@ -138,25 +132,6 @@ public class BaseTunerSetupActivity extends SetupActivity {
}
@Override
- public void onRequestPermissionsResult(
- int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
- if (requestCode == PERMISSIONS_REQUEST_ACCESS_COARSE_LOCATION) {
- if (grantResults.length > 0
- && grantResults[0] == PackageManager.PERMISSION_GRANTED
- && Experiments.CLOUD_EPG.get()) {
- try {
- // Updating postal code takes time, therefore we should update postal code
- // right after the permission is granted, so that the subsequent operations,
- // especially EPG fetcher, could get the newly updated postal code.
- PostalCodeUtils.updatePostalCode(this);
- } catch (Exception e) {
- // Do nothing
- }
- }
- }
- }
-
- @Override
protected Fragment onCreateInitialFragment() {
if (mTunerType != null) {
SetupFragment fragment = new WelcomeFragment();
@@ -184,10 +159,16 @@ public class BaseTunerSetupActivity extends SetupActivity {
break;
default:
String postalCode = PostalCodeUtils.getLastPostalCode(this);
- if (mNeedToShowPostalCodeFragment
- || (CommonFeatures.ENABLE_CLOUD_EPG_REGION.isEnabled(
+ boolean needLocation =
+ CommonFeatures.ENABLE_CLOUD_EPG_REGION.isEnabled(
getApplicationContext())
- && TextUtils.isEmpty(postalCode))) {
+ && TextUtils.isEmpty(postalCode);
+ if (needLocation
+ && checkSelfPermission(
+ android.Manifest.permission.ACCESS_COARSE_LOCATION)
+ != PackageManager.PERMISSION_GRANTED) {
+ showLocationFragment();
+ } else if (mNeedToShowPostalCodeFragment || needLocation) {
// 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.
@@ -199,6 +180,23 @@ public class BaseTunerSetupActivity extends SetupActivity {
break;
}
return true;
+ case LocationFragment.ACTION_CATEGORY:
+ switch (actionId) {
+ case LocationFragment.ACTION_ALLOW_PERMISSION:
+ String postalCode =
+ params == null
+ ? null
+ : params.getString(LocationFragment.KEY_POSTAL_CODE);
+ if (postalCode == null) {
+ showPostalCodeFragment();
+ } else {
+ showConnectionTypeFragment();
+ }
+ break;
+ default:
+ showConnectionTypeFragment();
+ }
+ return true;
case PostalCodeFragment.ACTION_CATEGORY:
switch (actionId) {
case SetupMultiPaneFragment.ACTION_DONE:
@@ -210,7 +208,7 @@ public class BaseTunerSetupActivity extends SetupActivity {
}
return true;
case ConnectionTypeFragment.ACTION_CATEGORY:
- if (mTunerHalFactory.getOrCreate() == null) {
+ if (mTunerHalCreator.getOrCreate() == null) {
finish();
Toast.makeText(
getApplicationContext(),
@@ -233,7 +231,7 @@ public class BaseTunerSetupActivity extends SetupActivity {
getFragmentManager().popBackStack();
return true;
case ScanFragment.ACTION_FINISH:
- mTunerHalFactory.clear();
+ mTunerHalCreator.clear();
showScanResultFragment();
return true;
default: // fall out
@@ -269,22 +267,36 @@ public class BaseTunerSetupActivity extends SetupActivity {
}
/** Gets the currently used tuner HAL. */
- TunerHal getTunerHal() {
- return mTunerHalFactory.getOrCreate();
+ Tuner getTunerHal() {
+ return mTunerHalCreator.getOrCreate();
}
/** Generates tuner HAL. */
void generateTunerHal() {
- mTunerHalFactory.generate();
+ mTunerHalCreator.generate();
}
/** Clears the currently used tuner HAL. */
protected void clearTunerHal() {
- mTunerHalFactory.clear();
+ mTunerHalCreator.clear();
+ }
+
+ protected void showLocationFragment() {
+ SetupFragment fragment = new LocationFragment();
+ fragment.setShortDistance(
+ SetupFragment.FRAGMENT_ENTER_TRANSITION | SetupFragment.FRAGMENT_RETURN_TRANSITION);
+ showFragment(fragment, true);
}
protected void showPostalCodeFragment() {
+ showPostalCodeFragment(null);
+ }
+
+ protected void showPostalCodeFragment(Bundle args) {
SetupFragment fragment = new PostalCodeFragment();
+ if (args != null) {
+ fragment.setArguments(args);
+ }
fragment.setShortDistance(
SetupFragment.FRAGMENT_ENTER_TRANSITION | SetupFragment.FRAGMENT_RETURN_TRANSITION);
showFragment(fragment, true);
@@ -320,25 +332,28 @@ public class BaseTunerSetupActivity extends SetupActivity {
/**
* A callback to be invoked when the TvInputService is enabled or disabled.
*
+ * @param tunerSetupIntent
* @param context a {@link Context} instance
* @param enabled {@code true} for the {@link TunerTvInputService} to be enabled; otherwise
* {@code false}
*/
- public static void onTvInputEnabled(Context context, boolean enabled, Integer tunerType) {
+ public static void onTvInputEnabled(
+ Context context, boolean enabled, Integer tunerType, Intent tunerSetupIntent) {
// Send a notification for tuner setup if there's no channels and the tuner TV input
// setup has been not done.
boolean channelScanDoneOnPreference = TunerPreferences.isScanDone(context);
int channelCountOnPreference = TunerPreferences.getScannedChannelCount(context);
if (enabled && !channelScanDoneOnPreference && channelCountOnPreference == 0) {
TunerPreferences.setShouldShowSetupActivity(context, true);
- sendNotification(context, tunerType);
+ sendNotification(context, tunerType, tunerSetupIntent);
} else {
TunerPreferences.setShouldShowSetupActivity(context, false);
cancelNotification(context);
}
}
- private static void sendNotification(Context context, Integer tunerType) {
+ private static void sendNotification(
+ Context context, Integer tunerType, Intent tunerSetupIntent) {
SoftPreconditions.checkState(
tunerType != null, TAG, "tunerType is null when send notification");
if (tunerType == null) {
@@ -348,29 +363,29 @@ public class BaseTunerSetupActivity extends SetupActivity {
String contentTitle = resources.getString(R.string.ut_setup_notification_content_title);
int contentTextId = 0;
switch (tunerType) {
- case TunerHal.TUNER_TYPE_BUILT_IN:
+ case Tuner.TUNER_TYPE_BUILT_IN:
contentTextId = R.string.bt_setup_notification_content_text;
break;
- case TunerHal.TUNER_TYPE_USB:
+ case Tuner.TUNER_TYPE_USB:
contentTextId = R.string.ut_setup_notification_content_text;
break;
- case TunerHal.TUNER_TYPE_NETWORK:
+ case Tuner.TUNER_TYPE_NETWORK:
contentTextId = R.string.nt_setup_notification_content_text;
break;
default: // fall out
}
String contentText = resources.getString(contentTextId);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- sendNotificationInternal(context, contentTitle, contentText);
+ sendNotificationInternal(context, contentTitle, contentText, tunerSetupIntent);
} else {
Bitmap largeIcon =
BitmapFactory.decodeResource(resources, R.drawable.recommendation_antenna);
- sendRecommendationCard(context, contentTitle, contentText, largeIcon);
+ sendRecommendationCard(context, contentTitle, contentText, largeIcon, tunerSetupIntent);
}
}
private static void sendNotificationInternal(
- Context context, String contentTitle, String contentText) {
+ Context context, String contentTitle, String contentText, Intent tunerSetupIntent) {
NotificationManager notificationManager =
(NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.createNotificationChannel(
@@ -387,7 +402,8 @@ public class BaseTunerSetupActivity extends SetupActivity {
context.getResources()
.getIdentifier(
TAG_ICON, TAG_DRAWABLE, context.getPackageName()))
- .setContentIntent(createPendingIntentForSetupActivity(context))
+ .setContentIntent(
+ createPendingIntentForSetupActivity(context, tunerSetupIntent))
.setVisibility(Notification.VISIBILITY_PUBLIC)
.extend(new Notification.TvExtender())
.build();
@@ -397,10 +413,15 @@ public class BaseTunerSetupActivity extends SetupActivity {
/**
* Sends the recommendation card to start the tuner TV input setup activity.
*
+ * @param tunerSetupIntent
* @param context a {@link Context} instance
*/
private static void sendRecommendationCard(
- Context context, String contentTitle, String contentText, Bitmap largeIcon) {
+ Context context,
+ String contentTitle,
+ String contentText,
+ Bitmap largeIcon,
+ Intent tunerSetupIntent) {
// Build and send the notification.
Notification notification =
new NotificationCompat.BigPictureStyle(
@@ -418,7 +439,8 @@ public class BaseTunerSetupActivity extends SetupActivity {
TAG_DRAWABLE,
context.getPackageName()))
.setContentIntent(
- createPendingIntentForSetupActivity(context)))
+ createPendingIntentForSetupActivity(
+ context, tunerSetupIntent)))
.build();
NotificationManager notificationManager =
(NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
@@ -429,30 +451,27 @@ public class BaseTunerSetupActivity extends SetupActivity {
* Returns a {@link PendingIntent} to launch the tuner TV input service.
*
* @param context a {@link Context} instance
+ * @param tunerSetupIntent
*/
- private static PendingIntent createPendingIntentForSetupActivity(Context context) {
+ private static PendingIntent createPendingIntentForSetupActivity(
+ Context context, Intent tunerSetupIntent) {
return PendingIntent.getActivity(
- context,
- 0,
- BaseApplication.getSingletons(context).getTunerSetupIntent(context),
- PendingIntent.FLAG_UPDATE_CURRENT);
+ context, 0, tunerSetupIntent, PendingIntent.FLAG_UPDATE_CURRENT);
}
- /** A static factory for {@link TunerHal} instances * */
+ /** Creates {@link Tuner} instances in a worker thread * */
@VisibleForTesting
- protected static class TunerHalFactory {
+ protected static class TunerHalCreator {
private Context mContext;
- @VisibleForTesting TunerHal mTunerHal;
- private TunerHalFactory.GenerateTunerHalTask mGenerateTunerHalTask;
+ @VisibleForTesting Tuner mTunerHal;
+ private TunerHalCreator.GenerateTunerHalTask mGenerateTunerHalTask;
private final Executor mExecutor;
+ private final TunerFactory mTunerFactory;
- TunerHalFactory(Context context) {
- this(context, AsyncTask.SERIAL_EXECUTOR);
- }
-
- TunerHalFactory(Context context, Executor executor) {
+ TunerHalCreator(Context context, Executor executor, TunerFactory tunerFactory) {
mContext = context;
mExecutor = executor;
+ mTunerFactory = tunerFactory;
}
/**
@@ -460,7 +479,7 @@ public class BaseTunerSetupActivity extends SetupActivity {
* before, tries to generate it synchronously.
*/
@WorkerThread
- TunerHal getOrCreate() {
+ Tuner getOrCreate() {
if (mGenerateTunerHalTask != null
&& mGenerateTunerHalTask.getStatus() != AsyncTask.Status.FINISHED) {
try {
@@ -478,7 +497,7 @@ public class BaseTunerSetupActivity extends SetupActivity {
@MainThread
void generate() {
if (mGenerateTunerHalTask == null && mTunerHal == null) {
- mGenerateTunerHalTask = new TunerHalFactory.GenerateTunerHalTask();
+ mGenerateTunerHalTask = new TunerHalCreator.GenerateTunerHalTask();
mGenerateTunerHalTask.executeOnExecutor(mExecutor);
}
}
@@ -497,18 +516,18 @@ public class BaseTunerSetupActivity extends SetupActivity {
}
@WorkerThread
- protected TunerHal createInstance() {
- return TunerHal.createInstance(mContext);
+ protected Tuner createInstance() {
+ return mTunerFactory.createInstance(mContext);
}
- class GenerateTunerHalTask extends AsyncTask<Void, Void, TunerHal> {
+ class GenerateTunerHalTask extends AsyncTask<Void, Void, Tuner> {
@Override
- protected TunerHal doInBackground(Void... args) {
+ protected Tuner doInBackground(Void... args) {
return createInstance();
}
@Override
- protected void onPostExecute(TunerHal tunerHal) {
+ protected void onPostExecute(Tuner tunerHal) {
mTunerHal = tunerHal;
}
}
diff --git a/tuner/src/com/android/tv/tuner/ChannelScanFileParser.java b/tuner/src/com/android/tv/tuner/setup/ChannelScanFileParser.java
index d2ed6c38..43c584ed 100644
--- a/tuner/src/com/android/tv/tuner/ChannelScanFileParser.java
+++ b/tuner/src/com/android/tv/tuner/setup/ChannelScanFileParser.java
@@ -14,10 +14,10 @@
* limitations under the License.
*/
-package com.android.tv.tuner;
+package com.android.tv.tuner.setup;
import android.util.Log;
-import com.android.tv.tuner.data.nano.Channel;
+import com.android.tv.tuner.api.ScanChannel;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
@@ -26,49 +26,9 @@ import java.util.ArrayList;
import java.util.List;
/** Parses plain text formatted scan files, which contain the list of channels. */
-public class ChannelScanFileParser {
+public final class ChannelScanFileParser {
private static final String TAG = "ChannelScanFileParser";
- public static final class ScanChannel {
- public final int type;
- public final int frequency;
- public final String modulation;
- public final String filename;
- /**
- * Radio frequency (channel) number specified at
- * https://en.wikipedia.org/wiki/North_American_television_frequencies This can be {@code
- * null} for cases like cable signal.
- */
- public final Integer radioFrequencyNumber;
-
- public static ScanChannel forTuner(
- int frequency, String modulation, Integer radioFrequencyNumber) {
- return new ScanChannel(
- Channel.TunerType.TYPE_TUNER,
- frequency,
- modulation,
- null,
- radioFrequencyNumber);
- }
-
- public static ScanChannel forFile(int frequency, String filename) {
- return new ScanChannel(Channel.TunerType.TYPE_FILE, frequency, "file:", filename, null);
- }
-
- private ScanChannel(
- int type,
- int frequency,
- String modulation,
- String filename,
- Integer radioFrequencyNumber) {
- this.type = type;
- this.frequency = frequency;
- this.modulation = modulation;
- this.filename = filename;
- this.radioFrequencyNumber = radioFrequencyNumber;
- }
- }
-
/**
* Parses a given scan file and returns the list of {@link ScanChannel} objects.
*
@@ -105,4 +65,6 @@ public class ChannelScanFileParser {
}
return scanChannelList;
}
+
+ private ChannelScanFileParser(){}
}
diff --git a/tuner/src/com/android/tv/tuner/setup/LiveTvTunerSetupActivity.java b/tuner/src/com/android/tv/tuner/setup/LiveTvTunerSetupActivity.java
index 722de7c6..741edc78 100644
--- a/tuner/src/com/android/tv/tuner/setup/LiveTvTunerSetupActivity.java
+++ b/tuner/src/com/android/tv/tuner/setup/LiveTvTunerSetupActivity.java
@@ -17,20 +17,37 @@
package com.android.tv.tuner.setup;
import android.app.FragmentManager;
+import android.content.pm.PackageManager;
import android.os.AsyncTask;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
import android.view.KeyEvent;
-import com.android.tv.tuner.TunerHal;
+import com.android.tv.common.util.PostalCodeUtils;
+import dagger.android.ContributesAndroidInjector;
/** An activity that serves tuner setup process. */
public class LiveTvTunerSetupActivity extends BaseTunerSetupActivity {
private static final String TAG = "LiveTvTunerSetupActivity";
@Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // TODO(shubang): use LocationFragment
+ if (checkSelfPermission(android.Manifest.permission.ACCESS_COARSE_LOCATION)
+ != PackageManager.PERMISSION_GRANTED) {
+ // No need to check the request result.
+ requestPermissions(
+ new String[] {android.Manifest.permission.ACCESS_COARSE_LOCATION},
+ PERMISSIONS_REQUEST_ACCESS_COARSE_LOCATION);
+ }
+ }
+
+ @Override
protected void executeGetTunerTypeAndCountAsyncTask() {
new AsyncTask<Void, Void, Integer>() {
@Override
protected Integer doInBackground(Void... arg0) {
- return TunerHal.getTunerTypeAndCount(LiveTvTunerSetupActivity.this).first;
+ return mTunerFactory.getTunerTypeAndCount(LiveTvTunerSetupActivity.this).first;
}
@Override
@@ -72,4 +89,31 @@ public class LiveTvTunerSetupActivity extends BaseTunerSetupActivity {
}
return super.onKeyUp(keyCode, event);
}
+
+ @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) {
+ 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
+ }
+ }
+ }
+ }
+
+ /**
+ * Exports {@link LiveTvTunerSetupActivity} for Dagger codegen to create the appropriate
+ * injector.
+ */
+ @dagger.Module
+ public abstract static class Module {
+ @ContributesAndroidInjector
+ abstract LiveTvTunerSetupActivity contributeLiveTvTunerSetupActivityInjector();
+ }
}
diff --git a/tuner/src/com/android/tv/tuner/setup/LocationFragment.java b/tuner/src/com/android/tv/tuner/setup/LocationFragment.java
new file mode 100644
index 00000000..1234ae20
--- /dev/null
+++ b/tuner/src/com/android/tv/tuner/setup/LocationFragment.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.setup;
+
+import static com.android.tv.tuner.setup.BaseTunerSetupActivity.PERMISSIONS_REQUEST_ACCESS_COARSE_LOCATION;
+
+import android.content.pm.PackageManager;
+import android.location.Address;
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.annotation.NonNull;
+import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
+import android.support.v17.leanback.widget.GuidedAction;
+import android.util.Log;
+
+import com.android.tv.common.ui.setup.SetupActionHelper;
+import com.android.tv.common.ui.setup.SetupGuidedStepFragment;
+import com.android.tv.common.ui.setup.SetupMultiPaneFragment;
+import com.android.tv.common.util.LocationUtils;
+import com.android.tv.tuner.R;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/** A fragment shows the rationale of location permission */
+public class LocationFragment extends SetupMultiPaneFragment {
+ private static final String TAG = "com.android.tv.tuner.setup.LocationFragment";
+ private static final boolean DEBUG = true;
+
+ public static final String ACTION_CATEGORY = "com.android.tv.tuner.setup.LocationFragment";
+ public static final String KEY_POSTAL_CODE = "key_postal_code";
+
+ public static final int ACTION_ALLOW_PERMISSION = 1;
+ public static final int ENTER_ZIP_CODE = 2;
+ public static final int ACTION_GETTING_LOCATION = 3;
+ public static final int GET_LOCATION_TIMEOUT_MS = 3000;
+
+ @Override
+ protected SetupGuidedStepFragment onCreateContentFragment() {
+ return new ContentFragment();
+ }
+
+ @Override
+ protected String getActionCategory() {
+ return ACTION_CATEGORY;
+ }
+
+ @Override
+ protected boolean needsDoneButton() {
+ return false;
+ }
+
+ /** The content fragment of {@link LocationFragment}. */
+ public static class ContentFragment extends SetupGuidedStepFragment
+ implements LocationUtils.OnUpdateAddressListener {
+ private final List<GuidedAction> mGettingLocationAction = new ArrayList<>();
+ private final Handler mHandler = new Handler();
+ private final Object mPostalCodeLock = new Object();
+
+ private String mPostalCode;
+ private boolean mPermissionGranted;
+
+ private final Runnable mTimeoutRunnable =
+ () -> {
+ synchronized (mPostalCodeLock) {
+ if (DEBUG) {
+ Log.d(TAG,
+ "get location timeout. mPostalCode=" + mPostalCode);
+ }
+ if (mPostalCode == null) {
+ // timeout. setup activity will get null postal code
+ LocationUtils.removeOnUpdateAddressListener(this);
+ passPostalCode();
+ }
+ }
+ };
+
+ @NonNull
+ @Override
+ public Guidance onCreateGuidance(Bundle savedInstanceState) {
+ String title = getString(R.string.location_guidance_title);
+ String description = getString(R.string.location_guidance_description);
+ return new Guidance(title, description, getString(R.string.ut_setup_breadcrumb), null);
+ }
+
+ @Override
+ public void onCreateActions(
+ @NonNull List<GuidedAction> actions, Bundle savedInstanceState) {
+ actions.add(
+ new GuidedAction.Builder(getActivity())
+ .id(ACTION_ALLOW_PERMISSION)
+ .title(getString(R.string.location_choices_allow_permission))
+ .build());
+ actions.add(
+ new GuidedAction.Builder(getActivity())
+ .id(ENTER_ZIP_CODE)
+ .title(getString(R.string.location_choices_enter_zip_code))
+ .build());
+ actions.add(
+ new GuidedAction.Builder(getActivity())
+ .id(ACTION_SKIP)
+ .title(getString(com.android.tv.common.R.string.action_text_skip))
+ .build());
+ mGettingLocationAction.add(
+ new GuidedAction.Builder(getActivity())
+ .id(ACTION_GETTING_LOCATION)
+ .title(getString(R.string.location_choices_getting_location))
+ .focusable(false)
+ .build()
+ );
+ }
+
+ @Override
+ public void onGuidedActionClicked(GuidedAction action) {
+ if (DEBUG) {
+ Log.d(TAG, "onGuidedActionClicked. Action ID = " + action.getId());
+ }
+ if (action.getId() == ACTION_ALLOW_PERMISSION) {
+ // request permission when users click this action
+ mPermissionGranted = false;
+ requestPermissions(
+ new String[] {android.Manifest.permission.ACCESS_COARSE_LOCATION},
+ PERMISSIONS_REQUEST_ACCESS_COARSE_LOCATION);
+ } else {
+ super.onGuidedActionClicked(action);
+ }
+ }
+
+ @Override
+ protected String getActionCategory() {
+ return ACTION_CATEGORY;
+ }
+
+ @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) {
+ synchronized (mPostalCodeLock) {
+ mPermissionGranted = true;
+ if (mPostalCode == null) {
+ // get postal code immediately if available
+ try {
+ Address address = LocationUtils.getCurrentAddress(getActivity());
+ if (address != null) {
+ mPostalCode = address.getPostalCode();
+ }
+ } catch (IOException e) {
+ // do nothing
+ }
+ }
+ if (DEBUG) {
+ Log.d(TAG, "permission granted. mPostalCode=" + mPostalCode);
+ }
+ if (mPostalCode != null) {
+ // if postal code is known, pass it the setup activity
+ LocationUtils.removeOnUpdateAddressListener(this);
+ passPostalCode();
+ } else {
+ // show "getting location" message
+ setActions(mGettingLocationAction);
+ // post timeout runnable
+ mHandler.postDelayed(mTimeoutRunnable, GET_LOCATION_TIMEOUT_MS);
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ public boolean onUpdateAddress(Address address) {
+ synchronized (mPostalCodeLock) {
+ // it takes time to get location after the permission is granted,
+ // so this listener is needed
+ mPostalCode = address.getPostalCode();
+ if (DEBUG) {
+ Log.d(TAG, "onUpdateAddress. mPostalCode=" + mPostalCode);
+ }
+ if (mPermissionGranted && mPostalCode != null) {
+ // pass the postal code only if permission is granted
+ passPostalCode();
+ return true;
+ }
+ return false;
+ }
+ }
+
+ @Override
+ public void onResume() {
+ if (DEBUG) {
+ Log.d(TAG, "onResume");
+ }
+ super.onResume();
+ LocationUtils.addOnUpdateAddressListener(this);
+ }
+
+ @Override
+ public void onPause() {
+ if (DEBUG) {
+ Log.d(TAG, "onPause");
+ }
+ LocationUtils.removeOnUpdateAddressListener(this);
+ mHandler.removeCallbacks(mTimeoutRunnable);
+ super.onPause();
+ }
+
+ private void passPostalCode() {
+ synchronized (mPostalCodeLock) {
+ mHandler.removeCallbacks(mTimeoutRunnable);
+ Bundle params = new Bundle();
+ if (mPostalCode != null) {
+ params.putString(KEY_POSTAL_CODE, mPostalCode);
+ }
+ SetupActionHelper.onActionClick(
+ this, ACTION_CATEGORY, ACTION_ALLOW_PERMISSION, params);
+ }
+ }
+ }
+}
diff --git a/tuner/src/com/android/tv/tuner/setup/PostalCodeFragment.java b/tuner/src/com/android/tv/tuner/setup/PostalCodeFragment.java
index f4b9f65e..52247972 100644
--- a/tuner/src/com/android/tv/tuner/setup/PostalCodeFragment.java
+++ b/tuner/src/com/android/tv/tuner/setup/PostalCodeFragment.java
@@ -32,10 +32,11 @@ import com.android.tv.common.util.PostalCodeUtils;
import com.android.tv.tuner.R;
import java.util.List;
-/** A fragment for initial screen. */
+/** A fragment for users to enter postal code. */
public class PostalCodeFragment extends SetupMultiPaneFragment {
public static final String ACTION_CATEGORY = "com.android.tv.tuner.setup.PostalCodeFragment";
public static final String KEY_POSTAL_CODE = "postal_code";
+ public static final String KEY_GET_LOCATION_FAILED = "get_location_failed";
private static final int VIEW_TYPE_EDITABLE = 1;
@Override
@@ -43,6 +44,11 @@ public class PostalCodeFragment extends SetupMultiPaneFragment {
ContentFragment fragment = new ContentFragment();
Bundle arguments = new Bundle();
arguments.putBoolean(SetupGuidedStepFragment.KEY_THREE_PANE, true);
+ if (getArguments() != null) {
+ arguments.putBoolean(
+ KEY_GET_LOCATION_FAILED,
+ getArguments().getBoolean(KEY_GET_LOCATION_FAILED, false));
+ }
fragment.setArguments(arguments);
return fragment;
}
@@ -139,9 +145,16 @@ public class PostalCodeFragment extends SetupMultiPaneFragment {
@Override
public Guidance onCreateGuidance(Bundle savedInstanceState) {
String title = getString(R.string.postal_code_guidance_title);
- String description = getString(R.string.postal_code_guidance_description);
+ StringBuilder description = new StringBuilder();
+ if (getArguments().getBoolean(KEY_GET_LOCATION_FAILED, false)) {
+ description
+ .append(getString(R.string
+ .postal_code_guidance_description_get_location_failed))
+ .append(" ");
+ }
+ description.append(getString(R.string.postal_code_guidance_description));
String breadcrumb = getString(R.string.ut_setup_breadcrumb);
- return new Guidance(title, description, breadcrumb, null);
+ return new Guidance(title, description.toString(), breadcrumb, null);
}
@Override
diff --git a/tuner/src/com/android/tv/tuner/setup/ScanFragment.java b/tuner/src/com/android/tv/tuner/setup/ScanFragment.java
index 3ac86e19..7d59284c 100644
--- a/tuner/src/com/android/tv/tuner/setup/ScanFragment.java
+++ b/tuner/src/com/android/tv/tuner/setup/ScanFragment.java
@@ -37,21 +37,21 @@ import android.widget.ProgressBar;
import android.widget.TextView;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.common.ui.setup.SetupFragment;
-import com.android.tv.tuner.ChannelScanFileParser;
import com.android.tv.tuner.R;
-import com.android.tv.tuner.TunerHal;
-import com.android.tv.tuner.TunerPreferences;
+import com.android.tv.tuner.api.ScanChannel;
+import com.android.tv.tuner.api.Tuner;
import com.android.tv.tuner.data.PsipData;
import com.android.tv.tuner.data.TunerChannel;
import com.android.tv.tuner.data.nano.Channel;
+import com.android.tv.tuner.prefs.TunerPreferences;
import com.android.tv.tuner.source.FileTsStreamer;
import com.android.tv.tuner.source.TsDataSource;
import com.android.tv.tuner.source.TsStreamer;
import com.android.tv.tuner.source.TunerTsStreamer;
-import com.android.tv.tuner.tvinput.ChannelDataManager;
-import com.android.tv.tuner.tvinput.EventDetector;
+import com.android.tv.tuner.ts.EventDetector;
+import com.android.tv.tuner.tvinput.datamanager.ChannelDataManager;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
@@ -99,7 +99,7 @@ public class ScanFragment extends SetupFragment {
if (DEBUG) Log.d(TAG, "onCreateView");
View view = super.onCreateView(inflater, container, savedInstanceState);
mChannelNumbers = new ArrayList<>();
- mChannelDataManager = new ChannelDataManager(getActivity());
+ mChannelDataManager = new ChannelDataManager(getActivity().getApplicationContext());
mChannelDataManager.checkDataVersion(getActivity());
mAdapter = new ChannelAdapter();
mProgressBar = (ProgressBar) view.findViewById(R.id.tune_progress);
@@ -126,10 +126,10 @@ public class ScanFragment extends SetupFragment {
startScan(args == null ? 0 : args.getInt(EXTRA_FOR_CHANNEL_SCAN_FILE, 0));
TextView scanTitleView = (TextView) view.findViewById(R.id.tune_title);
switch (tunerType) {
- case TunerHal.TUNER_TYPE_USB:
+ case Tuner.TUNER_TYPE_USB:
scanTitleView.setText(R.string.ut_channel_scan);
break;
- case TunerHal.TUNER_TYPE_NETWORK:
+ case Tuner.TUNER_TYPE_NETWORK:
scanTitleView.setText(R.string.nt_channel_scan);
break;
default:
@@ -176,12 +176,9 @@ public class ScanFragment extends SetupFragment {
// Notifies a user of waiting to finish the scanning process.
new Handler()
.postDelayed(
- new Runnable() {
- @Override
- public void run() {
- if (mChannelScanTask != null) {
- mChannelScanTask.showFinishingProgressDialog();
- }
+ () -> {
+ if (mChannelScanTask != null) {
+ mChannelScanTask.showFinishingProgressDialog();
}
},
SHOW_PROGRESS_DIALOG_DELAY_MS);
@@ -248,7 +245,7 @@ public class ScanFragment extends SetupFragment {
}
private class ChannelScanTask extends AsyncTask<Void, Integer, Void>
- implements EventDetector.EventListener, ChannelDataManager.ChannelScanListener {
+ implements EventDetector.EventListener, ChannelDataManager.ChannelHandlingDoneListener {
private static final int MAX_PROGRESS = 100;
private final Activity mActivity;
@@ -257,7 +254,7 @@ public class ScanFragment extends SetupFragment {
private final TsStreamer mFileTsStreamer;
private final ConditionVariable mConditionStopped;
- private final List<ChannelScanFileParser.ScanChannel> mScanChannelList = new ArrayList<>();
+ private final List<ScanChannel> mScanChannelList = new ArrayList<>();
private boolean mIsCanceled;
private boolean mIsFinished;
private ProgressDialog mFinishingProgressDialog;
@@ -269,7 +266,7 @@ public class ScanFragment extends SetupFragment {
if (FAKE_MODE) {
mScanTsStreamer = new FakeTsStreamer(this);
} else {
- TunerHal hal = ((BaseTunerSetupActivity) mActivity).getTunerHal();
+ Tuner hal = ((BaseTunerSetupActivity) mActivity).getTunerHal();
if (hal == null) {
throw new RuntimeException("Failed to open a DVB device");
}
@@ -282,41 +279,35 @@ public class ScanFragment extends SetupFragment {
private void maybeSetChannelListVisible() {
mActivity.runOnUiThread(
- new Runnable() {
- @Override
- public void run() {
- int channelsFound = mAdapter.getCount();
- if (!mChannelListVisible && channelsFound > 0) {
- String format =
- getResources()
- .getQuantityString(
- R.plurals.ut_channel_scan_message,
- channelsFound,
- channelsFound);
- mScanningMessage.setText(String.format(format, channelsFound));
- mChannelHolder.setVisibility(View.VISIBLE);
- mChannelListVisible = true;
- }
+ () -> {
+ int channelsFound = mAdapter.getCount();
+ if (!mChannelListVisible && channelsFound > 0) {
+ String format =
+ getResources()
+ .getQuantityString(
+ R.plurals.ut_channel_scan_message,
+ channelsFound,
+ channelsFound);
+ mScanningMessage.setText(String.format(format, channelsFound));
+ mChannelHolder.setVisibility(View.VISIBLE);
+ mChannelListVisible = true;
}
});
}
private void addChannel(final TunerChannel channel) {
mActivity.runOnUiThread(
- new Runnable() {
- @Override
- public void run() {
- mAdapter.add(channel);
- if (mChannelListVisible) {
- int channelsFound = mAdapter.getCount();
- String format =
- getResources()
- .getQuantityString(
- R.plurals.ut_channel_scan_message,
- channelsFound,
- channelsFound);
- mScanningMessage.setText(String.format(format, channelsFound));
- }
+ () -> {
+ mAdapter.add(channel);
+ if (mChannelListVisible) {
+ int channelsFound = mAdapter.getCount();
+ String format =
+ getResources()
+ .getQuantityString(
+ R.plurals.ut_channel_scan_message,
+ channelsFound,
+ channelsFound);
+ mScanningMessage.setText(String.format(format, channelsFound));
}
});
}
@@ -366,7 +357,7 @@ public class ScanFragment extends SetupFragment {
long startMs = System.currentTimeMillis();
int i = 1;
- for (ChannelScanFileParser.ScanChannel scanChannel : mScanChannelList) {
+ for (ScanChannel scanChannel : mScanChannelList) {
int frequency = scanChannel.frequency;
String modulation = scanChannel.modulation;
Log.i(TAG, "Tuning to " + frequency + " " + modulation);
@@ -403,7 +394,7 @@ public class ScanFragment extends SetupFragment {
if (DEBUG) Log.i(TAG, "Channel scan ended");
}
- private void addChannelsWithoutVct(ChannelScanFileParser.ScanChannel scanChannel) {
+ private void addChannelsWithoutVct(ScanChannel scanChannel) {
if (scanChannel.radioFrequencyNumber == null
|| !(mScanTsStreamer instanceof TunerTsStreamer)) {
return;
@@ -515,7 +506,7 @@ public class ScanFragment extends SetupFragment {
}
@Override
- public boolean startStream(ChannelScanFileParser.ScanChannel channel) {
+ public boolean startStream(ScanChannel channel) {
if (++mProgramNumber % 2 == 1) {
return true;
}
diff --git a/tuner/src/com/android/tv/tuner/setup/ScanResultFragment.java b/tuner/src/com/android/tv/tuner/setup/ScanResultFragment.java
index 480bf081..bd3f9ad9 100644
--- a/tuner/src/com/android/tv/tuner/setup/ScanResultFragment.java
+++ b/tuner/src/com/android/tv/tuner/setup/ScanResultFragment.java
@@ -25,11 +25,11 @@ 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.api.Tuner;
+import com.android.tv.tuner.prefs.TunerPreferences;
import java.util.List;
-/** A fragment for initial screen. */
+/** A fragment to show found channels. */
public class ScanResultFragment extends SetupMultiPaneFragment {
public static final String ACTION_CATEGORY = "com.android.tv.tuner.setup.ScanResultFragment";
@@ -83,10 +83,10 @@ public class ScanResultFragment extends SetupMultiPaneFragment {
(args == null ? 0 : args.getInt(BaseTunerSetupActivity.KEY_TUNER_TYPE, 0));
title = getString(R.string.ut_result_not_found_title);
switch (tunerType) {
- case TunerHal.TUNER_TYPE_USB:
+ case Tuner.TUNER_TYPE_USB:
description = getString(R.string.ut_result_not_found_description);
break;
- case TunerHal.TUNER_TYPE_NETWORK:
+ case Tuner.TUNER_TYPE_NETWORK:
description = getString(R.string.nt_result_not_found_description);
break;
default:
diff --git a/tuner/src/com/android/tv/tuner/setup/WelcomeFragment.java b/tuner/src/com/android/tv/tuner/setup/WelcomeFragment.java
index 788ba918..2a414df7 100644
--- a/tuner/src/com/android/tv/tuner/setup/WelcomeFragment.java
+++ b/tuner/src/com/android/tv/tuner/setup/WelcomeFragment.java
@@ -24,8 +24,8 @@ 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.api.Tuner;
+import com.android.tv.tuner.prefs.TunerPreferences;
import java.util.List;
/** A fragment for initial screen. */
@@ -69,14 +69,14 @@ public class WelcomeFragment extends SetupMultiPaneFragment {
getArguments()
.getInt(
BaseTunerSetupActivity.KEY_TUNER_TYPE,
- TunerHal.TUNER_TYPE_BUILT_IN);
+ Tuner.TUNER_TYPE_BUILT_IN);
if (mChannelCountOnPreference == 0) {
switch (tunerType) {
- case TunerHal.TUNER_TYPE_USB:
+ case Tuner.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:
+ case Tuner.TUNER_TYPE_NETWORK:
title = getString(R.string.nt_setup_new_title);
description = getString(R.string.nt_setup_new_description);
break;
@@ -87,10 +87,10 @@ public class WelcomeFragment extends SetupMultiPaneFragment {
} else {
title = getString(R.string.bt_setup_again_title);
switch (tunerType) {
- case TunerHal.TUNER_TYPE_USB:
+ case Tuner.TUNER_TYPE_USB:
description = getString(R.string.ut_setup_again_description);
break;
- case TunerHal.TUNER_TYPE_NETWORK:
+ case Tuner.TUNER_TYPE_NETWORK:
description = getString(R.string.nt_setup_again_description);
break;
default:
diff --git a/tuner/src/com/android/tv/tuner/singletons/TunerSingletons.java b/tuner/src/com/android/tv/tuner/singletons/TunerSingletons.java
new file mode 100644
index 00000000..48b17dcb
--- /dev/null
+++ b/tuner/src/com/android/tv/tuner/singletons/TunerSingletons.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.tuner.singletons;
+
+import com.android.tv.common.singletons.HasTvInputId;
+
+/** Singletons used in tuner applications */
+public interface TunerSingletons extends HasTvInputId {}
diff --git a/tuner/src/com/android/tv/tuner/tvinput/FileSourceEventDetector.java b/tuner/src/com/android/tv/tuner/source/FileSourceEventDetector.java
index ab05aa02..85932c8c 100644
--- a/tuner/src/com/android/tv/tuner/tvinput/FileSourceEventDetector.java
+++ b/tuner/src/com/android/tv/tuner/source/FileSourceEventDetector.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.tv.tuner.tvinput;
+package com.android.tv.tuner.source;
import android.util.Log;
import android.util.SparseArray;
@@ -27,9 +27,8 @@ import com.android.tv.tuner.data.PsipData.VctItem;
import com.android.tv.tuner.data.TunerChannel;
import com.android.tv.tuner.data.nano.Track.AtscAudioTrack;
import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack;
-import com.android.tv.tuner.source.FileTsStreamer;
+import com.android.tv.tuner.ts.EventDetector.EventListener;
import com.android.tv.tuner.ts.TsParser;
-import com.android.tv.tuner.tvinput.EventDetector.EventListener;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
@@ -57,7 +56,7 @@ public class FileSourceEventDetector {
private FileTsStreamer.StreamProvider mStreamProvider;
private int mProgramNumber = ALL_PROGRAM_NUMBERS;
- public FileSourceEventDetector(EventDetector.EventListener listener, boolean enableDvbSignal) {
+ public FileSourceEventDetector(EventListener listener, boolean enableDvbSignal) {
mEventListener = listener;
mEnableDvbSignal = enableDvbSignal;
}
diff --git a/tuner/src/com/android/tv/tuner/source/FileTsStreamer.java b/tuner/src/com/android/tv/tuner/source/FileTsStreamer.java
index 38a59b3d..99d37e39 100644
--- a/tuner/src/com/android/tv/tuner/source/FileTsStreamer.java
+++ b/tuner/src/com/android/tv/tuner/source/FileTsStreamer.java
@@ -21,12 +21,11 @@ import android.os.Environment;
import android.util.Log;
import android.util.SparseBooleanArray;
import com.android.tv.common.SoftPreconditions;
-import com.android.tv.tuner.ChannelScanFileParser.ScanChannel;
-import com.android.tv.tuner.TunerFeatures;
+import com.android.tv.tuner.api.ScanChannel;
import com.android.tv.tuner.data.TunerChannel;
+import com.android.tv.tuner.features.TunerFeatures;
+import com.android.tv.tuner.ts.EventDetector.EventListener;
import com.android.tv.tuner.ts.TsParser;
-import com.android.tv.tuner.tvinput.EventDetector;
-import com.android.tv.tuner.tvinput.FileSourceEventDetector;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.upstream.DataSpec;
import java.io.BufferedInputStream;
@@ -125,7 +124,7 @@ public class FileTsStreamer implements TsStreamer {
*
* @param eventListener the listener for channel & program information
*/
- public FileTsStreamer(EventDetector.EventListener eventListener, Context context) {
+ public FileTsStreamer(EventListener eventListener, Context context) {
mEventDetector =
new FileSourceEventDetector(
eventListener, TunerFeatures.ENABLE_FILE_DVB.isEnabled(context));
diff --git a/tuner/src/com/android/tv/tuner/source/TsDataSource.java b/tuner/src/com/android/tv/tuner/source/TsDataSource.java
index be902944..cf3c25d9 100644
--- a/tuner/src/com/android/tv/tuner/source/TsDataSource.java
+++ b/tuner/src/com/android/tv/tuner/source/TsDataSource.java
@@ -16,6 +16,7 @@
package com.android.tv.tuner.source;
+import com.android.tv.common.compat.TvInputConstantCompat;
import com.google.android.exoplayer.upstream.DataSource;
/** {@link DataSource} for MPEG-TS stream, which will be used by {@link TsExtractor}. */
@@ -46,4 +47,8 @@ public abstract class TsDataSource implements DataSource {
* @param offset 0 <= offset <= buffered position
*/
public void shiftStartPosition(long offset) {}
+
+ public int getSignalStrength() {
+ return TvInputConstantCompat.SIGNAL_STRENGTH_NOT_USED;
+ }
}
diff --git a/tuner/src/com/android/tv/tuner/source/TsDataSourceManager.java b/tuner/src/com/android/tv/tuner/source/TsDataSourceManager.java
index 08acbc88..28756a93 100644
--- a/tuner/src/com/android/tv/tuner/source/TsDataSourceManager.java
+++ b/tuner/src/com/android/tv/tuner/source/TsDataSourceManager.java
@@ -18,49 +18,58 @@ 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.api.Tuner;
import com.android.tv.tuner.data.TunerChannel;
import com.android.tv.tuner.data.nano.Channel;
-import com.android.tv.tuner.tvinput.EventDetector;
+import com.android.tv.tuner.ts.EventDetector.EventListener;
+import com.google.auto.factory.AutoFactory;
+import com.google.auto.factory.Provided;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+import javax.inject.Inject;
+import javax.inject.Provider;
/**
- * Manages {@link DataSource} for playback and recording. The class hides handling of {@link
- * TunerHal} and {@link TsStreamer} from other classes. One TsDataSourceManager should be created
- * for per session.
+ * Manages {@link TsDataSource} for playback and recording. The class hides handling of {@link
+ * Tuner} and {@link TsStreamer} from other classes. One TsDataSourceManager should be created for
+ * per session.
*/
+@AutoFactory
public class TsDataSourceManager {
- private static final Object sLock = new Object();
private static final Map<TsDataSource, TsStreamer> sTsStreamers = new ConcurrentHashMap<>();
- private static int sSequenceId;
+ private static final AtomicInteger sSequenceId = new AtomicInteger();
- private final int mId;
+ private final int mId = sSequenceId.incrementAndGet();
private final boolean mIsRecording;
- private final TunerTsStreamerManager mTunerStreamerManager =
- TunerTsStreamerManager.getInstance();
+ private final TunerTsStreamerManager mTunerStreamerManager;
private boolean mKeepTuneStatus;
/**
- * Creates TsDataSourceManager to create and release {@link DataSource} which will be used for
- * playing and recording.
+ * Factory for {@link }TsDataSourceManager}.
*
- * @param isRecording {@code true} when for recording, {@code false} otherwise
- * @return {@link TsDataSourceManager}
+ * <p>This wrapper class keeps other classes from needing to reference the {@link AutoFactory}
+ * generated class.
*/
- public static TsDataSourceManager createSourceManager(boolean isRecording) {
- int id;
- synchronized (sLock) {
- id = ++sSequenceId;
+ public static final class Factory {
+ private final TsDataSourceManagerFactory mDelegate;
+
+ @Inject
+ public Factory(Provider<TunerTsStreamerManager> tunerStreamerManagerProvider) {
+ mDelegate = new TsDataSourceManagerFactory(tunerStreamerManagerProvider);
+ }
+
+ public TsDataSourceManager create(boolean isRecording) {
+ return mDelegate.create(isRecording);
}
- return new TsDataSourceManager(id, isRecording);
}
- private TsDataSourceManager(int id, boolean isRecording) {
- mId = id;
+ TsDataSourceManager(
+ boolean isRecording, @Provided TunerTsStreamerManager tunerStreamerManager) {
mIsRecording = isRecording;
+ this.mTunerStreamerManager = tunerStreamerManager;
mKeepTuneStatus = true;
}
@@ -73,7 +82,7 @@ public class TsDataSourceManager {
* @return {@link TsDataSource} which will provide the specified channel stream
*/
public TsDataSource createDataSource(
- Context context, TunerChannel channel, EventDetector.EventListener eventListener) {
+ Context context, TunerChannel channel, EventListener eventListener) {
if (channel.getType() == Channel.TunerType.TYPE_FILE) {
// MPEG2 TS captured stream file recording is not supported.
if (mIsRecording) {
@@ -92,7 +101,7 @@ public class TsDataSourceManager {
}
/**
- * Releases the specified {@link TsDataSource} and underlying {@link TunerHal}.
+ * Releases the specified {@link TsDataSource} and underlying {@link Tuner}.
*
* @param source to release
*/
@@ -114,10 +123,10 @@ public class TsDataSourceManager {
}
/**
- * Indicates whether the underlying {@link TunerHal} should be kept or not when data source is
+ * Indicates whether the underlying {@link Tuner} should be kept or not when data source is
* being released. TODO: If b/30750953 is fixed, we can remove this function.
*
- * @param keepTuneStatus underlying {@link TunerHal} will be reused when data source releasing.
+ * @param keepTuneStatus underlying {@link Tuner} will be reused when data source releasing.
*/
public void setKeepTuneStatus(boolean keepTuneStatus) {
mKeepTuneStatus = keepTuneStatus;
@@ -125,7 +134,7 @@ public class TsDataSourceManager {
/** Add tuner hal into TunerTsStreamerManager for test. */
@VisibleForTesting
- public void addTunerHalForTest(TunerHal tunerHal) {
+ public void addTunerHalForTest(Tuner tunerHal) {
mTunerStreamerManager.addTunerHal(tunerHal, mId);
}
diff --git a/tuner/src/com/android/tv/tuner/source/TsStreamer.java b/tuner/src/com/android/tv/tuner/source/TsStreamer.java
index 3dbba7e7..e5658e71 100644
--- a/tuner/src/com/android/tv/tuner/source/TsStreamer.java
+++ b/tuner/src/com/android/tv/tuner/source/TsStreamer.java
@@ -16,7 +16,7 @@
package com.android.tv.tuner.source;
-import com.android.tv.tuner.ChannelScanFileParser;
+import com.android.tv.tuner.api.ScanChannel;
import com.android.tv.tuner.data.TunerChannel;
/**
@@ -27,10 +27,10 @@ public interface TsStreamer {
/**
* Starts streaming the data for channel scanning process.
*
- * @param channel {@link ChannelScanFileParser.ScanChannel} to be scanned
+ * @param channel {@link ScanChannel} to be scanned
* @return {@code true} if ready to stream, otherwise {@code false}
*/
- boolean startStream(ChannelScanFileParser.ScanChannel channel);
+ boolean startStream(ScanChannel channel);
/**
* Starts streaming the data for channel playing or recording.
diff --git a/tuner/src/com/android/tv/tuner/source/TunerSourceModule.java b/tuner/src/com/android/tv/tuner/source/TunerSourceModule.java
new file mode 100644
index 00000000..12d2de1b
--- /dev/null
+++ b/tuner/src/com/android/tv/tuner/source/TunerSourceModule.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.tuner.source;
+
+import com.android.tv.tuner.api.TunerFactory;
+import dagger.Module;
+import dagger.Provides;
+import javax.inject.Singleton;
+
+/** Dagger module for TV Tuners Sources. */
+@Module()
+public class TunerSourceModule {
+ @Provides
+ @Singleton
+ TunerTsStreamerManager providesTunerTsStreamerManager(TunerFactory tunerFactory) {
+ return new TunerTsStreamerManager(tunerFactory);
+ }
+}
diff --git a/tuner/src/com/android/tv/tuner/source/TunerTsStreamer.java b/tuner/src/com/android/tv/tuner/source/TunerTsStreamer.java
index 21b7a1f8..9e68c910 100644
--- a/tuner/src/com/android/tv/tuner/source/TunerTsStreamer.java
+++ b/tuner/src/com/android/tv/tuner/source/TunerTsStreamer.java
@@ -20,12 +20,12 @@ import android.content.Context;
import android.util.Log;
import android.util.Pair;
import com.android.tv.common.SoftPreconditions;
-import com.android.tv.tuner.ChannelScanFileParser;
-import com.android.tv.tuner.TunerHal;
-import com.android.tv.tuner.TunerPreferences;
+import com.android.tv.tuner.api.ScanChannel;
+import com.android.tv.tuner.api.Tuner;
import com.android.tv.tuner.data.TunerChannel;
-import com.android.tv.tuner.tvinput.EventDetector;
-import com.android.tv.tuner.tvinput.EventDetector.EventListener;
+import com.android.tv.tuner.prefs.TunerPreferences;
+import com.android.tv.tuner.ts.EventDetector;
+import com.android.tv.tuner.ts.EventDetector.EventListener;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.upstream.DataSpec;
import java.io.IOException;
@@ -53,7 +53,7 @@ public class TunerTsStreamer implements TsStreamer {
private final AtomicLong mLastReadPosition = new AtomicLong();
private boolean mStreaming;
- private final TunerHal mTunerHal;
+ private final Tuner mTunerHal;
private TunerChannel mChannel;
private Thread mStreamingThread;
private final EventDetector mEventDetector;
@@ -121,6 +121,11 @@ public class TunerTsStreamer implements TsStreamer {
}
return ret;
}
+
+ @Override
+ public int getSignalStrength() {
+ return mTsStreamer.getSignalStrength();
+ }
}
/**
* Creates {@link TsStreamer} for playing or recording the specified channel.
@@ -128,7 +133,7 @@ public class TunerTsStreamer implements TsStreamer {
* @param tunerHal the HAL for tuner device
* @param eventListener the listener for channel & program information
*/
- public TunerTsStreamer(TunerHal tunerHal, EventListener eventListener, Context context) {
+ public TunerTsStreamer(Tuner tunerHal, EventListener eventListener, Context context) {
mTunerHal = tunerHal;
mEventDetector = new EventDetector(mTunerHal);
if (eventListener != null) {
@@ -140,7 +145,7 @@ public class TunerTsStreamer implements TsStreamer {
: null;
}
- public TunerTsStreamer(TunerHal tunerHal, EventListener eventListener) {
+ public TunerTsStreamer(Tuner tunerHal, EventListener eventListener) {
this(tunerHal, eventListener, null);
}
@@ -149,20 +154,20 @@ public class TunerTsStreamer implements TsStreamer {
if (mTunerHal.tune(
channel.getFrequency(), channel.getModulation(), channel.getDisplayNumber(false))) {
if (channel.hasVideo()) {
- mTunerHal.addPidFilter(channel.getVideoPid(), TunerHal.FILTER_TYPE_VIDEO);
+ mTunerHal.addPidFilter(channel.getVideoPid(), Tuner.FILTER_TYPE_VIDEO);
}
boolean audioFilterSet = false;
for (Integer audioPid : channel.getAudioPids()) {
if (!audioFilterSet) {
- mTunerHal.addPidFilter(audioPid, TunerHal.FILTER_TYPE_AUDIO);
+ mTunerHal.addPidFilter(audioPid, Tuner.FILTER_TYPE_AUDIO);
audioFilterSet = true;
} else {
// FILTER_TYPE_AUDIO overrides the previous filter for audio. We use
// FILTER_TYPE_OTHER from the secondary one to get the all audio tracks.
- mTunerHal.addPidFilter(audioPid, TunerHal.FILTER_TYPE_OTHER);
+ mTunerHal.addPidFilter(audioPid, Tuner.FILTER_TYPE_OTHER);
}
}
- mTunerHal.addPidFilter(channel.getPcrPid(), TunerHal.FILTER_TYPE_PCR);
+ mTunerHal.addPidFilter(channel.getPcrPid(), Tuner.FILTER_TYPE_PCR);
if (mEventDetector != null) {
mEventDetector.startDetecting(
channel.getFrequency(),
@@ -193,7 +198,7 @@ public class TunerTsStreamer implements TsStreamer {
}
@Override
- public boolean startStream(ChannelScanFileParser.ScanChannel channel) {
+ public boolean startStream(ScanChannel channel) {
if (mTunerHal.tune(channel.frequency, channel.modulation, null)) {
mEventDetector.startDetecting(
channel.frequency, channel.modulation, EventDetector.ALL_PROGRAM_NUMBERS);
@@ -255,11 +260,11 @@ public class TunerTsStreamer implements TsStreamer {
}
/**
- * Returns the current {@link TunerHal} which provides MPEG-TS stream for TunerTsStreamer.
+ * Returns the current {@link Tuner} which provides MPEG-TS stream for TunerTsStreamer.
*
- * @return {@link TunerHal}
+ * @return {@link Tuner}
*/
- public TunerHal getTunerHal() {
+ public Tuner getTunerHal() {
return mTunerHal;
}
@@ -303,6 +308,10 @@ public class TunerTsStreamer implements TsStreamer {
}
}
+ public int getSignalStrength() {
+ return mTunerHal.getSignalStrength();
+ }
+
private class StreamingThread extends Thread {
@Override
public void run() {
diff --git a/tuner/src/com/android/tv/tuner/source/TunerTsStreamerManager.java b/tuner/src/com/android/tv/tuner/source/TunerTsStreamerManager.java
index 44fb41e6..076206c4 100644
--- a/tuner/src/com/android/tv/tuner/source/TunerTsStreamerManager.java
+++ b/tuner/src/com/android/tv/tuner/source/TunerTsStreamerManager.java
@@ -17,52 +17,49 @@
package com.android.tv.tuner.source;
import android.content.Context;
+import android.support.annotation.VisibleForTesting;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.common.util.AutoCloseableUtils;
-import com.android.tv.tuner.TunerHal;
+import com.android.tv.tuner.api.Tuner;
+import com.android.tv.tuner.api.TunerFactory;
import com.android.tv.tuner.data.TunerChannel;
-import com.android.tv.tuner.tvinput.EventDetector;
+import com.android.tv.tuner.ts.EventDetector.EventListener;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
+import javax.inject.Inject;
+import javax.inject.Singleton;
/**
* Manages {@link TunerTsStreamer} for playback and recording. The class hides handling of {@link
- * TunerHal} from other classes. This class is used by {@link TsDataSourceManager}. Don't use this
+ * Tuner} from other classes. This class is used by {@link TsDataSourceManager}. Don't use this
* class directly.
*/
-class TunerTsStreamerManager {
+@Singleton
+@VisibleForTesting
+public class TunerTsStreamerManager {
// The lock will protect mStreamerFinder, mSourceToStreamerMap and some part of TsStreamCreator
// to support timely {@link TunerTsStreamer} cancellation due to a new tune request from
// the same session.
private final Object mCancelLock = new Object();
private final StreamerFinder mStreamerFinder = new StreamerFinder();
private final Map<Integer, TsStreamerCreator> mCreators = new HashMap<>();
- private final Map<Integer, EventDetector.EventListener> mListeners = new HashMap<>();
+ private final Map<Integer, EventListener> mListeners = new HashMap<>();
private final Map<TsDataSource, TunerTsStreamer> mSourceToStreamerMap = new HashMap<>();
- private final TunerHalManager mTunerHalManager = new TunerHalManager();
- private static TunerTsStreamerManager sInstance;
+ private final TunerHalManager mTunerHalManager;
- /**
- * Returns the singleton instance for the class
- *
- * @return TunerTsStreamerManager
- */
- static synchronized TunerTsStreamerManager getInstance() {
- if (sInstance == null) {
- sInstance = new TunerTsStreamerManager();
- }
- return sInstance;
+ @Inject
+ @VisibleForTesting
+ public TunerTsStreamerManager(TunerFactory tunerFactory) {
+ mTunerHalManager = new TunerHalManager(tunerFactory);
}
- private TunerTsStreamerManager() {}
-
synchronized TsDataSource createDataSource(
Context context,
TunerChannel channel,
- EventDetector.EventListener listener,
+ EventListener listener,
int sessionId,
boolean reuse) {
TsStreamerCreator creator;
@@ -95,7 +92,7 @@ class TunerTsStreamerManager {
}
// Created streamer was cancelled by a new tune request.
streamer.stopStream();
- TunerHal hal = streamer.getTunerHal();
+ Tuner hal = streamer.getTunerHal();
hal.setHasPendingTune(false);
mTunerHalManager.releaseTunerHal(hal, sessionId, reuse);
return null;
@@ -109,7 +106,7 @@ class TunerTsStreamerManager {
if (streamer == null) {
return;
}
- EventDetector.EventListener listener = mListeners.remove(sessionId);
+ EventListener listener = mListeners.remove(sessionId);
streamer.unregisterListener(listener);
TunerChannel channel = streamer.getChannel();
SoftPreconditions.checkState(channel != null);
@@ -119,7 +116,7 @@ class TunerTsStreamerManager {
}
}
streamer.stopStream();
- TunerHal hal = streamer.getTunerHal();
+ Tuner hal = streamer.getTunerHal();
hal.setHasPendingTune(false);
mTunerHalManager.releaseTunerHal(hal, sessionId, reuse);
}
@@ -133,7 +130,7 @@ class TunerTsStreamerManager {
}
/** Add tuner hal into TunerHalManager for test. */
- void addTunerHal(TunerHal tunerHal, int sessionId) {
+ void addTunerHal(Tuner tunerHal, int sessionId) {
mTunerHalManager.addTunerHal(tunerHal, sessionId);
}
@@ -188,21 +185,20 @@ class TunerTsStreamerManager {
private class TsStreamerCreator {
private final Context mContext;
private final TunerChannel mChannel;
- private final EventDetector.EventListener mEventListener;
+ private final EventListener mEventListener;
// mCancelled will be {@code true} if a new tune request for the same session
// cancels create().
private boolean mCancelled;
- private TunerHal mTunerHal;
+ private Tuner mTunerHal;
- private TsStreamerCreator(
- Context context, TunerChannel channel, EventDetector.EventListener listener) {
+ private TsStreamerCreator(Context context, TunerChannel channel, EventListener listener) {
mContext = context;
mChannel = channel;
mEventListener = listener;
}
private TunerTsStreamer create(int sessionId, boolean reuse) {
- TunerHal hal = mTunerHalManager.getOrCreateTunerHal(mContext, sessionId);
+ Tuner hal = mTunerHalManager.getOrCreateTunerHal(mContext, sessionId);
if (hal == null) {
return null;
}
@@ -248,15 +244,20 @@ class TunerTsStreamerManager {
}
/**
- * Supports sharing {@link TunerHal} among multiple sessions. The class also supports session
- * affinity for {@link TunerHal} allocation.
+ * Supports sharing {@link Tuner} among multiple sessions. The class also supports session
+ * affinity for {@link Tuner} allocation.
*/
private static class TunerHalManager {
- private final Map<Integer, TunerHal> mTunerHals = new HashMap<>();
+ private final Map<Integer, Tuner> mTunerHals = new HashMap<>();
+ private final TunerFactory mTunerFactory;
+
+ private TunerHalManager(TunerFactory mTunerFactory) {
+ this.mTunerFactory = mTunerFactory;
+ }
- private TunerHal getOrCreateTunerHal(Context context, int sessionId) {
+ private Tuner getOrCreateTunerHal(Context context, int sessionId) {
// Handles session affinity.
- TunerHal hal = mTunerHals.get(sessionId);
+ Tuner hal = mTunerHals.get(sessionId);
if (hal != null) {
mTunerHals.remove(sessionId);
return hal;
@@ -269,15 +270,15 @@ class TunerTsStreamerManager {
mTunerHals.remove(key);
return hal;
}
- return TunerHal.createInstance(context);
+ return mTunerFactory.createInstance(context);
}
- private void releaseTunerHal(TunerHal hal, int sessionId, boolean reuse) {
+ private void releaseTunerHal(Tuner hal, int sessionId, boolean reuse) {
if (!reuse || !hal.isReusable()) {
AutoCloseableUtils.closeQuietly(hal);
return;
}
- TunerHal cachedHal = mTunerHals.get(sessionId);
+ Tuner cachedHal = mTunerHals.get(sessionId);
if (cachedHal != hal) {
mTunerHals.put(sessionId, hal);
if (cachedHal != null) {
@@ -287,7 +288,7 @@ class TunerTsStreamerManager {
}
private void releaseCachedHal(int sessionId) {
- TunerHal hal = mTunerHals.get(sessionId);
+ Tuner hal = mTunerHals.get(sessionId);
if (hal != null) {
mTunerHals.remove(sessionId);
}
@@ -296,7 +297,7 @@ class TunerTsStreamerManager {
}
}
- private void addTunerHal(TunerHal tunerHal, int sessionId) {
+ private void addTunerHal(Tuner tunerHal, int sessionId) {
mTunerHals.put(sessionId, tunerHal);
}
}
diff --git a/tuner/src/com/android/tv/tuner/tvinput/EventDetector.java b/tuner/src/com/android/tv/tuner/ts/EventDetector.java
index c529c6db..6d1fc277 100644
--- a/tuner/src/com/android/tv/tuner/tvinput/EventDetector.java
+++ b/tuner/src/com/android/tv/tuner/ts/EventDetector.java
@@ -14,18 +14,18 @@
* limitations under the License.
*/
-package com.android.tv.tuner.tvinput;
+package com.android.tv.tuner.ts;
import android.util.Log;
import android.util.SparseArray;
import android.util.SparseBooleanArray;
-import com.android.tv.tuner.TunerHal;
+import com.android.tv.tuner.api.Tuner;
import com.android.tv.tuner.data.PsiData;
import com.android.tv.tuner.data.PsipData;
+import com.android.tv.tuner.data.PsipData.EitItem;
import com.android.tv.tuner.data.TunerChannel;
import com.android.tv.tuner.data.nano.Track.AtscAudioTrack;
import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack;
-import com.android.tv.tuner.ts.TsParser;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
@@ -39,7 +39,7 @@ public class EventDetector {
private static final boolean DEBUG = false;
public static final int ALL_PROGRAM_NUMBERS = -1;
- private final TunerHal mTunerHal;
+ private final Tuner mTunerHal;
private TsParser mTsParser;
private final Set<Integer> mPidSet = new HashSet<>();
@@ -62,7 +62,7 @@ public class EventDetector {
for (PsiData.PatItem i : items) {
if (mProgramNumber == ALL_PROGRAM_NUMBERS
|| mProgramNumber == i.getProgramNo()) {
- mTunerHal.addPidFilter(i.getPmtPid(), TunerHal.FILTER_TYPE_OTHER);
+ mTunerHal.addPidFilter(i.getPmtPid(), Tuner.FILTER_TYPE_OTHER);
}
}
}
@@ -225,15 +225,7 @@ public class EventDetector {
};
/** Listener for detecting ATSC TV channels and receiving EPG data. */
- public interface EventListener {
-
- /**
- * Fired when new information of an ATSC TV channel arrived.
- *
- * @param channel an ATSC TV channel
- * @param channelArrivedAtFirstTime tells whether this channel arrived at first time
- */
- void onChannelDetected(TunerChannel channel, boolean channelArrivedAtFirstTime);
+ public interface EventListener extends com.android.tv.tuner.api.ChannelScanListener {
/**
* Fired when new program events of an ATSC TV channel arrived.
@@ -241,7 +233,7 @@ public class EventDetector {
* @param channel an ATSC TV channel
* @param items a list of EIT items that were received
*/
- void onEventDetected(TunerChannel channel, List<PsipData.EitItem> items);
+ void onEventDetected(TunerChannel channel, List<EitItem> items);
/**
* Fired when information of all detectable ATSC TV channels in current frequency arrived.
@@ -250,21 +242,20 @@ public class EventDetector {
}
/**
- * Creates a detector for ATSC TV channles and program information.
+ * Creates a detector for ATSC TV channels and program information.
*
- * @param usbTunerInteface {@link TunerHal}
+ * @param tunerHal
*/
- public EventDetector(TunerHal usbTunerInteface) {
- mTunerHal = usbTunerInteface;
+ public EventDetector(Tuner tunerHal) {
+ mTunerHal = tunerHal;
}
private void reset() {
// TODO: Use TsParser.reset()
- int deliverySystemType = mTunerHal.getDeliverySystemType();
mTsParser =
new TsParser(
mTsOutputListener,
- TunerHal.isDvbDeliverySystem(mTunerHal.getDeliverySystemType()));
+ Tuner.isDvbDeliverySystem(mTunerHal.getDeliverySystemType()));
mPidSet.clear();
mVctProgramNumberSet.clear();
mSdtProgramNumberSet.clear();
@@ -293,7 +284,7 @@ public class EventDetector {
return;
}
mPidSet.add(pid);
- mTunerHal.addPidFilter(pid, TunerHal.FILTER_TYPE_OTHER);
+ mTunerHal.addPidFilter(pid, Tuner.FILTER_TYPE_OTHER);
}
/**
diff --git a/tuner/src/com/android/tv/tuner/ts/TsParser.java b/tuner/src/com/android/tv/tuner/ts/TsParser.java
index 2307c22a..be46983b 100644
--- a/tuner/src/com/android/tv/tuner/ts/TsParser.java
+++ b/tuner/src/com/android/tv/tuner/ts/TsParser.java
@@ -26,8 +26,9 @@ import com.android.tv.tuner.data.PsipData.EttItem;
import com.android.tv.tuner.data.PsipData.MgtItem;
import com.android.tv.tuner.data.PsipData.SdtItem;
import com.android.tv.tuner.data.PsipData.VctItem;
+import com.android.tv.tuner.data.SectionParser;
+import com.android.tv.tuner.data.SectionParser.OutputListener;
import com.android.tv.tuner.data.TunerChannel;
-import com.android.tv.tuner.ts.SectionParser.OutputListener;
import com.android.tv.tuner.util.ByteArrayBuffer;
import java.util.ArrayList;
import java.util.Arrays;
diff --git a/tuner/src/com/android/tv/tuner/tvinput/AudioCapabilitiesReceiverV1Wrapper.java b/tuner/src/com/android/tv/tuner/tvinput/AudioCapabilitiesReceiverV1Wrapper.java
new file mode 100644
index 00000000..4c35ea43
--- /dev/null
+++ b/tuner/src/com/android/tv/tuner/tvinput/AudioCapabilitiesReceiverV1Wrapper.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.tvinput;
+
+import android.content.Context;
+import android.os.Handler;
+import android.util.Log;
+import com.google.android.exoplayer.audio.AudioCapabilities;
+import com.google.android.exoplayer.audio.AudioCapabilitiesReceiver;
+
+/**
+ * Wraps {@link AudioCapabilitiesReceiver} to support listening for audio capabilities changes on
+ * custom threads.
+ */
+public final class AudioCapabilitiesReceiverV1Wrapper {
+
+ private static final String TAG = "AudioCapabilitiesReceiverV1Wrapper";
+
+ private final AudioCapabilitiesReceiver mAudioCapabilitiesReceiver;
+ private final Handler mHandler;
+ private final AudioCapabilitiesReceiver.Listener mListener;
+ private boolean mRegistered;
+
+ /**
+ * Creates an instance.
+ *
+ * @param context A context for registering the receiver.
+ * @param handler A handler on the which mListener events will be posted.
+ * @param listener The listener to notify when audio capabilities change.
+ */
+ public AudioCapabilitiesReceiverV1Wrapper(
+ Context context, Handler handler, AudioCapabilitiesReceiver.Listener listener) {
+ mAudioCapabilitiesReceiver =
+ new AudioCapabilitiesReceiver(context, this::onAudioCapabilitiesChanged);
+ mHandler = handler;
+ mListener = listener;
+ }
+
+ /** @see AudioCapabilitiesReceiver#register() */
+ public AudioCapabilities register() {
+ mRegistered = true;
+ return mAudioCapabilitiesReceiver.register();
+ }
+
+ /** @see AudioCapabilitiesReceiver#unregister() */
+ public void unregister() {
+ if (mRegistered) {
+ try {
+ mAudioCapabilitiesReceiver.unregister();
+ } catch (IllegalArgumentException e) {
+ // Workaround for b/115739362.
+ Log.e(
+ TAG,
+ "Ignoring exception when unregistering audio capabilities receiver: ",
+ e);
+ }
+ mRegistered = false;
+ } else {
+ Log.e(TAG, "Attempt to unregister a non-registered audio capabilities receiver.");
+ }
+ }
+
+ private void onAudioCapabilitiesChanged(AudioCapabilities audioCapabilities) {
+ mHandler.post(() -> mListener.onAudioCapabilitiesChanged(audioCapabilities));
+ }
+}
diff --git a/tuner/src/com/android/tv/tuner/tvinput/BaseTunerTvInputService.java b/tuner/src/com/android/tv/tuner/tvinput/BaseTunerTvInputService.java
index e577e35e..d22b6399 100644
--- a/tuner/src/com/android/tv/tuner/tvinput/BaseTunerTvInputService.java
+++ b/tuner/src/com/android/tv/tuner/tvinput/BaseTunerTvInputService.java
@@ -23,26 +23,29 @@ import android.content.Context;
import android.media.tv.TvInputService;
import android.util.Log;
import com.android.tv.common.feature.CommonFeatures;
-import com.google.android.exoplayer.audio.AudioCapabilities;
-import com.google.android.exoplayer.audio.AudioCapabilitiesReceiver;
+import com.android.tv.tuner.source.TsDataSourceManager;
+import com.android.tv.tuner.tvinput.datamanager.ChannelDataManager;
+import com.android.tv.tuner.tvinput.factory.TunerSessionFactory;
+import dagger.android.AndroidInjection;
+import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags;
import java.util.Collections;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.TimeUnit;
+import javax.inject.Inject;
/** {@link BaseTunerTvInputService} serves TV channels coming from a tuner device. */
-public class BaseTunerTvInputService extends TvInputService
- implements AudioCapabilitiesReceiver.Listener {
+public class BaseTunerTvInputService extends TvInputService {
private static final String TAG = "BaseTunerTvInputService";
private static final boolean DEBUG = false;
private static final int DVR_STORAGE_CLEANUP_JOB_ID = 100;
- // WeakContainer for {@link TvInputSessionImpl}
- private final Set<TunerSession> mTunerSessions = Collections.newSetFromMap(new WeakHashMap<>());
+ private final Set<Session> mTunerSessions = Collections.newSetFromMap(new WeakHashMap<>());
private ChannelDataManager mChannelDataManager;
- private AudioCapabilitiesReceiver mAudioCapabilitiesReceiver;
- private AudioCapabilities mAudioCapabilities;
+ @Inject ConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags;
+ @Inject TsDataSourceManager.Factory mTsDataSourceManagerFactory;
+ @Inject TunerSessionFactory mTunerSessionFactory;
@Override
public void onCreate() {
@@ -51,11 +54,10 @@ public class BaseTunerTvInputService extends TvInputService
this.stopSelf();
return;
}
+ AndroidInjection.inject(this);
super.onCreate();
if (DEBUG) Log.d(TAG, "onCreate");
mChannelDataManager = new ChannelDataManager(getApplicationContext());
- mAudioCapabilitiesReceiver = new AudioCapabilitiesReceiver(getApplicationContext(), this);
- mAudioCapabilitiesReceiver.register();
if (CommonFeatures.DVR.isEnabled(this)) {
JobScheduler jobScheduler =
(JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);
@@ -80,12 +82,16 @@ public class BaseTunerTvInputService extends TvInputService
if (DEBUG) Log.d(TAG, "onDestroy");
super.onDestroy();
mChannelDataManager.release();
- mAudioCapabilitiesReceiver.unregister();
}
@Override
public RecordingSession onCreateRecordingSession(String inputId) {
- return new TunerRecordingSession(this, inputId, mChannelDataManager);
+ return new TunerRecordingSession(
+ this,
+ inputId,
+ mChannelDataManager,
+ mConcurrentDvrPlaybackFlags,
+ mTsDataSourceManagerFactory);
}
@Override
@@ -93,13 +99,13 @@ public class BaseTunerTvInputService extends TvInputService
if (DEBUG) Log.d(TAG, "onCreateSession");
try {
// TODO(b/65445352): Support multiple TunerSessions for multiple tuners
- if (!allSessionsReleased()) {
+ if (!mTunerSessions.isEmpty()) {
Log.d(TAG, "abort creating an session");
return null;
}
- final TunerSession session = new TunerSession(this, mChannelDataManager);
+ final Session session =
+ mTunerSessionFactory.create(this, mChannelDataManager, this::onReleased);
mTunerSessions.add(session);
- session.setAudioCapabilities(mAudioCapabilities);
session.setOverlayViewEnabled(true);
return session;
} catch (RuntimeException e) {
@@ -109,22 +115,7 @@ public class BaseTunerTvInputService extends TvInputService
}
}
- @Override
- public void onAudioCapabilitiesChanged(AudioCapabilities audioCapabilities) {
- mAudioCapabilities = audioCapabilities;
- for (TunerSession session : mTunerSessions) {
- if (!session.isReleased()) {
- session.setAudioCapabilities(audioCapabilities);
- }
- }
- }
-
- private boolean allSessionsReleased() {
- for (TunerSession session : mTunerSessions) {
- if (!session.isReleased()) {
- return false;
- }
- }
- return true;
+ private void onReleased(Session session) {
+ mTunerSessions.remove(session);
}
}
diff --git a/tuner/src/com/android/tv/tuner/tvinput/TunerRecordingSession.java b/tuner/src/com/android/tv/tuner/tvinput/TunerRecordingSession.java
index a1f0c773..55616931 100644
--- a/tuner/src/com/android/tv/tuner/tvinput/TunerRecordingSession.java
+++ b/tuner/src/com/android/tv/tuner/tvinput/TunerRecordingSession.java
@@ -17,25 +17,38 @@
package com.android.tv.tuner.tvinput;
import android.content.Context;
-import android.media.tv.TvInputService;
import android.net.Uri;
import android.support.annotation.MainThread;
import android.support.annotation.Nullable;
import android.support.annotation.WorkerThread;
import android.util.Log;
+import com.android.tv.common.compat.RecordingSessionCompat;
+import com.android.tv.tuner.source.TsDataSourceManager;
+import com.android.tv.tuner.tvinput.datamanager.ChannelDataManager;
+import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags;
/** Processes DVR recordings, and deletes the previously recorded contents. */
-public class TunerRecordingSession extends TvInputService.RecordingSession {
+public class TunerRecordingSession extends RecordingSessionCompat {
private static final String TAG = "TunerRecordingSession";
private static final boolean DEBUG = false;
private final TunerRecordingSessionWorker mSessionWorker;
public TunerRecordingSession(
- Context context, String inputId, ChannelDataManager channelDataManager) {
+ Context context,
+ String inputId,
+ ChannelDataManager channelDataManager,
+ ConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlags,
+ TsDataSourceManager.Factory tsDataSourceManagerFactory) {
super(context);
mSessionWorker =
- new TunerRecordingSessionWorker(context, inputId, channelDataManager, this);
+ new TunerRecordingSessionWorker(
+ context,
+ inputId,
+ channelDataManager,
+ this,
+ concurrentDvrPlaybackFlags,
+ tsDataSourceManagerFactory);
}
// RecordingSession
@@ -85,6 +98,15 @@ public class TunerRecordingSession extends TvInputService.RecordingSession {
notifyTuned(channelUri);
}
+ // Called from TunerRecordingSessionImpl in a worker thread.
+ @WorkerThread
+ public void onRecordingUri(String recUri) {
+ if (DEBUG) {
+ Log.d(TAG, "Notifying recording session URI." + recUri);
+ }
+ notifyRecordingStarted(recUri);
+ }
+
@WorkerThread
public void onRecordFinished(final Uri recordedProgramUri) {
if (DEBUG) {
diff --git a/tuner/src/com/android/tv/tuner/tvinput/TunerRecordingSessionWorker.java b/tuner/src/com/android/tv/tuner/tvinput/TunerRecordingSessionWorker.java
index b2001225..2c0c09a6 100644
--- a/tuner/src/com/android/tv/tuner/tvinput/TunerRecordingSessionWorker.java
+++ b/tuner/src/com/android/tv/tuner/tvinput/TunerRecordingSessionWorker.java
@@ -16,6 +16,8 @@
package com.android.tv.tuner.tvinput;
+import static com.android.tv.tuner.features.TunerFeatures.TVPROVIDER_ALLOWS_COLUMN_CREATION;
+
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
@@ -26,16 +28,18 @@ import android.media.tv.TvContract.RecordedPrograms;
import android.media.tv.TvInputManager;
import android.net.Uri;
import android.os.AsyncTask;
+import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Message;
import android.support.annotation.IntDef;
import android.support.annotation.MainThread;
import android.support.annotation.Nullable;
-import android.support.media.tv.Program;
import android.util.Log;
import android.util.Pair;
+import androidx.tvprovider.media.tv.Program;
import com.android.tv.common.BaseApplication;
+import com.android.tv.common.data.RecordedProgramState;
import com.android.tv.common.recording.RecordingCapability;
import com.android.tv.common.recording.RecordingStorageStatusManager;
import com.android.tv.common.util.CommonUtils;
@@ -48,23 +52,31 @@ import com.android.tv.tuner.exoplayer.ExoPlayerSampleExtractor;
import com.android.tv.tuner.exoplayer.SampleExtractor;
import com.android.tv.tuner.exoplayer.buffer.BufferManager;
import com.android.tv.tuner.exoplayer.buffer.DvrStorageManager;
+import com.android.tv.tuner.exoplayer.buffer.PlaybackBufferListener;
import com.android.tv.tuner.source.TsDataSource;
import com.android.tv.tuner.source.TsDataSourceManager;
+import com.android.tv.tuner.ts.EventDetector.EventListener;
+import com.android.tv.tuner.tvinput.datamanager.ChannelDataManager;
import com.google.android.exoplayer.C;
+import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags;
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.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Random;
+import java.util.Set;
import java.util.concurrent.TimeUnit;
/** Implements a DVR feature. */
public class TunerRecordingSessionWorker
implements PlaybackBufferListener,
- EventDetector.EventListener,
+ EventListener,
SampleExtractor.OnCompletionListener,
Handler.Callback {
private static final String TAG = "TunerRecordingSessionW";
@@ -87,6 +99,14 @@ public class TunerRecordingSessionWorker
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 static final int MSG_UPDATE_PARTIAL_STATE = 8;
+ private static final String COLUMN_SERIES_ID = "series_id";
+ private static final String COLUMN_STATE = "state";
+
+ private boolean mProgramHasSeriesIdColumn;
+ private boolean mRecordedProgramHasSeriesIdColumn;
+ private boolean mRecordedProgramHasStateColumn;
+
private final RecordingCapability mCapabilities;
private static final String[] PROGRAM_PROJECTION = {
@@ -108,6 +128,9 @@ public class TunerRecordingSessionWorker
TvContract.Programs.COLUMN_INTERNAL_PROVIDER_DATA
};
+ private static final String[] PROGRAM_PROJECTION_WITH_SERIES_ID =
+ createProjectionWithSeriesId();
+
@IntDef({STATE_IDLE, STATE_TUNING, STATE_TUNED, STATE_RECORDING})
@Retention(RetentionPolicy.SOURCE)
public @interface DvrSessionState {}
@@ -119,6 +142,7 @@ public class TunerRecordingSessionWorker
private static final long CHANNEL_ID_NONE = -1;
private static final int MAX_TUNING_RETRY = 6;
+ private final ConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags;
private final Context mContext;
private final ChannelDataManager mChannelDataManager;
@@ -132,12 +156,14 @@ public class TunerRecordingSessionWorker
private File mStorageDir;
private long mRecordStartTime;
private long mRecordEndTime;
+ private Uri mRecordedProgramUri;
private boolean mRecorderRunning;
private SampleExtractor mRecorder;
private final TunerRecordingSession mSession;
@DvrSessionState private int mSessionState = STATE_IDLE;
private final String mInputId;
private Uri mProgramUri;
+ private String mSeriesId;
private PsipData.EitItem mCurrenProgram;
private List<AtscCaptionTrack> mCaptionTracks;
@@ -147,7 +173,10 @@ public class TunerRecordingSessionWorker
Context context,
String inputId,
ChannelDataManager dataManager,
- TunerRecordingSession session) {
+ TunerRecordingSession session,
+ ConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlags,
+ TsDataSourceManager.Factory tsDataSourceManagerFactory) {
+ mConcurrentDvrPlaybackFlags = concurrentDvrPlaybackFlags;
mRandom.setSeed(System.nanoTime());
mContext = context;
HandlerThread handlerThread = new HandlerThread(TAG);
@@ -157,7 +186,7 @@ public class TunerRecordingSessionWorker
BaseApplication.getSingletons(context).getRecordingStorageStatusManager();
mChannelDataManager = dataManager;
mChannelDataManager.checkDataVersion(context);
- mSourceManager = TsDataSourceManager.createSourceManager(true);
+ mSourceManager = tsDataSourceManagerFactory.create(true);
mCapabilities = new DvbDeviceAccessor(context).getRecordingCapability(inputId);
mInputId = inputId;
if (DEBUG) Log.d(TAG, mCapabilities.toString());
@@ -306,6 +335,7 @@ public class TunerRecordingSessionWorker
}
new DeleteRecordingTask().execute(mStorageDir);
mSession.onError(TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE);
+ mContext.getContentResolver().delete(mRecordedProgramUri, null, null);
reset();
} else {
mHandler.sendEmptyMessageDelayed(
@@ -330,6 +360,11 @@ public class TunerRecordingSessionWorker
updateCaptionTracks(pair.first, pair.second);
return true;
}
+ case MSG_UPDATE_PARTIAL_STATE:
+ {
+ updateRecordedProgram(RecordedProgramState.PARTIAL, -1, -1);
+ return true;
+ }
}
return false;
}
@@ -422,17 +457,46 @@ public class TunerRecordingSessionWorker
mDvrStorageManager = new DvrStorageManager(mStorageDir, true);
mRecorder =
new ExoPlayerSampleExtractor(
- Uri.EMPTY, mTunerSource, new BufferManager(mDvrStorageManager), this, true);
+ Uri.EMPTY,
+ mTunerSource,
+ new BufferManager(mDvrStorageManager),
+ this,
+ true,
+ mConcurrentDvrPlaybackFlags);
mRecorder.setOnCompletionListener(this, mHandler);
mProgramUri = programUri;
mSessionState = STATE_RECORDING;
mRecorderRunning = true;
+ if (mConcurrentDvrPlaybackFlags.enabled()) {
+ mRecordedProgramUri =
+ insertRecordedProgram(
+ getRecordedProgram(),
+ mChannel.getChannelId(),
+ Uri.fromFile(mStorageDir).toString(),
+ calculateRecordingSizeInBytes(),
+ mRecordStartTime,
+ mRecordStartTime);
+ if (mRecordedProgramUri == null) {
+ new DeleteRecordingTask().execute(mStorageDir);
+ mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN);
+ Log.e(TAG, "Inserting a recording to DB failed");
+ return false;
+ }
+ mSession.onRecordingUri(mRecordedProgramUri.toString());
+ mHandler.sendEmptyMessageDelayed(
+ MSG_UPDATE_PARTIAL_STATE, MIN_PARTIAL_RECORDING_DURATION_MS);
+ }
mHandler.sendEmptyMessage(MSG_PREPARE_RECODER);
mHandler.removeMessages(MSG_MONITOR_STORAGE_STATUS);
mHandler.sendEmptyMessageDelayed(MSG_MONITOR_STORAGE_STATUS, STORAGE_MONITOR_INTERVAL_MS);
return true;
}
+ private int calculateRecordingSizeInBytes() {
+ // TODO(b/121153491): calcute recording size using mStorageDir
+ return 1024 * 1024;
+ }
+
private void stopRecorder() {
// Do not change session status.
if (mRecorder != null) {
@@ -485,9 +549,15 @@ public class TunerRecordingSessionWorker
long avg = mRecordStartTime / 2 + mRecordEndTime / 2;
programUri = TvContract.buildProgramsUriForChannel(mChannel.getChannelId(), avg, avg);
}
- try (Cursor c = resolver.query(programUri, PROGRAM_PROJECTION, null, null, SORT_BY_TIME)) {
+ String[] projection =
+ checkProgramTable() ? PROGRAM_PROJECTION_WITH_SERIES_ID : PROGRAM_PROJECTION;
+ try (Cursor c = resolver.query(programUri, projection, null, null, SORT_BY_TIME)) {
if (c != null && c.moveToNext()) {
Program result = Program.fromCursor(c);
+ int index;
+ if ((index = c.getColumnIndex(COLUMN_SERIES_ID)) >= 0 && !c.isNull(index)) {
+ mSeriesId = c.getString(index);
+ }
if (DEBUG) {
Log.v(TAG, "Finished query for " + this);
}
@@ -516,9 +586,15 @@ public class TunerRecordingSessionWorker
values.put(RecordedPrograms.COLUMN_RECORDING_DATA_URI, storageUri);
values.put(RecordedPrograms.COLUMN_RECORDING_DURATION_MILLIS, endTime - startTime);
values.put(RecordedPrograms.COLUMN_RECORDING_DATA_BYTES, totalBytes);
- // startTime and endTime could be overridden by program's start and end value.
+ // startTime could be overridden by program's start value.
values.put(RecordedPrograms.COLUMN_START_TIME_UTC_MILLIS, startTime);
values.put(RecordedPrograms.COLUMN_END_TIME_UTC_MILLIS, endTime);
+ if (checkRecordedProgramTable(COLUMN_SERIES_ID)) {
+ values.put(COLUMN_SERIES_ID, mSeriesId);
+ }
+ if (mConcurrentDvrPlaybackFlags.enabled() && checkRecordedProgramTable(COLUMN_STATE)) {
+ values.put(COLUMN_STATE, RecordedProgramState.STARTED.name());
+ }
if (program != null) {
values.putAll(program.toContentValues());
}
@@ -526,6 +602,20 @@ public class TunerRecordingSessionWorker
.insert(TvContract.RecordedPrograms.CONTENT_URI, values);
}
+ private void updateRecordedProgram(RecordedProgramState state, long endTime, long totalBytes) {
+ ContentValues values = new ContentValues();
+ if (checkRecordedProgramTable(COLUMN_STATE)) {
+ values.put(COLUMN_STATE, state.name());
+ }
+ if (state.equals(RecordedProgramState.FINISHED)) {
+ values.put(RecordedPrograms.COLUMN_RECORDING_DATA_BYTES, totalBytes);
+ values.put(
+ RecordedPrograms.COLUMN_RECORDING_DURATION_MILLIS, endTime - mRecordStartTime);
+ values.put(RecordedPrograms.COLUMN_END_TIME_UTC_MILLIS, endTime);
+ }
+ mContext.getContentResolver().update(mRecordedProgramUri, values, null, null);
+ }
+
private void onRecordingResult(boolean success, long lastExtractedPositionUs) {
if (mSessionState != STATE_RECORDING) {
// Error notification is not needed.
@@ -541,6 +631,7 @@ public class TunerRecordingSessionWorker
< TimeUnit.MILLISECONDS.toMicros(MIN_PARTIAL_RECORDING_DURATION_MS)) {
new DeleteRecordingTask().execute(mStorageDir);
mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN);
+ mContext.getContentResolver().delete(mRecordedProgramUri, null, null);
Log.w(TAG, "Recording failed during recording");
return;
}
@@ -549,22 +640,120 @@ public class TunerRecordingSessionWorker
(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;
+ if (!mConcurrentDvrPlaybackFlags.enabled()) {
+ mRecordedProgramUri =
+ insertRecordedProgram(
+ getRecordedProgram(),
+ mChannel.getChannelId(),
+ Uri.fromFile(mStorageDir).toString(),
+ calculateRecordingSizeInBytes(),
+ mRecordStartTime,
+ recordEndTime);
+ if (mRecordedProgramUri == null) {
+ new DeleteRecordingTask().execute(mStorageDir);
+ mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN);
+ Log.e(TAG, "Inserting a recording to DB failed");
+ return;
+ }
+ } else {
+ updateRecordedProgram(
+ RecordedProgramState.FINISHED, recordEndTime, calculateRecordingSizeInBytes());
}
mDvrStorageManager.writeCaptionInfoFiles(mCaptionTracks);
- mSession.onRecordFinished(uri);
+ mSession.onRecordFinished(mRecordedProgramUri);
+ }
+
+ private boolean checkProgramTable() {
+ boolean canCreateColumn = TVPROVIDER_ALLOWS_COLUMN_CREATION.isEnabled(mContext);
+ if (!canCreateColumn) {
+ return false;
+ }
+ Uri uri = TvContract.Programs.CONTENT_URI;
+ if (!mProgramHasSeriesIdColumn) {
+ if (getExistingColumns(uri).contains(COLUMN_SERIES_ID)) {
+ mProgramHasSeriesIdColumn = true;
+ } else if (addColumnToTable(uri, COLUMN_SERIES_ID)) {
+ mProgramHasSeriesIdColumn = true;
+ }
+ }
+ return mProgramHasSeriesIdColumn;
+ }
+
+ private boolean checkRecordedProgramTable(String column) {
+ boolean canCreateColumn = TVPROVIDER_ALLOWS_COLUMN_CREATION.isEnabled(mContext);
+ if (!canCreateColumn) {
+ return false;
+ }
+ Uri uri = TvContract.RecordedPrograms.CONTENT_URI;
+ switch (column) {
+ case COLUMN_SERIES_ID:
+ {
+ if (!mRecordedProgramHasSeriesIdColumn) {
+ if (getExistingColumns(uri).contains(COLUMN_SERIES_ID)) {
+ mRecordedProgramHasSeriesIdColumn = true;
+ } else if (addColumnToTable(uri, COLUMN_SERIES_ID)) {
+ mRecordedProgramHasSeriesIdColumn = true;
+ }
+ }
+ return mRecordedProgramHasSeriesIdColumn;
+ }
+ case COLUMN_STATE:
+ {
+ if (!mRecordedProgramHasStateColumn) {
+ if (getExistingColumns(uri).contains(COLUMN_STATE)) {
+ mRecordedProgramHasStateColumn = true;
+ } else if (addColumnToTable(uri, COLUMN_STATE)) {
+ mRecordedProgramHasStateColumn = true;
+ }
+ }
+ return mRecordedProgramHasStateColumn;
+ }
+ default:
+ return false;
+ }
+ }
+
+ private Set<String> getExistingColumns(Uri uri) {
+ Bundle result =
+ mContext.getContentResolver()
+ .call(uri, TvContract.METHOD_GET_COLUMNS, uri.toString(), null);
+ if (result != null) {
+ String[] columns = result.getStringArray(TvContract.EXTRA_EXISTING_COLUMN_NAMES);
+ if (columns != null) {
+ return new HashSet<>(Arrays.asList(columns));
+ }
+ }
+ Log.e(TAG, "Query existing column names from " + uri + " returned null");
+ return Collections.emptySet();
+ }
+
+ /**
+ * Add a column to the table
+ *
+ * @return {@code true} if the column is added successfully; {@code false} otherwise.
+ */
+ private boolean addColumnToTable(Uri contentUri, String columnName) {
+ Bundle extra = new Bundle();
+ extra.putCharSequence(TvContract.EXTRA_COLUMN_NAME, columnName);
+ extra.putCharSequence(TvContract.EXTRA_DATA_TYPE, "TEXT");
+ // If the add operation fails, the following just returns null without crashing.
+ Bundle allColumns =
+ mContext.getContentResolver()
+ .call(
+ contentUri,
+ TvContract.METHOD_ADD_COLUMN,
+ contentUri.toString(),
+ extra);
+ if (allColumns == null) {
+ Log.w(TAG, "Adding new column failed. Uri=" + contentUri);
+ }
+ return allColumns != null;
+ }
+
+ private static String[] createProjectionWithSeriesId() {
+ List<String> projectionList = new ArrayList<>(Arrays.asList(PROGRAM_PROJECTION));
+ projectionList.add(COLUMN_SERIES_ID);
+ return projectionList.toArray(new String[0]);
}
private static class DeleteRecordingTask extends AsyncTask<File, Void, Void> {
@@ -575,7 +764,9 @@ public class TunerRecordingSessionWorker
return null;
}
for (File file : files) {
- CommonUtils.deleteDirOrFile(file);
+ if (!CommonUtils.deleteDirOrFile(file)) {
+ Log.w(TAG, "Unable to delete recording data at " + file);
+ }
}
return null;
}
diff --git a/tuner/src/com/android/tv/tuner/tvinput/TunerSession.java b/tuner/src/com/android/tv/tuner/tvinput/TunerSession.java
index c9d997f1..fedb5f6b 100644
--- a/tuner/src/com/android/tv/tuner/tvinput/TunerSession.java
+++ b/tuner/src/com/android/tv/tuner/tvinput/TunerSession.java
@@ -21,95 +21,58 @@ import android.content.Context;
import android.media.PlaybackParams;
import android.media.tv.TvContentRating;
import android.media.tv.TvInputManager;
-import android.media.tv.TvInputService;
import android.net.Uri;
import android.os.Build;
-import android.os.Handler;
-import android.os.Message;
import android.os.SystemClock;
-import android.text.Html;
import android.util.Log;
-import android.view.LayoutInflater;
import android.view.Surface;
import android.view.View;
-import android.view.ViewGroup;
-import android.widget.TextView;
-import android.widget.Toast;
import com.android.tv.common.CommonPreferences.CommonPreferencesChangedListener;
-import com.android.tv.common.util.SystemPropertiesProxy;
-import com.android.tv.tuner.R;
-import com.android.tv.tuner.TunerPreferences;
-import com.android.tv.tuner.cc.CaptionLayout;
-import com.android.tv.tuner.cc.CaptionTrackRenderer;
-import com.android.tv.tuner.data.Cea708Data.CaptionEvent;
-import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack;
-import com.android.tv.tuner.util.GlobalSettingsUtils;
-import com.android.tv.tuner.util.StatusTextUtils;
-import com.google.android.exoplayer.audio.AudioCapabilities;
+import com.android.tv.common.compat.TisSessionCompat;
+import com.android.tv.tuner.prefs.TunerPreferences;
+import com.android.tv.tuner.source.TsDataSourceManager;
+import com.android.tv.tuner.tvinput.datamanager.ChannelDataManager;
+import com.android.tv.tuner.tvinput.factory.TunerSessionFactory.SessionReleasedCallback;
+import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags;
/**
- * Provides a tuner TV input session. It handles Overlay UI works. Main tuner input functions are
- * implemented in {@link TunerSessionWorker}.
+ * Provides a tuner TV input session. Main tuner input functions are implemented in {@link
+ * TunerSessionWorker}.
*/
-public class TunerSession extends TvInputService.Session
- implements Handler.Callback, CommonPreferencesChangedListener {
+public class TunerSession extends TisSessionCompat implements CommonPreferencesChangedListener {
+
private static final String TAG = "TunerSession";
private static final boolean DEBUG = false;
- private static final String USBTUNER_SHOW_DEBUG = "persist.tv.tuner.show_debug";
-
- public static final int MSG_UI_SHOW_MESSAGE = 1;
- public static final int MSG_UI_HIDE_MESSAGE = 2;
- public static final int MSG_UI_SHOW_AUDIO_UNPLAYABLE = 3;
- public static final int MSG_UI_HIDE_AUDIO_UNPLAYABLE = 4;
- public static final int MSG_UI_PROCESS_CAPTION_TRACK = 5;
- public static final int MSG_UI_START_CAPTION_TRACK = 6;
- public static final int MSG_UI_STOP_CAPTION_TRACK = 7;
- public static final int MSG_UI_RESET_CAPTION_TRACK = 8;
- public static final int MSG_UI_CLEAR_CAPTION_RENDERER = 9;
- public static final int MSG_UI_SET_STATUS_TEXT = 10;
- public static final int MSG_UI_TOAST_RESCAN_NEEDED = 11;
- private final Context mContext;
- private final Handler mUiHandler;
- private final View mOverlayView;
- private final TextView mMessageView;
- private final TextView mStatusView;
- private final TextView mAudioStatusView;
- private final ViewGroup mMessageLayout;
- private final CaptionTrackRenderer mCaptionTrackRenderer;
+ private final TunerSessionOverlay mTunerSessionOverlay;
private final TunerSessionWorker mSessionWorker;
- private boolean mReleased = false;
+ private final SessionReleasedCallback mReleasedCallback;
private boolean mPlayPaused;
private long mTuneStartTimestamp;
- public TunerSession(Context context, ChannelDataManager channelDataManager) {
+ public TunerSession(
+ Context context,
+ ChannelDataManager channelDataManager,
+ SessionReleasedCallback releasedCallback,
+ ConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlags,
+ TsDataSourceManager.Factory tsDataSourceManagerFactory) {
super(context);
- mContext = context;
- mUiHandler = new Handler(this);
- LayoutInflater inflater =
- (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
- mOverlayView = inflater.inflate(R.layout.ut_overlay_view, null);
- mMessageLayout = (ViewGroup) mOverlayView.findViewById(R.id.message_layout);
- mMessageLayout.setVisibility(View.INVISIBLE);
- mMessageView = (TextView) mOverlayView.findViewById(R.id.message);
- mStatusView = (TextView) mOverlayView.findViewById(R.id.tuner_status);
- boolean showDebug = SystemPropertiesProxy.getBoolean(USBTUNER_SHOW_DEBUG, false);
- mStatusView.setVisibility(showDebug ? View.VISIBLE : View.INVISIBLE);
- mAudioStatusView = (TextView) mOverlayView.findViewById(R.id.audio_status);
- mAudioStatusView.setVisibility(View.INVISIBLE);
- CaptionLayout captionLayout = (CaptionLayout) mOverlayView.findViewById(R.id.caption);
- mCaptionTrackRenderer = new CaptionTrackRenderer(captionLayout);
- mSessionWorker = new TunerSessionWorker(context, channelDataManager, this);
+ mReleasedCallback = releasedCallback;
+ mTunerSessionOverlay = new TunerSessionOverlay(context);
+ mSessionWorker =
+ new TunerSessionWorker(
+ context,
+ channelDataManager,
+ this,
+ mTunerSessionOverlay,
+ concurrentDvrPlaybackFlags,
+ tsDataSourceManagerFactory);
TunerPreferences.setCommonPreferencesChangedListener(this);
}
- public boolean isReleased() {
- return mReleased;
- }
-
@Override
public View onCreateOverlayView() {
- return mOverlayView;
+ return mTunerSessionOverlay.getOverlayView();
}
@Override
@@ -207,16 +170,12 @@ public class TunerSession extends TvInputService.Session
if (DEBUG) {
Log.d(TAG, "onRelease");
}
- mReleased = true;
+ // The session worker needs to be released before the overlay to ensure no messages are
+ // added by the worker after releasing the overlay.
mSessionWorker.release();
- mUiHandler.removeCallbacksAndMessages(null);
+ mTunerSessionOverlay.release();
TunerPreferences.setCommonPreferencesChangedListener(null);
- }
-
- /** Sets {@link AudioCapabilities}. */
- public void setAudioCapabilities(AudioCapabilities audioCapabilities) {
- mSessionWorker.sendMessage(
- TunerSessionWorker.MSG_AUDIO_CAPABILITIES_CHANGED, audioCapabilities);
+ mReleasedCallback.onReleased(this);
}
@Override
@@ -241,99 +200,6 @@ public class TunerSession extends TvInputService.Session
}
}
- public void sendUiMessage(int message) {
- mUiHandler.sendEmptyMessage(message);
- }
-
- public void sendUiMessage(int message, Object object) {
- mUiHandler.obtainMessage(message, object).sendToTarget();
- }
-
- public void sendUiMessage(int message, int arg1, int arg2, Object object) {
- mUiHandler.obtainMessage(message, arg1, arg2, object).sendToTarget();
- }
-
- @Override
- public boolean handleMessage(Message msg) {
- switch (msg.what) {
- case MSG_UI_SHOW_MESSAGE:
- {
- mMessageView.setText((String) msg.obj);
- mMessageLayout.setVisibility(View.VISIBLE);
- return true;
- }
- case MSG_UI_HIDE_MESSAGE:
- {
- mMessageLayout.setVisibility(View.INVISIBLE);
- return true;
- }
- case MSG_UI_SHOW_AUDIO_UNPLAYABLE:
- {
- // Showing message of enabling surround sound only when global surround sound
- // setting is "never".
- final int value =
- GlobalSettingsUtils.getEncodedSurroundOutputSettings(mContext);
- if (value == GlobalSettingsUtils.ENCODED_SURROUND_OUTPUT_NEVER) {
- mAudioStatusView.setText(
- Html.fromHtml(
- StatusTextUtils.getAudioWarningInHTML(
- mContext.getString(
- R.string.ut_surround_sound_disabled))));
- } else {
- mAudioStatusView.setText(
- Html.fromHtml(
- StatusTextUtils.getAudioWarningInHTML(
- mContext.getString(
- R.string
- .audio_passthrough_not_supported))));
- }
- mAudioStatusView.setVisibility(View.VISIBLE);
- return true;
- }
- case MSG_UI_HIDE_AUDIO_UNPLAYABLE:
- {
- mAudioStatusView.setVisibility(View.INVISIBLE);
- return true;
- }
- case MSG_UI_PROCESS_CAPTION_TRACK:
- {
- mCaptionTrackRenderer.processCaptionEvent((CaptionEvent) msg.obj);
- return true;
- }
- case MSG_UI_START_CAPTION_TRACK:
- {
- mCaptionTrackRenderer.start((AtscCaptionTrack) msg.obj);
- return true;
- }
- case MSG_UI_STOP_CAPTION_TRACK:
- {
- mCaptionTrackRenderer.stop();
- return true;
- }
- case MSG_UI_RESET_CAPTION_TRACK:
- {
- mCaptionTrackRenderer.reset();
- return true;
- }
- case MSG_UI_CLEAR_CAPTION_RENDERER:
- {
- mCaptionTrackRenderer.clear();
- return true;
- }
- case MSG_UI_SET_STATUS_TEXT:
- {
- mStatusView.setText((CharSequence) msg.obj);
- return true;
- }
- case MSG_UI_TOAST_RESCAN_NEEDED:
- {
- Toast.makeText(mContext, R.string.ut_rescan_needed, Toast.LENGTH_LONG).show();
- return true;
- }
- }
- return false;
- }
-
@Override
public void onCommonPreferencesChanged() {
mSessionWorker.sendMessage(TunerSessionWorker.MSG_TUNER_PREFERENCES_CHANGED);
diff --git a/tuner/src/com/android/tv/tuner/tvinput/TunerSessionExoV2.java b/tuner/src/com/android/tv/tuner/tvinput/TunerSessionExoV2.java
new file mode 100644
index 00000000..4eca44d6
--- /dev/null
+++ b/tuner/src/com/android/tv/tuner/tvinput/TunerSessionExoV2.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.tvinput;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.media.PlaybackParams;
+import android.media.tv.TvContentRating;
+import android.media.tv.TvInputManager;
+import android.net.Uri;
+import android.os.Build;
+import android.os.SystemClock;
+import android.util.Log;
+import android.view.Surface;
+import android.view.View;
+import com.android.tv.common.CommonPreferences.CommonPreferencesChangedListener;
+import com.android.tv.common.compat.TisSessionCompat;
+import com.android.tv.tuner.prefs.TunerPreferences;
+import com.android.tv.tuner.source.TsDataSourceManager;
+import com.android.tv.tuner.tvinput.datamanager.ChannelDataManager;
+import com.android.tv.tuner.tvinput.factory.TunerSessionFactory.SessionReleasedCallback;
+import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags;
+
+/** Provides a tuner TV input session. */
+public class TunerSessionExoV2 extends TisSessionCompat
+ implements CommonPreferencesChangedListener {
+
+ private static final String TAG = "TunerSessionExoV2";
+ private static final boolean DEBUG = false;
+
+ private final TunerSessionOverlay mTunerSessionOverlay;
+ private final TunerSessionWorkerExoV2 mSessionWorker;
+ private final SessionReleasedCallback mReleasedCallback;
+ private boolean mPlayPaused;
+ private long mTuneStartTimestamp;
+
+ public TunerSessionExoV2(
+ Context context,
+ ChannelDataManager channelDataManager,
+ SessionReleasedCallback releasedCallback,
+ ConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlags,
+ TsDataSourceManager.Factory tsDataSourceManagerFactory) {
+ super(context);
+ mReleasedCallback = releasedCallback;
+ mTunerSessionOverlay = new TunerSessionOverlay(context);
+ mSessionWorker =
+ new TunerSessionWorkerExoV2(
+ context,
+ channelDataManager,
+ this,
+ mTunerSessionOverlay,
+ concurrentDvrPlaybackFlags,
+ tsDataSourceManagerFactory);
+ TunerPreferences.setCommonPreferencesChangedListener(this);
+ }
+
+ @Override
+ public View onCreateOverlayView() {
+ return mTunerSessionOverlay.getOverlayView();
+ }
+
+ @Override
+ public boolean onSelectTrack(int type, String trackId) {
+ mSessionWorker.sendMessage(TunerSessionWorkerExoV2.MSG_SELECT_TRACK, type, 0, trackId);
+ return false;
+ }
+
+ @Override
+ public void onSetCaptionEnabled(boolean enabled) {
+ mSessionWorker.setCaptionEnabled(enabled);
+ }
+
+ @Override
+ public void onSetStreamVolume(float volume) {
+ mSessionWorker.setStreamVolume(volume);
+ }
+
+ @Override
+ public boolean onSetSurface(Surface surface) {
+ mSessionWorker.setSurface(surface);
+ return true;
+ }
+
+ @Override
+ public void onTimeShiftPause() {
+ mSessionWorker.sendMessage(TunerSessionWorkerExoV2.MSG_TIMESHIFT_PAUSE);
+ mPlayPaused = true;
+ }
+
+ @Override
+ public void onTimeShiftResume() {
+ mSessionWorker.sendMessage(TunerSessionWorkerExoV2.MSG_TIMESHIFT_RESUME);
+ mPlayPaused = false;
+ }
+
+ @Override
+ public void onTimeShiftSeekTo(long timeMs) {
+ if (DEBUG) Log.d(TAG, "Timeshift seekTo requested position: " + timeMs / 1000);
+ mSessionWorker.sendMessage(
+ TunerSessionWorkerExoV2.MSG_TIMESHIFT_SEEK_TO, mPlayPaused ? 1 : 0, 0, timeMs);
+ }
+
+ @Override
+ public void onTimeShiftSetPlaybackParams(PlaybackParams params) {
+ mSessionWorker.sendMessage(
+ TunerSessionWorkerExoV2.MSG_TIMESHIFT_SET_PLAYBACKPARAMS, params);
+ }
+
+ @Override
+ public long onTimeShiftGetStartPosition() {
+ return mSessionWorker.getStartPosition();
+ }
+
+ @Override
+ public long onTimeShiftGetCurrentPosition() {
+ return mSessionWorker.getCurrentPosition();
+ }
+
+ @Override
+ public boolean onTune(Uri channelUri) {
+ if (DEBUG) {
+ Log.d(TAG, "onTune to " + channelUri != null ? channelUri.toString() : "");
+ }
+ if (channelUri == null) {
+ Log.w(TAG, "onTune() is failed due to null channelUri.");
+ mSessionWorker.stopTune();
+ return false;
+ }
+ mTuneStartTimestamp = SystemClock.elapsedRealtime();
+ mSessionWorker.tune(channelUri);
+ mPlayPaused = false;
+ return true;
+ }
+
+ @TargetApi(Build.VERSION_CODES.N)
+ @Override
+ public void onTimeShiftPlay(Uri recordUri) {
+ if (recordUri == null) {
+ Log.w(TAG, "onTimeShiftPlay() is failed due to null channelUri.");
+ mSessionWorker.stopTune();
+ return;
+ }
+ mTuneStartTimestamp = SystemClock.elapsedRealtime();
+ mSessionWorker.tune(recordUri);
+ mPlayPaused = false;
+ }
+
+ @Override
+ public void onUnblockContent(TvContentRating unblockedRating) {
+ mSessionWorker.sendMessage(TunerSessionWorkerExoV2.MSG_UNBLOCKED_RATING, unblockedRating);
+ }
+
+ @Override
+ public void onRelease() {
+ if (DEBUG) {
+ Log.d(TAG, "onRelease");
+ }
+ // The session worker needs to be released before the overlay to ensure no messages are
+ // added by the worker after releasing the overlay.
+ mSessionWorker.release();
+ mTunerSessionOverlay.release();
+ TunerPreferences.setCommonPreferencesChangedListener(null);
+ mReleasedCallback.onReleased(this);
+ }
+
+ @Override
+ public void notifyVideoAvailable() {
+ super.notifyVideoAvailable();
+ if (mTuneStartTimestamp != 0) {
+ Log.i(
+ TAG,
+ "[Profiler] Video available in "
+ + (SystemClock.elapsedRealtime() - mTuneStartTimestamp)
+ + " ms");
+ mTuneStartTimestamp = 0;
+ }
+ }
+
+ @Override
+ public void notifyVideoUnavailable(int reason) {
+ super.notifyVideoUnavailable(reason);
+ if (reason != TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING
+ && reason != TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL) {
+ notifyTimeShiftStatusChanged(TvInputManager.TIME_SHIFT_STATUS_UNAVAILABLE);
+ }
+ }
+
+ @Override
+ public void onCommonPreferencesChanged() {
+ mSessionWorker.sendMessage(TunerSessionWorkerExoV2.MSG_TUNER_PREFERENCES_CHANGED);
+ }
+}
diff --git a/tuner/src/com/android/tv/tuner/tvinput/TunerSessionOverlay.java b/tuner/src/com/android/tv/tuner/tvinput/TunerSessionOverlay.java
new file mode 100644
index 00000000..9f21e16a
--- /dev/null
+++ b/tuner/src/com/android/tv/tuner/tvinput/TunerSessionOverlay.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.tvinput;
+
+import android.content.Context;
+import android.media.tv.TvInputService.Session;
+import android.os.Handler;
+import android.os.Message;
+import android.text.Html;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+import android.widget.Toast;
+import com.android.tv.common.util.SystemPropertiesProxy;
+import com.android.tv.tuner.R;
+import com.android.tv.tuner.cc.CaptionLayout;
+import com.android.tv.tuner.cc.CaptionTrackRenderer;
+import com.android.tv.tuner.data.Cea708Data.CaptionEvent;
+import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack;
+import com.android.tv.tuner.util.GlobalSettingsUtils;
+import com.android.tv.tuner.util.StatusTextUtils;
+
+/** Executes {@link Session} overlay changes on the main thread. */
+/* package */ final class TunerSessionOverlay implements Handler.Callback {
+
+ /** Displays the given {@link String} message object in the message view. */
+ public static final int MSG_UI_SHOW_MESSAGE = 1;
+ /** Hides the message view. Does not expect a message object. */
+ public static final int MSG_UI_HIDE_MESSAGE = 2;
+ /**
+ * Displays a message in the audio status view to signal audio is not supported. Does not expect
+ * a message object.
+ */
+ public static final int MSG_UI_SHOW_AUDIO_UNPLAYABLE = 3;
+ /** Hides the audio status view. Does not expect a message object. */
+ public static final int MSG_UI_HIDE_AUDIO_UNPLAYABLE = 4;
+ /** Feeds the given {@link CaptionEvent} message object to the {@link CaptionTrackRenderer}. */
+ public static final int MSG_UI_PROCESS_CAPTION_TRACK = 5;
+ /**
+ * Invokes {@link CaptionTrackRenderer#start(AtscCaptionTrack)} passing the given {@link
+ * AtscCaptionTrack} message object as argument.
+ */
+ public static final int MSG_UI_START_CAPTION_TRACK = 6;
+ /** Invokes {@link CaptionTrackRenderer#stop()}. Does not expect a message object. */
+ public static final int MSG_UI_STOP_CAPTION_TRACK = 7;
+ /** Invokes {@link CaptionTrackRenderer#reset()}. Does not expect a message object. */
+ public static final int MSG_UI_RESET_CAPTION_TRACK = 8;
+ /** Invokes {@link CaptionTrackRenderer#clear()}. Does not expect a message object. */
+ public static final int MSG_UI_CLEAR_CAPTION_RENDERER = 9;
+ /** Displays the given {@link CharSequence} message object in the status view. */
+ public static final int MSG_UI_SET_STATUS_TEXT = 10;
+ /** Displays a toast signalling that a re-scan is required. Does not expect a message object. */
+ public static final int MSG_UI_TOAST_RESCAN_NEEDED = 11;
+
+ private static final String USBTUNER_SHOW_DEBUG = "persist.tv.tuner.show_debug";
+
+ private final Context mContext;
+ private final Handler mHandler;
+ private final View mOverlayView;
+ private final TextView mMessageView;
+ private final TextView mStatusView;
+ private final TextView mAudioStatusView;
+ private final ViewGroup mMessageLayout;
+ private final CaptionTrackRenderer mCaptionTrackRenderer;
+
+ /**
+ * Creates and inflates a {@link Session} overlay from the given context.
+ *
+ * @param context The {@link Context} of the {@link Session}.
+ */
+ public TunerSessionOverlay(Context context) {
+ mContext = context;
+ mHandler = new Handler(this);
+ LayoutInflater inflater =
+ (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ boolean showDebug = SystemPropertiesProxy.getBoolean(USBTUNER_SHOW_DEBUG, false);
+ mOverlayView = inflater.inflate(R.layout.ut_overlay_view, null);
+ mMessageLayout = mOverlayView.findViewById(R.id.message_layout);
+ mMessageLayout.setVisibility(View.INVISIBLE);
+ mMessageView = mOverlayView.findViewById(R.id.message);
+ mStatusView = mOverlayView.findViewById(R.id.tuner_status);
+ mStatusView.setVisibility(showDebug ? View.VISIBLE : View.INVISIBLE);
+ mAudioStatusView = mOverlayView.findViewById(R.id.audio_status);
+ mAudioStatusView.setVisibility(View.INVISIBLE);
+ CaptionLayout captionLayout = mOverlayView.findViewById(R.id.caption);
+ mCaptionTrackRenderer = new CaptionTrackRenderer(captionLayout);
+ }
+
+ /** Clears any pending messages in the message queue. */
+ public void release() {
+ mHandler.removeCallbacksAndMessages(null);
+ }
+
+ /** Returns a {@link View} representation of the overlay. */
+ public View getOverlayView() {
+ return mOverlayView;
+ }
+
+ /**
+ * Posts a message to be handled on the main thread. Only messages that do not expect a message
+ * object may be posted through this method.
+ *
+ * @param message One of the {@code MSG_UI_*} constants.
+ */
+ public void sendUiMessage(int message) {
+ mHandler.sendEmptyMessage(message);
+ }
+
+ /**
+ * Posts a message to be handled on the main thread.
+ *
+ * @param message One of the {@code MSG_UI_*} constants.
+ * @param object The object of the message. The required message object type depends on the
+ * message being posted.
+ */
+ public void sendUiMessage(int message, Object object) {
+ mHandler.obtainMessage(message, object).sendToTarget();
+ }
+
+ @Override
+ public boolean handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_UI_SHOW_MESSAGE:
+ mMessageView.setText((String) msg.obj);
+ mMessageLayout.setVisibility(View.VISIBLE);
+ return true;
+ case MSG_UI_HIDE_MESSAGE:
+ mMessageLayout.setVisibility(View.INVISIBLE);
+ return true;
+ case MSG_UI_SHOW_AUDIO_UNPLAYABLE:
+ // Showing message of enabling surround sound only when global surround sound
+ // setting is "never".
+ final int value = GlobalSettingsUtils.getEncodedSurroundOutputSettings(mContext);
+ if (value == GlobalSettingsUtils.ENCODED_SURROUND_OUTPUT_NEVER) {
+ mAudioStatusView.setText(
+ Html.fromHtml(
+ StatusTextUtils.getAudioWarningInHTML(
+ mContext.getString(
+ R.string.ut_surround_sound_disabled))));
+ } else {
+ mAudioStatusView.setText(
+ Html.fromHtml(
+ StatusTextUtils.getAudioWarningInHTML(
+ mContext.getString(
+ R.string.audio_passthrough_not_supported))));
+ }
+ mAudioStatusView.setVisibility(View.VISIBLE);
+ return true;
+ case MSG_UI_HIDE_AUDIO_UNPLAYABLE:
+ mAudioStatusView.setVisibility(View.INVISIBLE);
+ return true;
+ case MSG_UI_PROCESS_CAPTION_TRACK:
+ mCaptionTrackRenderer.processCaptionEvent((CaptionEvent) msg.obj);
+ return true;
+ case MSG_UI_START_CAPTION_TRACK:
+ mCaptionTrackRenderer.start((AtscCaptionTrack) msg.obj);
+ return true;
+ case MSG_UI_STOP_CAPTION_TRACK:
+ mCaptionTrackRenderer.stop();
+ return true;
+ case MSG_UI_RESET_CAPTION_TRACK:
+ mCaptionTrackRenderer.reset();
+ return true;
+ case MSG_UI_CLEAR_CAPTION_RENDERER:
+ mCaptionTrackRenderer.clear();
+ return true;
+ case MSG_UI_SET_STATUS_TEXT:
+ mStatusView.setText((CharSequence) msg.obj);
+ return true;
+ case MSG_UI_TOAST_RESCAN_NEEDED:
+ Toast.makeText(mContext, R.string.ut_rescan_needed, Toast.LENGTH_LONG).show();
+ return true;
+ default:
+ return false;
+ }
+ }
+}
diff --git a/tuner/src/com/android/tv/tuner/tvinput/TunerSessionWorker.java b/tuner/src/com/android/tv/tuner/tvinput/TunerSessionWorker.java
index 65750e08..d3f9409b 100644
--- a/tuner/src/com/android/tv/tuner/tvinput/TunerSessionWorker.java
+++ b/tuner/src/com/android/tv/tuner/tvinput/TunerSessionWorker.java
@@ -34,6 +34,8 @@ import android.os.Message;
import android.os.SystemClock;
import android.support.annotation.AnyThread;
import android.support.annotation.MainThread;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
import android.support.annotation.WorkerThread;
import android.text.Html;
import android.text.TextUtils;
@@ -45,10 +47,12 @@ import android.view.accessibility.CaptioningManager;
import com.android.tv.common.CommonPreferences.TrickplaySetting;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.common.TvContentRatingCache;
+import com.android.tv.common.compat.TvInputConstantCompat;
import com.android.tv.common.customization.CustomizationManager;
import com.android.tv.common.customization.CustomizationManager.TRICKPLAY_MODE;
+import com.android.tv.common.experiments.Experiments;
+import com.android.tv.common.feature.CommonFeatures;
import com.android.tv.common.util.SystemPropertiesProxy;
-import com.android.tv.tuner.TunerPreferences;
import com.android.tv.tuner.data.Cea708Data;
import com.android.tv.tuner.data.PsipData.EitItem;
import com.android.tv.tuner.data.PsipData.TvTracksInterface;
@@ -61,12 +65,19 @@ import com.android.tv.tuner.exoplayer.MpegTsRendererBuilder;
import com.android.tv.tuner.exoplayer.buffer.BufferManager;
import com.android.tv.tuner.exoplayer.buffer.BufferManager.StorageManager;
import com.android.tv.tuner.exoplayer.buffer.DvrStorageManager;
+import com.android.tv.tuner.exoplayer.buffer.PlaybackBufferListener;
import com.android.tv.tuner.exoplayer.buffer.TrickplayStorageManager;
+import com.android.tv.tuner.prefs.TunerPreferences;
import com.android.tv.tuner.source.TsDataSource;
import com.android.tv.tuner.source.TsDataSourceManager;
+import com.android.tv.tuner.ts.EventDetector.EventListener;
+import com.android.tv.tuner.tvinput.datamanager.ChannelDataManager;
+import com.android.tv.tuner.tvinput.debug.TunerDebug;
import com.android.tv.tuner.util.StatusTextUtils;
import com.google.android.exoplayer.ExoPlayer;
import com.google.android.exoplayer.audio.AudioCapabilities;
+import com.google.common.collect.ImmutableList;
+import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags;
import java.io.File;
import java.util.ArrayList;
import java.util.Iterator;
@@ -84,9 +95,10 @@ public class TunerSessionWorker
implements PlaybackBufferListener,
MpegTsPlayer.VideoEventListener,
MpegTsPlayer.Listener,
- EventDetector.EventListener,
+ EventListener,
ChannelDataManager.ProgramInfoListener,
Handler.Callback {
+
private static final String TAG = "TunerSessionWorker";
private static final boolean DEBUG = false;
private static final boolean ENABLE_PROFILER = true;
@@ -103,14 +115,13 @@ public class TunerSessionWorker
public static final int MSG_TIMESHIFT_RESUME = 5;
public static final int MSG_TIMESHIFT_SEEK_TO = 6;
public static final int MSG_TIMESHIFT_SET_PLAYBACKPARAMS = 7;
- public static final int MSG_AUDIO_CAPABILITIES_CHANGED = 8;
public static final int MSG_UNBLOCKED_RATING = 9;
public static final int MSG_TUNER_PREFERENCES_CHANGED = 10;
// Private messages
- private static final int MSG_TUNE = 1000;
+ @VisibleForTesting protected static final int MSG_TUNE = 1000;
private static final int MSG_RELEASE = 1001;
- private static final int MSG_RETRY_PLAYBACK = 1002;
+ @VisibleForTesting protected static final int MSG_RETRY_PLAYBACK = 1002;
private static final int MSG_START_PLAYBACK = 1003;
private static final int MSG_UPDATE_PROGRAM = 1008;
private static final int MSG_SCHEDULE_OF_PROGRAMS = 1009;
@@ -120,7 +131,7 @@ public class TunerSessionWorker
private static final int MSG_PARENTAL_CONTROLS = 1015;
private static final int MSG_RESCHEDULE_PROGRAMS = 1016;
private static final int MSG_BUFFER_START_TIME_CHANGED = 1017;
- private static final int MSG_CHECK_SIGNAL = 1018;
+ @VisibleForTesting protected static final int MSG_CHECK_SIGNAL = 1018;
private static final int MSG_DISCOVER_CAPTION_SERVICE_NUMBER = 1019;
private static final int MSG_RESET_PLAYBACK = 1020;
private static final int MSG_BUFFER_STATE_CHANGED = 1021;
@@ -128,6 +139,7 @@ public class TunerSessionWorker
private static final int MSG_STOP_TUNE = 1023;
private static final int MSG_SET_SURFACE = 1024;
private static final int MSG_NOTIFY_AUDIO_TRACK_UPDATED = 1025;
+ @VisibleForTesting protected static final int MSG_CHECK_SIGNAL_STRENGTH = 1026;
private static final int TS_PACKET_SIZE = 188;
private static final int CHECK_NO_SIGNAL_INITIAL_DELAY_MS = 4000;
@@ -137,6 +149,7 @@ public class TunerSessionWorker
private static final int RESCHEDULE_PROGRAMS_INITIAL_DELAY_MS = 4000;
private static final int RESCHEDULE_PROGRAMS_INTERVAL_MS = 10000;
private static final int RESCHEDULE_PROGRAMS_TOLERANCE_MS = 2000;
+ private static final int CHECK_SIGNAL_STRENGTH_INTERVAL_MS = 5000;
// The following 3s is defined empirically. This should be larger than 2s considering video
// key frame interval in the TS stream.
private static final int PLAYBACK_STATE_CHANGED_WAITING_THRESHOLD_MS = 3000;
@@ -162,6 +175,8 @@ public class TunerSessionWorker
private static final int TRICKPLAY_MONITOR_INTERVAL_MS = 250;
private static final int RELEASE_WAIT_INTERVAL_MS = 50;
private static final long TRICKPLAY_OFF_DURATION_MS = TimeUnit.DAYS.toMillis(14);
+ private static final long SEEK_MARGIN_MS = TimeUnit.SECONDS.toMillis(2);
+ public static final ImmutableList<TvContentRating> NO_CONTENT_RATINGS = ImmutableList.of();
// Since release() is done asynchronously, synchronization between multiple TunerSessionWorker
// creation/release is required.
@@ -202,10 +217,12 @@ public class TunerSessionWorker
private boolean mChannelBlocked;
private TvContentRating mUnblockedContentRating;
private long mLastPositionMs;
+ private final AudioCapabilitiesReceiverV1Wrapper mAudioCapabilitiesReceiver;
private AudioCapabilities mAudioCapabilities;
private long mLastLimitInBytes;
private final TvContentRatingCache mTvContentRatingCache = TvContentRatingCache.getInstance();
private final TunerSession mSession;
+ private final TunerSessionOverlay mTunerSessionOverlay;
private final boolean mHasSoftwareAudioDecoder;
private int mPlayerState = ExoPlayer.STATE_IDLE;
private long mPreparingStartTimeMs;
@@ -214,24 +231,62 @@ public class TunerSessionWorker
private boolean mIsActiveSession;
private boolean mReleaseRequested; // Guarded by mReleaseLock
private final Object mReleaseLock = new Object();
+ private final ConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags;
+
+ private int mSignalStrength;
+ private long mRecordedProgramStartTimeMs;
public TunerSessionWorker(
- Context context, ChannelDataManager channelDataManager, TunerSession tunerSession) {
+ Context context,
+ ChannelDataManager channelDataManager,
+ TunerSession tunerSession,
+ TunerSessionOverlay tunerSessionOverlay,
+ ConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlags,
+ TsDataSourceManager.Factory tsDataSourceManagerFactory) {
+ this(
+ context,
+ channelDataManager,
+ tunerSession,
+ tunerSessionOverlay,
+ null,
+ concurrentDvrPlaybackFlags,
+ tsDataSourceManagerFactory);
+ }
+
+ @VisibleForTesting
+ protected TunerSessionWorker(
+ Context context,
+ ChannelDataManager channelDataManager,
+ TunerSession tunerSession,
+ TunerSessionOverlay tunerSessionOverlay,
+ @Nullable Handler handler,
+ ConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlags,
+ TsDataSourceManager.Factory tsDataSourceManagerFactory) {
+ this.mConcurrentDvrPlaybackFlags = concurrentDvrPlaybackFlags;
if (DEBUG) Log.d(TAG, "TunerSessionWorker created");
mContext = context;
-
- // HandlerThread should be set up before it is registered as a listener in the all other
- // components.
- HandlerThread handlerThread = new HandlerThread(TAG);
- handlerThread.start();
- mHandler = new Handler(handlerThread.getLooper(), this);
+ if (handler != null) {
+ mHandler = handler;
+ } else {
+ // HandlerThread should be set up before it is registered as a listener in the all other
+ // components.
+ HandlerThread handlerThread = new HandlerThread(TAG);
+ handlerThread.start();
+ mHandler = new Handler(handlerThread.getLooper(), this);
+ }
mSession = tunerSession;
+ mTunerSessionOverlay = tunerSessionOverlay;
mChannelDataManager = channelDataManager;
mChannelDataManager.setListener(this);
mChannelDataManager.checkDataVersion(mContext);
- mSourceManager = TsDataSourceManager.createSourceManager(false);
+ mSourceManager = tsDataSourceManagerFactory.create(false);
mTvInputManager = (TvInputManager) context.getSystemService(Context.TV_INPUT_SERVICE);
mTvTracks = new ArrayList<>();
+ mAudioCapabilitiesReceiver =
+ new AudioCapabilitiesReceiverV1Wrapper(
+ context, mHandler, this::handleMessageAudioCapabilitiesChanged);
+ AudioCapabilities audioCapabilities = mAudioCapabilitiesReceiver.register();
+ mHandler.post(() -> handleMessageAudioCapabilitiesChanged(audioCapabilities));
mAudioTrackMap = new SparseArray<>();
mCaptionTrackMap = new SparseArray<>();
CaptioningManager captioningManager =
@@ -401,6 +456,7 @@ public class TunerSessionWorker
// TODO reimplement for google3
// Here disconnect ffmpeg
}
+ mAudioCapabilitiesReceiver.unregister();
mChannelDataManager.setListener(null);
mHandler.removeCallbacksAndMessages(null);
mHandler.sendEmptyMessage(MSG_RELEASE);
@@ -509,18 +565,18 @@ public class TunerSessionWorker
return;
}
Log.i(TAG, "AC3 audio cannot be played due to device limitation");
- mSession.sendUiMessage(TunerSession.MSG_UI_SHOW_AUDIO_UNPLAYABLE);
+ mTunerSessionOverlay.sendUiMessage(TunerSessionOverlay.MSG_UI_SHOW_AUDIO_UNPLAYABLE);
}
// MpegTsPlayer.VideoEventListener
@Override
public void onEmitCaptionEvent(Cea708Data.CaptionEvent event) {
- mSession.sendUiMessage(TunerSession.MSG_UI_PROCESS_CAPTION_TRACK, event);
+ mTunerSessionOverlay.sendUiMessage(TunerSessionOverlay.MSG_UI_PROCESS_CAPTION_TRACK, event);
}
@Override
public void onClearCaptionEvent() {
- mSession.sendUiMessage(TunerSession.MSG_UI_CLEAR_CAPTION_RENDERER);
+ mTunerSessionOverlay.sendUiMessage(TunerSessionOverlay.MSG_UI_CLEAR_CAPTION_RENDERER);
}
@Override
@@ -541,7 +597,7 @@ public class TunerSessionWorker
@Override
public void onRescanNeeded() {
- mSession.sendUiMessage(TunerSession.MSG_UI_TOAST_RESCAN_NEEDED);
+ mTunerSessionOverlay.sendUiMessage(TunerSessionOverlay.MSG_UI_TOAST_RESCAN_NEEDED);
}
@Override
@@ -596,10 +652,12 @@ public class TunerSessionWorker
private static class RecordedProgram {
// private final long mChannelId;
private final String mDataUri;
+ private final long mStartTimeMillis;
private static final String[] PROJECTION = {
TvContract.Programs.COLUMN_CHANNEL_ID,
TvContract.RecordedPrograms.COLUMN_RECORDING_DATA_URI,
+ TvContract.RecordedPrograms.COLUMN_START_TIME_UTC_MILLIS,
};
public RecordedProgram(Cursor cursor) {
@@ -607,11 +665,13 @@ public class TunerSessionWorker
// mChannelId = cursor.getLong(index++);
index++;
mDataUri = cursor.getString(index++);
+ mStartTimeMillis = cursor.getLong(index++);
}
public RecordedProgram(long channelId, String dataUri) {
// mChannelId = channelId;
mDataUri = dataUri;
+ mStartTimeMillis = 0;
}
public static RecordedProgram onQuery(Cursor c) {
@@ -625,6 +685,10 @@ public class TunerSessionWorker
public String getDataUri() {
return mDataUri;
}
+
+ public long getStartTime() {
+ return mStartTimeMillis;
+ }
}
private RecordedProgram getRecordedProgram(Uri recordedUri) {
@@ -650,6 +714,7 @@ public class TunerSessionWorker
private String parseRecording(Uri uri) {
RecordedProgram recording = getRecordedProgram(uri);
if (recording != null) {
+ mRecordedProgramStartTimeMs = recording.getStartTime();
return recording.getDataUri();
}
return null;
@@ -659,514 +724,630 @@ public class TunerSessionWorker
public boolean handleMessage(Message msg) {
switch (msg.what) {
case MSG_TUNE:
- {
- if (DEBUG) Log.d(TAG, "MSG_TUNE");
-
- // When sequential tuning messages arrived, it skips middle tuning messages in
- // order
- // to change to the last requested channel quickly.
- if (mHandler.hasMessages(MSG_TUNE)) {
- return true;
- }
- notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING);
- if (!mIsActiveSession) {
- // Wait until release is finished if there is a pending release.
- try {
- while (!sActiveSessionSemaphore.tryAcquire(
- RELEASE_WAIT_INTERVAL_MS, TimeUnit.MILLISECONDS)) {
- synchronized (mReleaseLock) {
- if (mReleaseRequested) {
- return true;
- }
- }
- }
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- }
- synchronized (mReleaseLock) {
- if (mReleaseRequested) {
- sActiveSessionSemaphore.release();
- return true;
- }
- }
- mIsActiveSession = true;
- }
- Uri channelUri = (Uri) msg.obj;
- String recording = null;
- long channelId = parseChannel(channelUri);
- TunerChannel channel =
- (channelId == -1) ? null : mChannelDataManager.getChannel(channelId);
- if (channelId == -1) {
- recording = parseRecording(channelUri);
- }
- if (channel == null && recording == null) {
- Log.w(TAG, "onTune() is failed. Can't find channel for " + channelUri);
- stopTune();
- notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN);
- return true;
- }
- clearCallbacksAndMessagesSafely();
- mChannelDataManager.removeAllCallbacksAndMessages();
- if (channel != null) {
- if (mTvInputManager.isParentalControlsEnabled() && channel.isLocked()) {
- Log.i(TAG, "onTune() is failed. Channel is blocked" + channel);
- mSession.notifyContentBlocked(TvContentRating.UNRATED);
- return true;
- }
- mChannelDataManager.requestProgramsData(channel);
- }
- prepareTune(channel, recording);
- // TODO: Need to refactor. notifyContentAllowed() should not be called if
- // parental
- // control is turned on.
- mSession.notifyContentAllowed();
- resetTvTracks();
- resetPlayback();
- mHandler.sendEmptyMessageDelayed(
- MSG_RESCHEDULE_PROGRAMS, RESCHEDULE_PROGRAMS_INITIAL_DELAY_MS);
- return true;
- }
+ return handleMessageTune((Uri) msg.obj);
case MSG_STOP_TUNE:
- {
- if (DEBUG) Log.d(TAG, "MSG_STOP_TUNE");
- mChannel = null;
- stopPlayback(true);
- stopCaptionTrack();
- resetTvTracks();
- notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN);
- return true;
- }
+ return handleMessageStopTune();
case MSG_RELEASE:
- {
- if (DEBUG) Log.d(TAG, "MSG_RELEASE");
- mHandler.removeCallbacksAndMessages(null);
- stopPlayback(true);
- stopCaptionTrack();
- mSourceManager.release();
- mHandler.getLooper().quitSafely();
- if (mIsActiveSession) {
- sActiveSessionSemaphore.release();
- }
- return true;
- }
+ return handleMessageRelease();
case MSG_RETRY_PLAYBACK:
- {
- if (System.identityHashCode(mPlayer) == (int) msg.obj) {
- Log.i(TAG, "Retrying the playback for channel: " + mChannel);
- mHandler.removeMessages(MSG_RETRY_PLAYBACK);
- // When there is a request of retrying playback, don't reuse TunerHal.
- mSourceManager.setKeepTuneStatus(false);
- mRetryCount++;
- if (DEBUG) {
- Log.d(TAG, "MSG_RETRY_PLAYBACK " + mRetryCount);
- }
- mChannelDataManager.removeAllCallbacksAndMessages();
- if (mRetryCount <= MAX_IMMEDIATE_RETRY_COUNT) {
- resetPlayback();
- } else {
- // When it reaches this point, it may be due to an error that occurred
- // in
- // the tuner device. Calling stopPlayback() resets the tuner device
- // to recover from the error.
- stopPlayback(false);
- stopCaptionTrack();
-
- notifyVideoUnavailable(
- TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL);
- Log.i(TAG, "Notify weak signal since fail to retry playback");
-
- // After MAX_IMMEDIATE_RETRY_COUNT, give some delay of an empirically
- // chosen
- // value before recovering the playback.
- mHandler.sendEmptyMessageDelayed(
- MSG_RESET_PLAYBACK, RECOVER_STOPPED_PLAYBACK_PERIOD_MS);
- }
- }
- return true;
- }
+ return handleMessageRetryPlayback((int) msg.obj);
case MSG_RESET_PLAYBACK:
- {
- if (DEBUG) Log.d(TAG, "MSG_RESET_PLAYBACK");
- mChannelDataManager.removeAllCallbacksAndMessages();
- resetPlayback();
- return true;
- }
+ return handleMessageResetPlayback();
case MSG_START_PLAYBACK:
- {
- if (DEBUG) Log.d(TAG, "MSG_START_PLAYBACK");
- if (mChannel != null || mRecordingId != null) {
- startPlayback((int) msg.obj);
- }
- return true;
- }
+ return handleMessageStartPlayback((int) msg.obj);
case MSG_UPDATE_PROGRAM:
- {
- if (mChannel != null) {
- EitItem program = (EitItem) msg.obj;
- updateTvTracks(program, false);
- mHandler.sendEmptyMessage(MSG_PARENTAL_CONTROLS);
- }
- return true;
- }
+ return handleMessageUpdateProgram((EitItem) msg.obj);
case MSG_SCHEDULE_OF_PROGRAMS:
- {
- mHandler.removeMessages(MSG_UPDATE_PROGRAM);
- Pair<TunerChannel, List<EitItem>> pair =
- (Pair<TunerChannel, List<EitItem>>) msg.obj;
- TunerChannel channel = pair.first;
- if (mChannel == null) {
- return true;
- }
- if (mChannel != null && mChannel.compareTo(channel) != 0) {
- return true;
- }
- mPrograms = pair.second;
- EitItem currentProgram = getCurrentProgram();
- if (currentProgram == null) {
- mProgram = null;
- }
- long currentTimeMs = getCurrentPosition();
- if (mPrograms != null) {
- for (EitItem item : mPrograms) {
- if (currentProgram != null && currentProgram.compareTo(item) == 0) {
- if (DEBUG) {
- Log.d(TAG, "Update current TvTracks " + item);
- }
- if (mProgram != null && mProgram.compareTo(item) == 0) {
- continue;
- }
- mProgram = item;
- updateTvTracks(item, false);
- } else if (item.getStartTimeUtcMillis() > currentTimeMs) {
- if (DEBUG) {
- Log.d(
- TAG,
- "Update next TvTracks "
- + item
- + " "
- + (item.getStartTimeUtcMillis()
- - currentTimeMs));
- }
- mHandler.sendMessageDelayed(
- mHandler.obtainMessage(MSG_UPDATE_PROGRAM, item),
- item.getStartTimeUtcMillis() - currentTimeMs);
- }
- }
- }
- mHandler.sendEmptyMessage(MSG_PARENTAL_CONTROLS);
- return true;
- }
+ // TODO: fix the unchecked cast waring.
+ Pair<TunerChannel, List<EitItem>> pair =
+ (Pair<TunerChannel, List<EitItem>>) msg.obj;
+ return handleMessageScheduleOfPrograms(pair);
case MSG_UPDATE_CHANNEL_INFO:
- {
- TunerChannel channel = (TunerChannel) msg.obj;
- if (mChannel != null && mChannel.compareTo(channel) == 0) {
- updateChannelInfo(channel);
- }
- return true;
- }
+ return handleMessageUpdateChannelInfo((TunerChannel) msg.obj);
case MSG_PROGRAM_DATA_RESULT:
- {
- TunerChannel channel = (TunerChannel) ((Pair) msg.obj).first;
-
- // If there already exists, skip it since real-time data is a top priority,
- if (mChannel != null
- && mChannel.compareTo(channel) == 0
- && mPrograms == null
- && mProgram == null) {
- sendMessage(MSG_SCHEDULE_OF_PROGRAMS, msg.obj);
- }
- return true;
- }
+ return handleMessageProgramDataResult(msg);
case MSG_TRICKPLAY_BY_SEEK:
- {
- if (mPlayer == null) {
- return true;
- }
- doTrickplayBySeek(msg.arg1);
- return true;
- }
+ return handleMessageTrickplayBySeek(msg.arg1);
case MSG_SMOOTH_TRICKPLAY_MONITOR:
- {
- if (mPlayer == null) {
- return true;
- }
- long systemCurrentTime = System.currentTimeMillis();
- long position = getCurrentPosition();
- if (mRecordingId == null) {
- // Checks if the position exceeds the upper bound when forwarding,
- // or exceed the lower bound when rewinding.
- // If the direction is not checked, there can be some issues.
- // (See b/29939781 for more details.)
- if ((position > systemCurrentTime && mPlaybackParams.getSpeed() > 0L)
- || (position < mBufferStartTimeMs
- && mPlaybackParams.getSpeed() < 0L)) {
- doTimeShiftResume();
- return true;
- }
- } else {
- if (position > mRecordingDuration || position < 0) {
- doTimeShiftPause();
- return true;
- }
- }
- mHandler.sendEmptyMessageDelayed(
- MSG_SMOOTH_TRICKPLAY_MONITOR, TRICKPLAY_MONITOR_INTERVAL_MS);
- return true;
- }
+ return handleMessageSmoothTrickplayMonitor();
case MSG_RESCHEDULE_PROGRAMS:
- {
- if (mHandler.hasMessages(MSG_SCHEDULE_OF_PROGRAMS)) {
- mHandler.sendEmptyMessage(MSG_RESCHEDULE_PROGRAMS);
- } else {
- doReschedulePrograms();
- }
- return true;
- }
+ return handleMessageReschedulePrograms();
case MSG_PARENTAL_CONTROLS:
- {
- doParentalControls();
- mHandler.removeMessages(MSG_PARENTAL_CONTROLS);
- mHandler.sendEmptyMessageDelayed(
- MSG_PARENTAL_CONTROLS, PARENTAL_CONTROLS_INTERVAL_MS);
- return true;
- }
+ return handleMessageParentalControl();
case MSG_UNBLOCKED_RATING:
- {
- mUnblockedContentRating = (TvContentRating) msg.obj;
- doParentalControls();
- mHandler.removeMessages(MSG_PARENTAL_CONTROLS);
- mHandler.sendEmptyMessageDelayed(
- MSG_PARENTAL_CONTROLS, PARENTAL_CONTROLS_INTERVAL_MS);
- return true;
- }
+ return handleMessageUnblockedRating((TvContentRating) msg.obj);
case MSG_DISCOVER_CAPTION_SERVICE_NUMBER:
- {
- int serviceNumber = (int) msg.obj;
- doDiscoverCaptionServiceNumber(serviceNumber);
- return true;
- }
+ return handleMessageDiscoverCaptionServiceNumber((int) msg.obj);
case MSG_SELECT_TRACK:
- {
- if (mPlayer == null) {
- Log.w(TAG, "mPlayer is null when doselectTrack is called");
- return false;
- }
- if (mChannel != null || mRecordingId != null) {
- doSelectTrack(msg.arg1, (String) msg.obj);
- }
- return true;
- }
+ return handleMessageSelectTrack(msg.arg1, (String) msg.obj);
case MSG_UPDATE_CAPTION_TRACK:
- {
- if (mCaptionEnabled) {
- startCaptionTrack();
- } else {
- stopCaptionTrack();
- }
- return true;
- }
+ return handleMessageUpdateCaptionTrack();
case MSG_TIMESHIFT_PAUSE:
- {
- if (DEBUG) Log.d(TAG, "MSG_TIMESHIFT_PAUSE");
- if (mPlayer == null) {
- return true;
- }
- setTrickplayEnabledIfNeeded();
- doTimeShiftPause();
- return true;
- }
+ return handleMessageTimeshiftPause();
case MSG_TIMESHIFT_RESUME:
- {
- if (DEBUG) Log.d(TAG, "MSG_TIMESHIFT_RESUME");
- if (mPlayer == null) {
- return true;
- }
- setTrickplayEnabledIfNeeded();
- doTimeShiftResume();
- return true;
- }
+ return handleMessageTimeshiftResume();
case MSG_TIMESHIFT_SEEK_TO:
- {
- long position = (long) msg.obj;
- if (DEBUG) Log.d(TAG, "MSG_TIMESHIFT_SEEK_TO (position=" + position + ")");
- if (mPlayer == null) {
- return true;
- }
- setTrickplayEnabledIfNeeded();
- doTimeShiftSeekTo(position);
- return true;
- }
+ return handleMessageTimeshiftSeekTo((long) msg.obj);
case MSG_TIMESHIFT_SET_PLAYBACKPARAMS:
- {
- if (mPlayer == null) {
- return true;
- }
- setTrickplayEnabledIfNeeded();
- doTimeShiftSetPlaybackParams((PlaybackParams) msg.obj);
- return true;
- }
- case MSG_AUDIO_CAPABILITIES_CHANGED:
- {
- AudioCapabilities capabilities = (AudioCapabilities) msg.obj;
- if (DEBUG) {
- Log.d(TAG, "MSG_AUDIO_CAPABILITIES_CHANGED " + capabilities);
- }
- if (capabilities == null) {
- return true;
- }
- if (!capabilities.equals(mAudioCapabilities)) {
- // HDMI supported encodings are changed. restart player.
- mAudioCapabilities = capabilities;
- resetPlayback();
- }
- return true;
- }
+ return handleMessageTimeshiftSetPlaybackParams((PlaybackParams) msg.obj);
case MSG_SET_STREAM_VOLUME:
- {
- if (mPlayer != null && mPlayer.isPlaying()) {
- mPlayer.setVolume(mVolume);
- }
- return true;
- }
+ return handleMessageSetStreamVolume();
case MSG_TUNER_PREFERENCES_CHANGED:
- {
- mHandler.removeMessages(MSG_TUNER_PREFERENCES_CHANGED);
- @TrickplaySetting
- int trickplaySetting = TunerPreferences.getTrickplaySetting(mContext);
- if (trickplaySetting != mTrickplaySetting) {
- boolean wasTrcikplayEnabled =
- mTrickplaySetting != TunerPreferences.TRICKPLAY_SETTING_DISABLED;
- boolean isTrickplayEnabled =
- trickplaySetting != TunerPreferences.TRICKPLAY_SETTING_DISABLED;
- mTrickplaySetting = trickplaySetting;
- if (isTrickplayEnabled != wasTrcikplayEnabled) {
- sendMessage(MSG_RESET_PLAYBACK, System.identityHashCode(mPlayer));
- }
- }
- return true;
- }
+ return handleMessageTunerPreferencesChanged();
case MSG_BUFFER_START_TIME_CHANGED:
- {
- if (mPlayer == null) {
- return true;
- }
- mBufferStartTimeMs = (long) msg.obj;
- if (!hasEnoughBackwardBuffer()
- && (!mPlayer.isPlaying() || mPlaybackParams.getSpeed() < 1.0f)) {
- mPlayer.setPlayWhenReady(true);
- mPlayer.setAudioTrackAndClosedCaption(true);
- mPlaybackParams.setSpeed(1.0f);
- }
- return true;
- }
+ return handleMessageBufferStartTimeChanged((long) msg.obj);
case MSG_BUFFER_STATE_CHANGED:
- {
- boolean available = (boolean) msg.obj;
- mSession.notifyTimeShiftStatusChanged(
- available
- ? TvInputManager.TIME_SHIFT_STATUS_AVAILABLE
- : TvInputManager.TIME_SHIFT_STATUS_UNAVAILABLE);
- return true;
- }
+ return handleMessageBufferStateChanged((boolean) msg.obj);
case MSG_CHECK_SIGNAL:
- if (mChannel == null || mPlayer == null) {
- return true;
+ return handleMessageCheckSignal();
+ case MSG_SET_SURFACE:
+ return handleMessageSetSurface();
+ case MSG_NOTIFY_AUDIO_TRACK_UPDATED:
+ return handleMessageAudioTrackUpdated();
+ case MSG_CHECK_SIGNAL_STRENGTH:
+ return handleMessageCheckSignalStrength();
+ default:
+ return unhandledMessage(msg);
+ }
+ }
+
+ private boolean handleMessageTune(Uri channelUri) {
+ if (DEBUG) {
+ Log.d(TAG, "MSG_TUNE");
+ }
+
+ // When sequential tuning messages arrived, it skips middle tuning messages in
+ // order
+ // to change to the last requested channel quickly.
+ if (mHandler.hasMessages(MSG_TUNE)) {
+ return true;
+ }
+ notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING);
+ if (!mIsActiveSession) {
+ // Wait until release is finished if there is a pending release.
+ try {
+ while (!sActiveSessionSemaphore.tryAcquire(
+ RELEASE_WAIT_INTERVAL_MS, TimeUnit.MILLISECONDS)) {
+ synchronized (mReleaseLock) {
+ if (mReleaseRequested) {
+ return true;
+ }
+ }
}
- TsDataSource source = mPlayer.getDataSource();
- long limitInBytes = source != null ? source.getBufferedPosition() : 0L;
- if (TunerDebug.ENABLED) {
- TunerDebug.calculateDiff();
- mSession.sendUiMessage(
- TunerSession.MSG_UI_SET_STATUS_TEXT,
- Html.fromHtml(
- StatusTextUtils.getStatusWarningInHTML(
- (limitInBytes - mLastLimitInBytes) / TS_PACKET_SIZE,
- TunerDebug.getVideoFrameDrop(),
- TunerDebug.getBytesInQueue(),
- TunerDebug.getAudioPositionUs(),
- TunerDebug.getAudioPositionUsRate(),
- TunerDebug.getAudioPtsUs(),
- TunerDebug.getAudioPtsUsRate(),
- TunerDebug.getVideoPtsUs(),
- TunerDebug.getVideoPtsUsRate())));
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ synchronized (mReleaseLock) {
+ if (mReleaseRequested) {
+ sActiveSessionSemaphore.release();
+ return true;
}
- mSession.sendUiMessage(TunerSession.MSG_UI_HIDE_MESSAGE);
- long currentTime = SystemClock.elapsedRealtime();
- long bufferingTimeMs =
- mBufferingStartTimeMs != INVALID_TIME
- ? currentTime - mBufferingStartTimeMs
- : mBufferingStartTimeMs;
- long preparingTimeMs =
- mPreparingStartTimeMs != INVALID_TIME
- ? currentTime - mPreparingStartTimeMs
- : mPreparingStartTimeMs;
- boolean isBufferingTooLong =
- bufferingTimeMs > PLAYBACK_STATE_CHANGED_WAITING_THRESHOLD_MS;
- boolean isPreparingTooLong =
- preparingTimeMs > PLAYBACK_STATE_CHANGED_WAITING_THRESHOLD_MS;
- boolean isWeakSignal =
- source != null
- && mChannel.getType() != Channel.TunerType.TYPE_FILE
- && (isBufferingTooLong || isPreparingTooLong);
- if (isWeakSignal && !mReportedWeakSignal) {
- if (!mHandler.hasMessages(MSG_RETRY_PLAYBACK)) {
- mHandler.sendMessageDelayed(
- mHandler.obtainMessage(
- MSG_RETRY_PLAYBACK, System.identityHashCode(mPlayer)),
- PLAYBACK_RETRY_DELAY_MS);
+ }
+ mIsActiveSession = true;
+ }
+ String recording = null;
+ long channelId = parseChannel(channelUri);
+ TunerChannel channel = (channelId == -1) ? null : mChannelDataManager.getChannel(channelId);
+ if (channelId == -1) {
+ recording = parseRecording(channelUri);
+ }
+ if (channel == null && recording == null) {
+ Log.w(TAG, "onTune() is failed. Can't find channel for " + channelUri);
+ stopTune();
+ notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN);
+ return true;
+ }
+ clearCallbacksAndMessagesSafely();
+ mChannelDataManager.removeAllCallbacksAndMessages();
+ if (channel != null) {
+ mChannelDataManager.requestProgramsData(channel);
+ }
+ prepareTune(channel, recording);
+ // TODO: Need to refactor. notifyContentAllowed() should not be called if
+ // parental
+ // control is turned on.
+ mSession.notifyContentAllowed();
+ resetTvTracks();
+ resetPlayback();
+ mHandler.sendEmptyMessageDelayed(
+ MSG_RESCHEDULE_PROGRAMS, RESCHEDULE_PROGRAMS_INITIAL_DELAY_MS);
+ return true;
+ }
+
+ private boolean handleMessageStopTune() {
+ if (DEBUG) {
+ Log.d(TAG, "MSG_STOP_TUNE");
+ }
+ mChannel = null;
+ stopPlayback(true);
+ stopCaptionTrack();
+ resetTvTracks();
+ notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN);
+ return true;
+ }
+
+ private boolean handleMessageRelease() {
+ if (DEBUG) {
+ Log.d(TAG, "MSG_RELEASE");
+ }
+ mHandler.removeCallbacksAndMessages(null);
+ stopPlayback(true);
+ stopCaptionTrack();
+ mSourceManager.release();
+ mHandler.getLooper().quitSafely();
+ if (mIsActiveSession) {
+ sActiveSessionSemaphore.release();
+ }
+ return true;
+ }
+
+ private boolean handleMessageRetryPlayback(int code) {
+ if (System.identityHashCode(mPlayer) == code) {
+ Log.i(TAG, "Retrying the playback for channel: " + mChannel);
+ mHandler.removeMessages(MSG_RETRY_PLAYBACK);
+ // When there is a request of retrying playback, don't reuse TunerHal.
+ mSourceManager.setKeepTuneStatus(false);
+ mRetryCount++;
+ if (DEBUG) {
+ Log.d(TAG, "MSG_RETRY_PLAYBACK " + mRetryCount);
+ }
+ mChannelDataManager.removeAllCallbacksAndMessages();
+ if (mRetryCount <= MAX_IMMEDIATE_RETRY_COUNT) {
+ resetPlayback();
+ } else {
+ // When it reaches this point, it may be due to an error that occurred
+ // in
+ // the tuner device. Calling stopPlayback() resets the tuner device
+ // to recover from the error.
+ stopPlayback(false);
+ stopCaptionTrack();
+
+ notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL);
+ Log.i(TAG, "Notify weak signal since fail to retry playback");
+
+ // After MAX_IMMEDIATE_RETRY_COUNT, give some delay of an empirically
+ // chosen
+ // value before recovering the playback.
+ mHandler.sendEmptyMessageDelayed(
+ MSG_RESET_PLAYBACK, RECOVER_STOPPED_PLAYBACK_PERIOD_MS);
+ }
+ }
+ return true;
+ }
+
+ private boolean handleMessageResetPlayback() {
+ if (DEBUG) {
+ Log.d(TAG, "MSG_RESET_PLAYBACK");
+ }
+ mChannelDataManager.removeAllCallbacksAndMessages();
+ resetPlayback();
+ return true;
+ }
+
+ private boolean handleMessageStartPlayback(int playerHashCode) {
+ if (DEBUG) {
+ Log.d(TAG, "MSG_START_PLAYBACK");
+ }
+ if (mChannel != null || mRecordingId != null) {
+ startPlayback(playerHashCode);
+ }
+ return true;
+ }
+
+ private boolean handleMessageUpdateProgram(EitItem program) {
+ if (mChannel != null) {
+ updateTvTracks(program, false);
+ mHandler.sendEmptyMessage(MSG_PARENTAL_CONTROLS);
+ }
+ return true;
+ }
+
+ private boolean handleMessageScheduleOfPrograms(Pair<TunerChannel, List<EitItem>> pair) {
+ mHandler.removeMessages(MSG_UPDATE_PROGRAM);
+ TunerChannel channel = pair.first;
+ if (mChannel == null) {
+ return true;
+ }
+ if (mChannel != null && mChannel.compareTo(channel) != 0) {
+ return true;
+ }
+ mPrograms = pair.second;
+ EitItem currentProgram = getCurrentProgram();
+ if (currentProgram == null) {
+ mProgram = null;
+ }
+ long currentTimeMs = getCurrentPosition();
+ if (mPrograms != null) {
+ for (EitItem item : mPrograms) {
+ if (currentProgram != null && currentProgram.compareTo(item) == 0) {
+ if (DEBUG) {
+ Log.d(TAG, "Update current TvTracks " + item);
}
- if (mPlayer != null) {
- mPlayer.setAudioTrackAndClosedCaption(false);
+ if (mProgram != null && mProgram.compareTo(item) == 0) {
+ continue;
}
- notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL);
- Log.i(
- TAG,
- "Notify weak signal due to signal check, "
- + String.format(
- "packetsPerSec:%d, bufferingTimeMs:%d, preparingTimeMs:%d, "
- + "videoFrameDrop:%d",
- (limitInBytes - mLastLimitInBytes) / TS_PACKET_SIZE,
- bufferingTimeMs,
- preparingTimeMs,
- TunerDebug.getVideoFrameDrop()));
- } else if (!isWeakSignal && mReportedWeakSignal) {
- boolean isPlaybackStable =
- mReadyStartTimeMs != INVALID_TIME
- && currentTime - mReadyStartTimeMs
- > PLAYBACK_STATE_CHANGED_WAITING_THRESHOLD_MS;
- if (!isPlaybackStable) {
- // Wait until playback becomes stable.
- } else if (mReportedDrawnToSurface) {
- mHandler.removeMessages(MSG_RETRY_PLAYBACK);
- notifyVideoAvailable();
- mPlayer.setAudioTrackAndClosedCaption(true);
+ mProgram = item;
+ updateTvTracks(item, false);
+ } else if (item.getStartTimeUtcMillis() > currentTimeMs) {
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "Update next TvTracks "
+ + item
+ + " "
+ + (item.getStartTimeUtcMillis() - currentTimeMs));
}
+ mHandler.sendMessageDelayed(
+ mHandler.obtainMessage(MSG_UPDATE_PROGRAM, item),
+ item.getStartTimeUtcMillis() - currentTimeMs);
}
- mLastLimitInBytes = limitInBytes;
- mHandler.sendEmptyMessageDelayed(MSG_CHECK_SIGNAL, CHECK_NO_SIGNAL_PERIOD_MS);
+ }
+ }
+ mHandler.sendEmptyMessage(MSG_PARENTAL_CONTROLS);
+ return true;
+ }
+
+ private boolean handleMessageUpdateChannelInfo(TunerChannel tunerChannel) {
+ if (mChannel != null && mChannel.compareTo(tunerChannel) == 0) {
+ updateChannelInfo(tunerChannel);
+ }
+ return true;
+ }
+
+ private boolean handleMessageProgramDataResult(Message msg) {
+ TunerChannel channel = (TunerChannel) ((Pair) msg.obj).first;
+
+ // If there already exists, skip it since real-time data is a top priority,
+ if (mChannel != null
+ && mChannel.compareTo(channel) == 0
+ && mPrograms == null
+ && mProgram == null) {
+ sendMessage(MSG_SCHEDULE_OF_PROGRAMS, msg.obj);
+ }
+ return true;
+ }
+
+ private boolean handleMessageTrickplayBySeek(int seekPositionMs) {
+ if (mPlayer == null) {
+ return true;
+ }
+ if (mRecordingId != null) {
+ long systemBufferTime =
+ System.currentTimeMillis() - SEEK_MARGIN_MS - mRecordedProgramStartTimeMs;
+ if (seekPositionMs > systemBufferTime) {
+ doTimeShiftResume();
return true;
- case MSG_SET_SURFACE:
- {
- if (mPlayer != null) {
- mPlayer.setSurface(mSurface);
- } else {
- // TODO: Since surface is dynamically set, we can remove the dependency of
- // playback start on mSurface nullity.
- resetPlayback();
- }
- return true;
+ }
+ }
+ doTrickplayBySeek(seekPositionMs);
+ return true;
+ }
+
+ private boolean handleMessageSmoothTrickplayMonitor() {
+ if (mPlayer == null) {
+ return true;
+ }
+ long systemCurrentTime = System.currentTimeMillis();
+ long position = getCurrentPosition();
+ if (mRecordingId == null) {
+ // Checks if the position exceeds the upper bound when forwarding,
+ // or exceed the lower bound when rewinding.
+ // If the direction is not checked, there can be some issues.
+ // (See b/29939781 for more details.)
+ if ((position > systemCurrentTime && mPlaybackParams.getSpeed() > 0L)
+ || (position < mBufferStartTimeMs && mPlaybackParams.getSpeed() < 0L)) {
+ doTimeShiftResume();
+ return true;
+ }
+ } else {
+ if (position > mRecordingDuration || position < 0) {
+ doTimeShiftPause();
+ return true;
+ }
+ long systemBufferTime =
+ systemCurrentTime - SEEK_MARGIN_MS - mRecordedProgramStartTimeMs;
+ if (position > systemBufferTime) {
+ doTimeShiftResume();
+ return true;
+ }
+ }
+ mHandler.sendEmptyMessageDelayed(
+ MSG_SMOOTH_TRICKPLAY_MONITOR, TRICKPLAY_MONITOR_INTERVAL_MS);
+ return true;
+ }
+
+ private boolean handleMessageReschedulePrograms() {
+ if (mHandler.hasMessages(MSG_SCHEDULE_OF_PROGRAMS)) {
+ mHandler.sendEmptyMessage(MSG_RESCHEDULE_PROGRAMS);
+ } else {
+ doReschedulePrograms();
+ }
+ return true;
+ }
+
+ private boolean handleMessageParentalControl() {
+ doParentalControls();
+ mHandler.removeMessages(MSG_PARENTAL_CONTROLS);
+ mHandler.sendEmptyMessageDelayed(MSG_PARENTAL_CONTROLS, PARENTAL_CONTROLS_INTERVAL_MS);
+ return true;
+ }
+
+ private boolean handleMessageUnblockedRating(TvContentRating unblockedContentRating) {
+ mUnblockedContentRating = unblockedContentRating;
+ return handleMessageParentalControl();
+ }
+
+ private boolean handleMessageDiscoverCaptionServiceNumber(int serviceNumber) {
+ doDiscoverCaptionServiceNumber(serviceNumber);
+ return true;
+ }
+
+ private boolean handleMessageSelectTrack(int type, String trackId) {
+ if (mPlayer == null) {
+ Log.w(TAG, "mPlayer is null when doselectTrack is called");
+ return false;
+ }
+ if (mChannel != null || mRecordingId != null) {
+ doSelectTrack(type, trackId);
+ }
+ return true;
+ }
+
+ private boolean handleMessageUpdateCaptionTrack() {
+ if (mCaptionEnabled) {
+ startCaptionTrack();
+ } else {
+ stopCaptionTrack();
+ }
+ return true;
+ }
+
+ private boolean handleMessageTimeshiftPause() {
+ if (DEBUG) {
+ Log.d(TAG, "MSG_TIMESHIFT_PAUSE");
+ }
+ if (mPlayer == null) {
+ return true;
+ }
+ setTrickplayEnabledIfNeeded();
+ doTimeShiftPause();
+ return true;
+ }
+
+ private boolean handleMessageTimeshiftResume() {
+ if (DEBUG) {
+ Log.d(TAG, "MSG_TIMESHIFT_RESUME");
+ }
+ if (mPlayer == null) {
+ return true;
+ }
+ setTrickplayEnabledIfNeeded();
+ doTimeShiftResume();
+ return true;
+ }
+
+ private boolean handleMessageTimeshiftSeekTo(long timeMs) {
+ if (DEBUG) {
+ Log.d(TAG, "MSG_TIMESHIFT_SEEK_TO (position=" + timeMs + ")");
+ }
+ if (mPlayer == null) {
+ return true;
+ }
+ setTrickplayEnabledIfNeeded();
+ doTimeShiftSeekTo(timeMs);
+ return true;
+ }
+
+ private boolean handleMessageTimeshiftSetPlaybackParams(PlaybackParams playbackParams) {
+ if (mPlayer == null) {
+ return true;
+ }
+ setTrickplayEnabledIfNeeded();
+ doTimeShiftSetPlaybackParams(playbackParams);
+ return true;
+ }
+
+ private boolean handleMessageAudioCapabilitiesChanged(AudioCapabilities audioCapabilities) {
+ if (DEBUG) {
+ Log.d(TAG, "MSG_AUDIO_CAPABILITIES_CHANGED " + audioCapabilities);
+ }
+ if (audioCapabilities == null) {
+ return true;
+ }
+ if (!audioCapabilities.equals(mAudioCapabilities)) {
+ // HDMI supported encodings are changed. restart player.
+ mAudioCapabilities = audioCapabilities;
+ resetPlayback();
+ }
+ return true;
+ }
+
+ private boolean handleMessageSetStreamVolume() {
+ if (mPlayer != null && mPlayer.isPlaying()) {
+ mPlayer.setVolume(mVolume);
+ }
+ return true;
+ }
+
+ private boolean handleMessageTunerPreferencesChanged() {
+ mHandler.removeMessages(MSG_TUNER_PREFERENCES_CHANGED);
+ @TrickplaySetting int trickplaySetting = TunerPreferences.getTrickplaySetting(mContext);
+ if (trickplaySetting != mTrickplaySetting) {
+ boolean wasTrcikplayEnabled =
+ mTrickplaySetting != TunerPreferences.TRICKPLAY_SETTING_DISABLED;
+ boolean isTrickplayEnabled =
+ trickplaySetting != TunerPreferences.TRICKPLAY_SETTING_DISABLED;
+ mTrickplaySetting = trickplaySetting;
+ if (isTrickplayEnabled != wasTrcikplayEnabled) {
+ sendMessage(MSG_RESET_PLAYBACK, System.identityHashCode(mPlayer));
+ }
+ }
+ return true;
+ }
+
+ private boolean handleMessageBufferStartTimeChanged(long bufferStartTimeMs) {
+ if (mPlayer == null) {
+ return true;
+ }
+ mBufferStartTimeMs = bufferStartTimeMs;
+ if (!hasEnoughBackwardBuffer()
+ && (!mPlayer.isPlaying() || mPlaybackParams.getSpeed() < 1.0f)) {
+ mPlayer.setPlayWhenReady(true);
+ mPlayer.setAudioTrackAndClosedCaption(true);
+ mPlaybackParams.setSpeed(1.0f);
+ }
+ return true;
+ }
+
+ private boolean handleMessageBufferStateChanged(boolean available) {
+ mSession.notifyTimeShiftStatusChanged(
+ available
+ ? TvInputManager.TIME_SHIFT_STATUS_AVAILABLE
+ : TvInputManager.TIME_SHIFT_STATUS_UNAVAILABLE);
+ return true;
+ }
+
+ private boolean handleMessageCheckSignal() {
+ if (mChannel == null || mPlayer == null) {
+ return true;
+ }
+ TsDataSource source = mPlayer.getDataSource();
+ long limitInBytes = source != null ? source.getBufferedPosition() : 0L;
+ if (TunerDebug.ENABLED) {
+ TunerDebug.calculateDiff();
+ mTunerSessionOverlay.sendUiMessage(
+ TunerSessionOverlay.MSG_UI_SET_STATUS_TEXT,
+ Html.fromHtml(
+ StatusTextUtils.getStatusWarningInHTML(
+ (limitInBytes - mLastLimitInBytes) / TS_PACKET_SIZE,
+ TunerDebug.getVideoFrameDrop(),
+ TunerDebug.getBytesInQueue(),
+ TunerDebug.getAudioPositionUs(),
+ TunerDebug.getAudioPositionUsRate(),
+ TunerDebug.getAudioPtsUs(),
+ TunerDebug.getAudioPtsUsRate(),
+ TunerDebug.getVideoPtsUs(),
+ TunerDebug.getVideoPtsUsRate())));
+ }
+ mTunerSessionOverlay.sendUiMessage(TunerSessionOverlay.MSG_UI_HIDE_MESSAGE);
+ long currentTime = SystemClock.elapsedRealtime();
+ long bufferingTimeMs =
+ mBufferingStartTimeMs != INVALID_TIME
+ ? currentTime - mBufferingStartTimeMs
+ : mBufferingStartTimeMs;
+ long preparingTimeMs =
+ mPreparingStartTimeMs != INVALID_TIME
+ ? currentTime - mPreparingStartTimeMs
+ : mPreparingStartTimeMs;
+ boolean isBufferingTooLong = bufferingTimeMs > PLAYBACK_STATE_CHANGED_WAITING_THRESHOLD_MS;
+ boolean isPreparingTooLong = preparingTimeMs > PLAYBACK_STATE_CHANGED_WAITING_THRESHOLD_MS;
+ boolean isWeakSignal =
+ source != null
+ && mChannel.getType() != Channel.TunerType.TYPE_FILE
+ && (isBufferingTooLong || isPreparingTooLong);
+ if (isWeakSignal && !mReportedWeakSignal) {
+ if (!mHandler.hasMessages(MSG_RETRY_PLAYBACK)) {
+ mHandler.sendMessageDelayed(
+ mHandler.obtainMessage(
+ MSG_RETRY_PLAYBACK, System.identityHashCode(mPlayer)),
+ PLAYBACK_RETRY_DELAY_MS);
+ }
+ if (mPlayer != null) {
+ mPlayer.setAudioTrackAndClosedCaption(false);
+ }
+ notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL);
+ if (CommonFeatures.TUNER_SIGNAL_STRENGTH.isEnabled(mContext)) {
+ mSession.notifySignalStrength(0);
+ mHandler.removeMessages(MSG_CHECK_SIGNAL_STRENGTH);
+ }
+ Log.i(
+ TAG,
+ "Notify weak signal due to signal check, "
+ + String.format(
+ "packetsPerSec:%d, bufferingTimeMs:%d, preparingTimeMs:%d, "
+ + "videoFrameDrop:%d",
+ (limitInBytes - mLastLimitInBytes) / TS_PACKET_SIZE,
+ bufferingTimeMs,
+ preparingTimeMs,
+ TunerDebug.getVideoFrameDrop()));
+ } else if (!isWeakSignal && mReportedWeakSignal) {
+ boolean isPlaybackStable =
+ mReadyStartTimeMs != INVALID_TIME
+ && currentTime - mReadyStartTimeMs
+ > PLAYBACK_STATE_CHANGED_WAITING_THRESHOLD_MS;
+ if (!isPlaybackStable) {
+ // Wait until playback becomes stable.
+ } else if (mReportedDrawnToSurface) {
+ mHandler.removeMessages(MSG_RETRY_PLAYBACK);
+ notifyVideoAvailable();
+ mPlayer.setAudioTrackAndClosedCaption(true);
+ if (mHandler.hasMessages(MSG_CHECK_SIGNAL_STRENGTH)) {
+ mHandler.removeMessages(MSG_CHECK_SIGNAL_STRENGTH);
}
- case MSG_NOTIFY_AUDIO_TRACK_UPDATED:
- {
- notifyAudioTracksUpdated();
- return true;
+ if (CommonFeatures.TUNER_SIGNAL_STRENGTH.isEnabled(mContext)) {
+ sendMessage(MSG_CHECK_SIGNAL_STRENGTH);
}
- default:
- {
- Log.w(TAG, "Unhandled message code: " + msg.what);
- return false;
+ }
+ }
+ mLastLimitInBytes = limitInBytes;
+ mHandler.sendEmptyMessageDelayed(MSG_CHECK_SIGNAL, CHECK_NO_SIGNAL_PERIOD_MS);
+ return true;
+ }
+
+ private boolean handleMessageSetSurface() {
+ if (mPlayer != null) {
+ mPlayer.setSurface(mSurface);
+ } else {
+ // TODO: Since surface is dynamically set, we can remove the dependency of
+ // playback start on mSurface nullity.
+ resetPlayback();
+ }
+ return true;
+ }
+
+ private boolean handleMessageAudioTrackUpdated() {
+ notifyAudioTracksUpdated();
+ return true;
+ }
+
+ private boolean handleMessageCheckSignalStrength() {
+ if (CommonFeatures.TUNER_SIGNAL_STRENGTH.isEnabled(mContext)) {
+ int signal;
+ if (mPlayer != null) {
+ TsDataSource source = mPlayer.getDataSource();
+ if (source != null) {
+ signal = source.getSignalStrength();
+ return handleSignal(signal);
}
+ }
+ }
+ return false;
+ }
+
+ @VisibleForTesting
+ protected boolean handleSignal(int signal) {
+ if (signal == TvInputConstantCompat.SIGNAL_STRENGTH_NOT_USED
+ || signal == TvInputConstantCompat.SIGNAL_STRENGTH_ERROR) {
+ notifySignal(signal);
+ return true;
+ }
+ if (signal != mSignalStrength && signal >= 0) {
+ notifySignal(signal);
}
+ mHandler.sendEmptyMessageDelayed(
+ MSG_CHECK_SIGNAL_STRENGTH, CHECK_SIGNAL_STRENGTH_INTERVAL_MS);
+ return true;
+ }
+
+ @VisibleForTesting
+ protected void notifySignal(int signal) {
+ mSession.notifySignalStrength(signal);
+ mSignalStrength = signal;
+ }
+
+ private boolean unhandledMessage(Message msg) {
+ Log.w(TAG, "Unhandled message code: " + msg.what);
+ return false;
}
// Private methods
@@ -1212,10 +1393,12 @@ public class TunerSessionWorker
}
}
- private MpegTsPlayer createPlayer(AudioCapabilities capabilities) {
+ @VisibleForTesting
+ protected MpegTsPlayer createPlayer(AudioCapabilities capabilities) {
if (capabilities == null) {
Log.w(TAG, "No Audio Capabilities");
}
+ mSourceManager.setKeepTuneStatus(true);
long now = System.currentTimeMillis();
if (mTrickplayModeCustomization == CustomizationManager.TRICKPLAY_MODE_ENABLED
&& mTrickplaySetting == TunerPreferences.TRICKPLAY_SETTING_NOT_SET) {
@@ -1249,19 +1432,27 @@ public class TunerSessionWorker
}
MpegTsPlayer player =
new MpegTsPlayer(
- new MpegTsRendererBuilder(mContext, bufferManager, this),
+ new MpegTsRendererBuilder(
+ mContext, bufferManager, this, mConcurrentDvrPlaybackFlags),
mHandler,
mSourceManager,
capabilities,
this);
Log.i(TAG, "Passthrough AC3 renderer");
if (DEBUG) Log.d(TAG, "ExoPlayer created");
+ player.setCaptionServiceNumber(Cea708Data.EMPTY_SERVICE_NUMBER);
+ player.setVideoEventListener(this);
+ player.setCaptionServiceNumber(
+ mCaptionTrack != null
+ ? mCaptionTrack.serviceNumber
+ : Cea708Data.EMPTY_SERVICE_NUMBER);
return player;
}
private void startCaptionTrack() {
if (mCaptionEnabled && mCaptionTrack != null) {
- mSession.sendUiMessage(TunerSession.MSG_UI_START_CAPTION_TRACK, mCaptionTrack);
+ mTunerSessionOverlay.sendUiMessage(
+ TunerSessionOverlay.MSG_UI_START_CAPTION_TRACK, mCaptionTrack);
if (mPlayer != null) {
mPlayer.setCaptionServiceNumber(mCaptionTrack.serviceNumber);
}
@@ -1272,14 +1463,14 @@ public class TunerSessionWorker
if (mPlayer != null) {
mPlayer.setCaptionServiceNumber(Cea708Data.EMPTY_SERVICE_NUMBER);
}
- mSession.sendUiMessage(TunerSession.MSG_UI_STOP_CAPTION_TRACK);
+ mTunerSessionOverlay.sendUiMessage(TunerSessionOverlay.MSG_UI_STOP_CAPTION_TRACK);
}
private void resetTvTracks() {
mTvTracks.clear();
mAudioTrackMap.clear();
mCaptionTrackMap.clear();
- mSession.sendUiMessage(TunerSession.MSG_UI_RESET_CAPTION_TRACK);
+ mTunerSessionOverlay.sendUiMessage(TunerSessionOverlay.MSG_UI_RESET_CAPTION_TRACK);
mSession.notifyTracksChanged(mTvTracks);
}
@@ -1478,7 +1669,7 @@ public class TunerSessionWorker
mBufferingStartTimeMs = INVALID_TIME;
mReadyStartTimeMs = INVALID_TIME;
mLastLimitInBytes = 0L;
- mSession.sendUiMessage(TunerSession.MSG_UI_HIDE_AUDIO_UNPLAYABLE);
+ mTunerSessionOverlay.sendUiMessage(TunerSessionOverlay.MSG_UI_HIDE_AUDIO_UNPLAYABLE);
mSession.notifyTimeShiftStatusChanged(TvInputManager.TIME_SHIFT_STATUS_UNAVAILABLE);
}
}
@@ -1518,24 +1709,14 @@ public class TunerSessionWorker
// Doesn't show buffering during weak signal.
notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING);
}
- mSession.sendUiMessage(TunerSession.MSG_UI_HIDE_MESSAGE);
+ mTunerSessionOverlay.sendUiMessage(TunerSessionOverlay.MSG_UI_HIDE_MESSAGE);
mPlayerStarted = true;
}
}
- private void preparePlayback() {
- SoftPreconditions.checkState(mPlayer == null);
- if (mChannel == null && mRecordingId == null) {
- return;
- }
- mSourceManager.setKeepTuneStatus(true);
+ @VisibleForTesting
+ protected void preparePlayback() {
MpegTsPlayer player = createPlayer(mAudioCapabilities);
- player.setCaptionServiceNumber(Cea708Data.EMPTY_SERVICE_NUMBER);
- player.setVideoEventListener(this);
- player.setCaptionServiceNumber(
- mCaptionTrack != null
- ? mCaptionTrack.serviceNumber
- : Cea708Data.EMPTY_SERVICE_NUMBER);
if (!player.prepare(mContext, mChannel, mHasSoftwareAudioDecoder, this)) {
mSourceManager.setKeepTuneStatus(false);
player.release();
@@ -1554,6 +1735,12 @@ public class TunerSessionWorker
mPlayerStarted = false;
mHandler.removeMessages(MSG_CHECK_SIGNAL);
mHandler.sendEmptyMessageDelayed(MSG_CHECK_SIGNAL, CHECK_NO_SIGNAL_INITIAL_DELAY_MS);
+ if (mHandler.hasMessages(MSG_CHECK_SIGNAL_STRENGTH)) {
+ mHandler.removeMessages(MSG_CHECK_SIGNAL_STRENGTH);
+ }
+ if (CommonFeatures.TUNER_SIGNAL_STRENGTH.isEnabled(mContext)) {
+ mHandler.sendEmptyMessage(MSG_CHECK_SIGNAL_STRENGTH);
+ }
}
}
@@ -1568,9 +1755,10 @@ public class TunerSessionWorker
timestamp = SystemClock.elapsedRealtime();
Log.i(TAG, "[Profiler] stopPlayback() takes " + (timestamp - oldTimestamp) + " ms");
}
- if (mChannelBlocked || mSurface == null) {
+ if (mChannelBlocked || mSurface == null || (mChannel == null && mRecordingId == null)) {
return;
}
+ SoftPreconditions.checkState(mPlayer == null);
preparePlayback();
}
@@ -1591,6 +1779,10 @@ public class TunerSessionWorker
}
mLastPositionMs = 0;
mCaptionTrack = null;
+ mSignalStrength = TvInputConstantCompat.SIGNAL_STRENGTH_UNKNOWN;
+ if (CommonFeatures.TUNER_SIGNAL_STRENGTH.isEnabled(mContext)) {
+ mSession.notifySignalStrength(mSignalStrength);
+ }
mHandler.sendEmptyMessage(MSG_PARENTAL_CONTROLS);
}
@@ -1793,10 +1985,14 @@ public class TunerSessionWorker
if (currentProgram == null) {
return null;
}
- TvContentRating[] ratings =
+ ImmutableList<TvContentRating> ratings =
mTvContentRatingCache.getRatings(currentProgram.getContentRating());
- if (ratings == null || ratings.length == 0) {
- ratings = new TvContentRating[] {TvContentRating.UNRATED};
+ if ((ratings == null || ratings.isEmpty())) {
+ if (Experiments.ENABLE_UNRATED_CONTENT_SETTINGS.get()) {
+ ratings = ImmutableList.of(TvContentRating.UNRATED);
+ } else {
+ ratings = NO_CONTENT_RATINGS;
+ }
}
for (TvContentRating rating : ratings) {
if (!Objects.equals(mUnblockedContentRating, rating)
diff --git a/tuner/src/com/android/tv/tuner/tvinput/TunerSessionWorkerExoV2.java b/tuner/src/com/android/tv/tuner/tvinput/TunerSessionWorkerExoV2.java
new file mode 100644
index 00000000..82afff15
--- /dev/null
+++ b/tuner/src/com/android/tv/tuner/tvinput/TunerSessionWorkerExoV2.java
@@ -0,0 +1,2073 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.tvinput;
+
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.database.Cursor;
+import android.media.MediaFormat;
+import android.media.PlaybackParams;
+import android.media.tv.TvContentRating;
+import android.media.tv.TvContract;
+import android.media.tv.TvInputManager;
+import android.media.tv.TvTrackInfo;
+import android.net.Uri;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Message;
+import android.os.SystemClock;
+import android.support.annotation.AnyThread;
+import android.support.annotation.MainThread;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.support.annotation.WorkerThread;
+import android.text.Html;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Pair;
+import android.util.SparseArray;
+import android.view.Surface;
+import android.view.accessibility.CaptioningManager;
+import com.android.tv.common.CommonPreferences.TrickplaySetting;
+import com.android.tv.common.SoftPreconditions;
+import com.android.tv.common.TvContentRatingCache;
+import com.android.tv.common.compat.TvInputConstantCompat;
+import com.android.tv.common.customization.CustomizationManager;
+import com.android.tv.common.customization.CustomizationManager.TRICKPLAY_MODE;
+import com.android.tv.common.experiments.Experiments;
+import com.android.tv.common.feature.CommonFeatures;
+import com.android.tv.common.util.SystemPropertiesProxy;
+import com.android.tv.tuner.data.Cea708Data;
+import com.android.tv.tuner.data.PsipData.EitItem;
+import com.android.tv.tuner.data.PsipData.TvTracksInterface;
+import com.android.tv.tuner.data.TunerChannel;
+import com.android.tv.tuner.data.nano.Channel;
+import com.android.tv.tuner.data.nano.Track.AtscAudioTrack;
+import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack;
+import com.android.tv.tuner.exoplayer.MpegTsPlayer;
+import com.android.tv.tuner.exoplayer.MpegTsRendererBuilder;
+import com.android.tv.tuner.exoplayer.buffer.BufferManager;
+import com.android.tv.tuner.exoplayer.buffer.BufferManager.StorageManager;
+import com.android.tv.tuner.exoplayer.buffer.DvrStorageManager;
+import com.android.tv.tuner.exoplayer.buffer.PlaybackBufferListener;
+import com.android.tv.tuner.exoplayer.buffer.TrickplayStorageManager;
+import com.android.tv.tuner.prefs.TunerPreferences;
+import com.android.tv.tuner.source.TsDataSource;
+import com.android.tv.tuner.source.TsDataSourceManager;
+import com.android.tv.tuner.ts.EventDetector.EventListener;
+import com.android.tv.tuner.tvinput.datamanager.ChannelDataManager;
+import com.android.tv.tuner.tvinput.debug.TunerDebug;
+import com.android.tv.tuner.util.StatusTextUtils;
+import com.google.android.exoplayer.ExoPlayer;
+import com.google.android.exoplayer.audio.AudioCapabilities;
+import com.google.common.collect.ImmutableList;
+import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+
+/** Handles playback related operations on a worker thread. */
+@WorkerThread
+public class TunerSessionWorkerExoV2
+ implements PlaybackBufferListener,
+ MpegTsPlayer.VideoEventListener,
+ MpegTsPlayer.Listener,
+ EventListener,
+ ChannelDataManager.ProgramInfoListener,
+ Handler.Callback {
+
+ private static final String TAG = "TunerSessionWorkerExoV2";
+ private static final boolean DEBUG = false;
+ private static final boolean ENABLE_PROFILER = true;
+ private static final String PLAY_FROM_CHANNEL = "channel";
+ private static final String MAX_BUFFER_SIZE_KEY = "tv.tuner.buffersize_mbytes";
+ private static final int MAX_BUFFER_SIZE_DEF = 2 * 1024; // 2GB
+ private static final int MIN_BUFFER_SIZE_DEF = 256; // 256MB
+
+ // Public messages
+ public static final int MSG_SELECT_TRACK = 1;
+ public static final int MSG_UPDATE_CAPTION_TRACK = 2;
+ public static final int MSG_SET_STREAM_VOLUME = 3;
+ public static final int MSG_TIMESHIFT_PAUSE = 4;
+ public static final int MSG_TIMESHIFT_RESUME = 5;
+ public static final int MSG_TIMESHIFT_SEEK_TO = 6;
+ public static final int MSG_TIMESHIFT_SET_PLAYBACKPARAMS = 7;
+ public static final int MSG_UNBLOCKED_RATING = 9;
+ public static final int MSG_TUNER_PREFERENCES_CHANGED = 10;
+
+ // Private messages
+ @VisibleForTesting protected static final int MSG_TUNE = 1000;
+ private static final int MSG_RELEASE = 1001;
+ @VisibleForTesting protected static final int MSG_RETRY_PLAYBACK = 1002;
+ private static final int MSG_START_PLAYBACK = 1003;
+ private static final int MSG_UPDATE_PROGRAM = 1008;
+ private static final int MSG_SCHEDULE_OF_PROGRAMS = 1009;
+ private static final int MSG_UPDATE_CHANNEL_INFO = 1010;
+ private static final int MSG_TRICKPLAY_BY_SEEK = 1011;
+ private static final int MSG_SMOOTH_TRICKPLAY_MONITOR = 1012;
+ private static final int MSG_PARENTAL_CONTROLS = 1015;
+ private static final int MSG_RESCHEDULE_PROGRAMS = 1016;
+ private static final int MSG_BUFFER_START_TIME_CHANGED = 1017;
+ @VisibleForTesting protected static final int MSG_CHECK_SIGNAL = 1018;
+ private static final int MSG_DISCOVER_CAPTION_SERVICE_NUMBER = 1019;
+ private static final int MSG_RESET_PLAYBACK = 1020;
+ private static final int MSG_BUFFER_STATE_CHANGED = 1021;
+ private static final int MSG_PROGRAM_DATA_RESULT = 1022;
+ private static final int MSG_STOP_TUNE = 1023;
+ private static final int MSG_SET_SURFACE = 1024;
+ private static final int MSG_NOTIFY_AUDIO_TRACK_UPDATED = 1025;
+ @VisibleForTesting protected static final int MSG_CHECK_SIGNAL_STRENGTH = 1026;
+
+ private static final int TS_PACKET_SIZE = 188;
+ private static final int CHECK_NO_SIGNAL_INITIAL_DELAY_MS = 4000;
+ private static final int CHECK_NO_SIGNAL_PERIOD_MS = 500;
+ private static final int RECOVER_STOPPED_PLAYBACK_PERIOD_MS = 2500;
+ private static final int PARENTAL_CONTROLS_INTERVAL_MS = 5000;
+ private static final int RESCHEDULE_PROGRAMS_INITIAL_DELAY_MS = 4000;
+ private static final int RESCHEDULE_PROGRAMS_INTERVAL_MS = 10000;
+ private static final int RESCHEDULE_PROGRAMS_TOLERANCE_MS = 2000;
+ private static final int CHECK_SIGNAL_STRENGTH_INTERVAL_MS = 5000;
+ // The following 3s is defined empirically. This should be larger than 2s considering video
+ // key frame interval in the TS stream.
+ private static final int PLAYBACK_STATE_CHANGED_WAITING_THRESHOLD_MS = 3000;
+ private static final int PLAYBACK_RETRY_DELAY_MS = 5000;
+ private static final int MAX_IMMEDIATE_RETRY_COUNT = 5;
+ private static final long INVALID_TIME = -1;
+
+ // Some examples of the track ids of the audio tracks, "a0", "a1", "a2".
+ // The number after prefix is being used for indicating a index of the given audio track.
+ private static final String AUDIO_TRACK_PREFIX = "a";
+
+ // Some examples of the tracks id of the caption tracks, "s1", "s2", "s3".
+ // The number after prefix is being used for indicating a index of a caption service number
+ // of the given caption track.
+ private static final String SUBTITLE_TRACK_PREFIX = "s";
+ private static final int TRACK_PREFIX_SIZE = 1;
+ private static final String VIDEO_TRACK_ID = "v";
+ private static final long BUFFER_UNDERFLOW_BUFFER_MS = 5000;
+
+ // Actual interval would be divided by the speed.
+ private static final int EXPECTED_KEY_FRAME_INTERVAL_MS = 500;
+ private static final int MIN_TRICKPLAY_SEEK_INTERVAL_MS = 20;
+ private static final int TRICKPLAY_MONITOR_INTERVAL_MS = 250;
+ private static final int RELEASE_WAIT_INTERVAL_MS = 50;
+ private static final long TRICKPLAY_OFF_DURATION_MS = TimeUnit.DAYS.toMillis(14);
+ private static final long SEEK_MARGIN_MS = TimeUnit.SECONDS.toMillis(2);
+ public static final ImmutableList<TvContentRating> NO_CONTENT_RATINGS = ImmutableList.of();
+
+ /**
+ * Guarantees that at most one active worker exists at any give time. Synchronization between
+ * multiple TunerSessionWorkerExoV2 is necessary when concurrent release and creation takes
+ * place.
+ */
+ private static Semaphore sActiveSessionSemaphore = new Semaphore(1);
+
+ private final Context mContext;
+ private final ChannelDataManager mChannelDataManager;
+ private final TsDataSourceManager mSourceManager;
+ private final int mMaxTrickplayBufferSizeMb;
+ private final File mTrickplayBufferDir;
+ private final @TRICKPLAY_MODE int mTrickplayModeCustomization;
+ private volatile Surface mSurface;
+ private volatile float mVolume = 1.0f;
+ private volatile boolean mCaptionEnabled;
+ private volatile MpegTsPlayer mPlayer;
+ private volatile TunerChannel mChannel;
+ private volatile Long mRecordingDuration;
+ private volatile long mRecordStartTimeMs;
+ private volatile long mBufferStartTimeMs;
+ private volatile boolean mTrickplayDisabledByStorageIssue;
+ private @TrickplaySetting int mTrickplaySetting;
+ private long mTrickplayExpiredMs;
+ private String mRecordingId;
+ private final Handler mHandler;
+ private int mRetryCount;
+ private final ArrayList<TvTrackInfo> mTvTracks;
+ private final SparseArray<AtscAudioTrack> mAudioTrackMap;
+ private final SparseArray<AtscCaptionTrack> mCaptionTrackMap;
+ private AtscCaptionTrack mCaptionTrack;
+ private PlaybackParams mPlaybackParams = new PlaybackParams();
+ private boolean mPlayerStarted = false;
+ private boolean mReportedDrawnToSurface = false;
+ private boolean mReportedWeakSignal = false;
+ private EitItem mProgram;
+ private List<EitItem> mPrograms;
+ private final TvInputManager mTvInputManager;
+ private boolean mChannelBlocked;
+ private TvContentRating mUnblockedContentRating;
+ private long mLastPositionMs;
+ private final AudioCapabilitiesReceiverV1Wrapper mAudioCapabilitiesReceiver;
+ private AudioCapabilities mAudioCapabilities;
+ private long mLastLimitInBytes;
+ private final TvContentRatingCache mTvContentRatingCache = TvContentRatingCache.getInstance();
+ private final TunerSessionExoV2 mSession;
+ private final TunerSessionOverlay mTunerSessionOverlay;
+ private final boolean mHasSoftwareAudioDecoder;
+ private int mPlayerState = ExoPlayer.STATE_IDLE;
+ private long mPreparingStartTimeMs;
+ private long mBufferingStartTimeMs;
+ private long mReadyStartTimeMs;
+ private boolean mIsActiveSession;
+ private boolean mReleaseRequested; // Guarded by mReleaseLock
+ private final Object mReleaseLock = new Object();
+ private final ConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags;
+
+ private int mSignalStrength;
+ private long mRecordedProgramStartTimeMs;
+
+ public TunerSessionWorkerExoV2(
+ Context context,
+ ChannelDataManager channelDataManager,
+ TunerSessionExoV2 tunerSession,
+ TunerSessionOverlay tunerSessionOverlay,
+ ConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlags,
+ TsDataSourceManager.Factory tsDataSourceManagerFactory) {
+ this(
+ context,
+ channelDataManager,
+ tunerSession,
+ tunerSessionOverlay,
+ null,
+ concurrentDvrPlaybackFlags,
+ tsDataSourceManagerFactory);
+ }
+
+ @VisibleForTesting
+ protected TunerSessionWorkerExoV2(
+ Context context,
+ ChannelDataManager channelDataManager,
+ TunerSessionExoV2 tunerSession,
+ TunerSessionOverlay tunerSessionOverlay,
+ @Nullable Handler handler,
+ ConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlags,
+ TsDataSourceManager.Factory tsDataSourceManagerFactory) {
+ mConcurrentDvrPlaybackFlags = concurrentDvrPlaybackFlags;
+ if (DEBUG) {
+ Log.d(TAG, "TunerSessionWorkerExoV2 created");
+ }
+ mContext = context;
+ if (handler != null) {
+ mHandler = handler;
+ } else {
+ // HandlerThread should be set up before it is registered as a listener in the all other
+ // components.
+ HandlerThread handlerThread = new HandlerThread(TAG);
+ handlerThread.start();
+ mHandler = new Handler(handlerThread.getLooper(), this);
+ }
+ mSession = tunerSession;
+ mTunerSessionOverlay = tunerSessionOverlay;
+ mChannelDataManager = channelDataManager;
+ mChannelDataManager.setListener(this);
+ mChannelDataManager.checkDataVersion(mContext);
+ mSourceManager = tsDataSourceManagerFactory.create(false);
+ mTvInputManager = (TvInputManager) context.getSystemService(Context.TV_INPUT_SERVICE);
+ mTvTracks = new ArrayList<>();
+ mAudioCapabilitiesReceiver =
+ new AudioCapabilitiesReceiverV1Wrapper(
+ context, mHandler, this::handleMessageAudioCapabilitiesChanged);
+ AudioCapabilities audioCapabilities = mAudioCapabilitiesReceiver.register();
+ mHandler.post(() -> handleMessageAudioCapabilitiesChanged(audioCapabilities));
+ mAudioTrackMap = new SparseArray<>();
+ mCaptionTrackMap = new SparseArray<>();
+ CaptioningManager captioningManager =
+ (CaptioningManager) context.getSystemService(Context.CAPTIONING_SERVICE);
+ mCaptionEnabled = captioningManager.isEnabled();
+ mPlaybackParams.setSpeed(1.0f);
+ mMaxTrickplayBufferSizeMb =
+ SystemPropertiesProxy.getInt(MAX_BUFFER_SIZE_KEY, MAX_BUFFER_SIZE_DEF);
+ mTrickplayModeCustomization = CustomizationManager.getTrickplayMode(context);
+ if (mTrickplayModeCustomization
+ == CustomizationManager.TRICKPLAY_MODE_USE_EXTERNAL_STORAGE) {
+ boolean useExternalStorage =
+ Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
+ && Environment.isExternalStorageRemovable();
+ mTrickplayBufferDir = useExternalStorage ? context.getExternalCacheDir() : null;
+ } else if (mTrickplayModeCustomization == CustomizationManager.TRICKPLAY_MODE_ENABLED) {
+ mTrickplayBufferDir = context.getCacheDir();
+ } else {
+ mTrickplayBufferDir = null;
+ }
+ mTrickplayDisabledByStorageIssue = mTrickplayBufferDir == null;
+ mTrickplaySetting = TunerPreferences.getTrickplaySetting(context);
+ if (mTrickplaySetting != TunerPreferences.TRICKPLAY_SETTING_NOT_SET
+ && mTrickplayModeCustomization
+ == CustomizationManager.TRICKPLAY_MODE_USE_EXTERNAL_STORAGE) {
+ // Consider the case of Customization package updates the value of trickplay mode
+ // to TRICKPLAY_MODE_USE_EXTERNAL_STORAGE after install.
+ mTrickplaySetting = TunerPreferences.TRICKPLAY_SETTING_NOT_SET;
+ TunerPreferences.setTrickplaySetting(context, mTrickplaySetting);
+ TunerPreferences.setTrickplayExpiredMs(context, 0);
+ }
+ mTrickplayExpiredMs = TunerPreferences.getTrickplayExpiredMs(context);
+ mPreparingStartTimeMs = INVALID_TIME;
+ mBufferingStartTimeMs = INVALID_TIME;
+ mReadyStartTimeMs = INVALID_TIME;
+ // Only one TunerSessionWorker can be connected to FfmpegDecoderClient at any given time.
+ // connect() will return false, if there is a connected TunerSessionWorker already.
+ mHasSoftwareAudioDecoder = false; // TODO reimplement ffmpeg for google3
+ // TODO connect the ffmpeg client and report if available.
+ }
+
+ // Public methods
+ @MainThread
+ public void tune(Uri channelUri) {
+ mHandler.removeCallbacksAndMessages(null);
+ mSourceManager.setHasPendingTune();
+ sendMessage(MSG_TUNE, channelUri);
+ }
+
+ @MainThread
+ public void stopTune() {
+ mHandler.removeCallbacksAndMessages(null);
+ sendMessage(MSG_STOP_TUNE);
+ }
+
+ /** Sets {@link Surface}. */
+ @MainThread
+ public void setSurface(Surface surface) {
+ if (surface != null && !surface.isValid()) {
+ Log.w(TAG, "Ignoring invalid surface.");
+ return;
+ }
+ // mSurface is kept even when tune is called right after. But, messages can be deleted by
+ // tune or updateChannelBlockStatus. So mSurface should be stored here, not through message.
+ mSurface = surface;
+ mHandler.sendEmptyMessage(MSG_SET_SURFACE);
+ }
+
+ /** Sets volume. */
+ @MainThread
+ public void setStreamVolume(float volume) {
+ // mVolume is kept even when tune is called right after. But, messages can be deleted by
+ // tune or updateChannelBlockStatus. So mVolume is stored here and mPlayer.setVolume will be
+ // called in MSG_SET_STREAM_VOLUME.
+ mVolume = volume;
+ mHandler.sendEmptyMessage(MSG_SET_STREAM_VOLUME);
+ }
+
+ /** Sets if caption is enabled or disabled. */
+ @MainThread
+ public void setCaptionEnabled(boolean captionEnabled) {
+ // mCaptionEnabled is kept even when tune is called right after. But, messages can be
+ // deleted by tune or updateChannelBlockStatus. So mCaptionEnabled is stored here and
+ // start/stopCaptionTrack will be called in MSG_UPDATE_CAPTION_STATUS.
+ mCaptionEnabled = captionEnabled;
+ mHandler.sendEmptyMessage(MSG_UPDATE_CAPTION_TRACK);
+ }
+
+ public TunerChannel getCurrentChannel() {
+ return mChannel;
+ }
+
+ @MainThread
+ public long getStartPosition() {
+ return mBufferStartTimeMs;
+ }
+
+ private String getRecordingPath() {
+ return Uri.parse(mRecordingId).getPath();
+ }
+
+ private Long getDurationForRecording(String recordingId) {
+ DvrStorageManager storageManager =
+ new DvrStorageManager(new File(getRecordingPath()), false);
+ List<BufferManager.TrackFormat> trackFormatList = storageManager.readTrackInfoFiles(false);
+ if (trackFormatList.isEmpty()) {
+ trackFormatList = storageManager.readTrackInfoFiles(true);
+ }
+ if (!trackFormatList.isEmpty()) {
+ BufferManager.TrackFormat trackFormat = trackFormatList.get(0);
+ Long durationUs = trackFormat.format.getLong(MediaFormat.KEY_DURATION);
+ // we need duration by milli for trickplay notification.
+ return durationUs != null ? durationUs / 1000 : null;
+ }
+ Log.e(TAG, "meta file for recording was not found: " + recordingId);
+ return null;
+ }
+
+ @MainThread
+ public long getCurrentPosition() {
+ // TODO: More precise time may be necessary.
+ MpegTsPlayer mpegTsPlayer = mPlayer;
+ long currentTime =
+ mpegTsPlayer != null
+ ? mRecordStartTimeMs + mpegTsPlayer.getCurrentPosition()
+ : mRecordStartTimeMs;
+ if (mChannel == null && mPlayerState == ExoPlayer.STATE_ENDED) {
+ currentTime = mRecordingDuration + mRecordStartTimeMs;
+ }
+ if (DEBUG) {
+ long systemCurrentTime = System.currentTimeMillis();
+ Log.d(
+ TAG,
+ "currentTime = "
+ + currentTime
+ + " ; System.currentTimeMillis() = "
+ + systemCurrentTime
+ + " ; diff = "
+ + (currentTime - systemCurrentTime));
+ }
+ return currentTime;
+ }
+
+ @AnyThread
+ public void sendMessage(int messageType) {
+ mHandler.sendEmptyMessage(messageType);
+ }
+
+ @AnyThread
+ public void sendMessage(int messageType, Object object) {
+ mHandler.obtainMessage(messageType, object).sendToTarget();
+ }
+
+ @AnyThread
+ public void sendMessage(int messageType, int arg1, int arg2, Object object) {
+ mHandler.obtainMessage(messageType, arg1, arg2, object).sendToTarget();
+ }
+
+ @MainThread
+ public void release() {
+ if (DEBUG) {
+ Log.d(TAG, "release()");
+ }
+ synchronized (mReleaseLock) {
+ mReleaseRequested = true;
+ }
+ if (mHasSoftwareAudioDecoder) {
+ // TODO reimplement for google3
+ // Here disconnect ffmpeg
+ }
+ mAudioCapabilitiesReceiver.unregister();
+ mChannelDataManager.setListener(null);
+ mHandler.removeCallbacksAndMessages(null);
+ mHandler.sendEmptyMessage(MSG_RELEASE);
+ }
+
+ // MpegTsPlayer.Listener
+ // Called in the same thread as mHandler.
+ @Override
+ public void onStateChanged(boolean playWhenReady, int playbackState) {
+ if (DEBUG) {
+ Log.d(TAG, "ExoPlayer state change: " + playbackState + " " + playWhenReady);
+ }
+ if (playbackState == mPlayerState) {
+ return;
+ }
+ mReadyStartTimeMs = INVALID_TIME;
+ mPreparingStartTimeMs = INVALID_TIME;
+ mBufferingStartTimeMs = INVALID_TIME;
+ if (playbackState == ExoPlayer.STATE_READY) {
+ if (DEBUG) {
+ Log.d(TAG, "ExoPlayer ready");
+ }
+ if (!mPlayerStarted) {
+ sendMessage(MSG_START_PLAYBACK, System.identityHashCode(mPlayer));
+ }
+ mReadyStartTimeMs = SystemClock.elapsedRealtime();
+ } else if (playbackState == ExoPlayer.STATE_PREPARING) {
+ mPreparingStartTimeMs = SystemClock.elapsedRealtime();
+ } else if (playbackState == ExoPlayer.STATE_BUFFERING) {
+ mBufferingStartTimeMs = SystemClock.elapsedRealtime();
+ } else if (playbackState == ExoPlayer.STATE_ENDED) {
+ // Final status
+ // notification of STATE_ENDED from MpegTsPlayer will be ignored afterwards.
+ Log.i(TAG, "Player ended: end of stream");
+ if (mChannel != null) {
+ sendMessage(MSG_RETRY_PLAYBACK, System.identityHashCode(mPlayer));
+ }
+ }
+ mPlayerState = playbackState;
+ }
+
+ @Override
+ public void onError(Exception e) {
+ if (TunerPreferences.getStoreTsStream(mContext)) {
+ // Crash intentionally to capture the error causing TS file.
+ Log.e(
+ TAG,
+ "Crash intentionally to capture the error causing TS file. " + e.getMessage());
+ SoftPreconditions.checkState(false);
+ }
+ // There maybe some errors that finally raise ExoPlaybackException and will be handled here.
+ // If we are playing live stream, retrying playback maybe helpful. But for recorded stream,
+ // retrying playback is not helpful.
+ if (mChannel != null) {
+ mHandler.obtainMessage(MSG_RETRY_PLAYBACK, System.identityHashCode(mPlayer))
+ .sendToTarget();
+ }
+ }
+
+ @Override
+ public void onVideoSizeChanged(int width, int height, float pixelWidthHeight) {
+ if (mChannel != null && mChannel.hasVideo()) {
+ updateVideoTrack(width, height);
+ }
+ if (mRecordingId != null) {
+ updateVideoTrack(width, height);
+ }
+ }
+
+ @Override
+ public void onDrawnToSurface(MpegTsPlayer player, Surface surface) {
+ if (mSurface != null && mPlayerStarted) {
+ if (DEBUG) {
+ Log.d(TAG, "MSG_DRAWN_TO_SURFACE");
+ }
+ if (mRecordingId != null) {
+ // Workaround of b/33298048: set it to 1 instead of 0.
+ mBufferStartTimeMs = mRecordStartTimeMs = 1;
+ } else {
+ mBufferStartTimeMs = mRecordStartTimeMs = System.currentTimeMillis();
+ }
+ notifyVideoAvailable();
+ mReportedDrawnToSurface = true;
+
+ // If surface is drawn successfully, it means that the playback was brought back
+ // to normal and therefore, the playback recovery status will be reset through
+ // setting a zero value to the retry count.
+ // TODO: Consider audio only channels for detecting playback status changes to
+ // be normal.
+ mRetryCount = 0;
+ if (mCaptionEnabled && mCaptionTrack != null) {
+ startCaptionTrack();
+ } else {
+ stopCaptionTrack();
+ }
+ mHandler.sendEmptyMessage(MSG_NOTIFY_AUDIO_TRACK_UPDATED);
+ }
+ }
+
+ @Override
+ public void onSmoothTrickplayForceStopped() {
+ if (mPlayer == null || !mHandler.hasMessages(MSG_SMOOTH_TRICKPLAY_MONITOR)) {
+ return;
+ }
+ mHandler.removeMessages(MSG_SMOOTH_TRICKPLAY_MONITOR);
+ doTrickplayBySeek((int) mPlayer.getCurrentPosition());
+ }
+
+ @Override
+ public void onAudioUnplayable() {
+ if (mPlayer == null) {
+ return;
+ }
+ Log.i(TAG, "AC3 audio cannot be played due to device limitation");
+ mTunerSessionOverlay.sendUiMessage(TunerSessionOverlay.MSG_UI_SHOW_AUDIO_UNPLAYABLE);
+ }
+
+ // MpegTsPlayer.VideoEventListener
+ @Override
+ public void onEmitCaptionEvent(Cea708Data.CaptionEvent event) {
+ mTunerSessionOverlay.sendUiMessage(TunerSessionOverlay.MSG_UI_PROCESS_CAPTION_TRACK, event);
+ }
+
+ @Override
+ public void onClearCaptionEvent() {
+ mTunerSessionOverlay.sendUiMessage(TunerSessionOverlay.MSG_UI_CLEAR_CAPTION_RENDERER);
+ }
+
+ @Override
+ public void onDiscoverCaptionServiceNumber(int serviceNumber) {
+ sendMessage(MSG_DISCOVER_CAPTION_SERVICE_NUMBER, serviceNumber);
+ }
+
+ // ChannelDataManager.ProgramInfoListener
+ @Override
+ public void onProgramsArrived(TunerChannel channel, List<EitItem> programs) {
+ sendMessage(MSG_SCHEDULE_OF_PROGRAMS, new Pair<>(channel, programs));
+ }
+
+ @Override
+ public void onChannelArrived(TunerChannel channel) {
+ sendMessage(MSG_UPDATE_CHANNEL_INFO, channel);
+ }
+
+ @Override
+ public void onRescanNeeded() {
+ mTunerSessionOverlay.sendUiMessage(TunerSessionOverlay.MSG_UI_TOAST_RESCAN_NEEDED);
+ }
+
+ @Override
+ public void onRequestProgramsResponse(TunerChannel channel, List<EitItem> programs) {
+ sendMessage(MSG_PROGRAM_DATA_RESULT, new Pair<>(channel, programs));
+ }
+
+ // PlaybackBufferListener
+ @Override
+ public void onBufferStartTimeChanged(long startTimeMs) {
+ sendMessage(MSG_BUFFER_START_TIME_CHANGED, startTimeMs);
+ }
+
+ @Override
+ public void onBufferStateChanged(boolean available) {
+ sendMessage(MSG_BUFFER_STATE_CHANGED, available);
+ }
+
+ @Override
+ public void onDiskTooSlow() {
+ mTrickplayDisabledByStorageIssue = true;
+ sendMessage(MSG_RETRY_PLAYBACK, System.identityHashCode(mPlayer));
+ }
+
+ // EventDetector.EventListener
+ @Override
+ public void onChannelDetected(TunerChannel channel, boolean channelArrivedAtFirstTime) {
+ mChannelDataManager.notifyChannelDetected(channel, channelArrivedAtFirstTime);
+ }
+
+ @Override
+ public void onEventDetected(TunerChannel channel, List<EitItem> items) {
+ mChannelDataManager.notifyEventDetected(channel, items);
+ }
+
+ @Override
+ public void onChannelScanDone() {
+ // do nothing.
+ }
+
+ private long parseChannel(Uri uri) {
+ try {
+ List<String> paths = uri.getPathSegments();
+ if (paths.size() > 1 && paths.get(0).equals(PLAY_FROM_CHANNEL)) {
+ return ContentUris.parseId(uri);
+ }
+ } catch (UnsupportedOperationException | NumberFormatException e) {
+ }
+ return -1;
+ }
+
+ private static class RecordedProgram {
+ // private final long mChannelId;
+ private final String mDataUri;
+ private final long mStartTimeMillis;
+
+ private static final String[] PROJECTION = {
+ TvContract.Programs.COLUMN_CHANNEL_ID,
+ TvContract.RecordedPrograms.COLUMN_RECORDING_DATA_URI,
+ TvContract.RecordedPrograms.COLUMN_START_TIME_UTC_MILLIS,
+ };
+
+ public RecordedProgram(Cursor cursor) {
+ int index = 0;
+ // mChannelId = cursor.getLong(index++);
+ index++;
+ mDataUri = cursor.getString(index++);
+ mStartTimeMillis = cursor.getLong(index++);
+ }
+
+ public RecordedProgram(long channelId, String dataUri) {
+ // mChannelId = channelId;
+ mDataUri = dataUri;
+ mStartTimeMillis = 0;
+ }
+
+ public static RecordedProgram onQuery(Cursor c) {
+ RecordedProgram recording = null;
+ if (c != null && c.moveToNext()) {
+ recording = new RecordedProgram(c);
+ }
+ return recording;
+ }
+
+ public String getDataUri() {
+ return mDataUri;
+ }
+
+ public long getStartTime() {
+ return mStartTimeMillis;
+ }
+ }
+
+ private RecordedProgram getRecordedProgram(Uri recordedUri) {
+ ContentResolver resolver = mContext.getContentResolver();
+ try (Cursor c = resolver.query(recordedUri, RecordedProgram.PROJECTION, null, null, null)) {
+ if (c != null) {
+ RecordedProgram result = RecordedProgram.onQuery(c);
+ if (DEBUG) {
+ Log.d(TAG, "Finished query for " + this);
+ }
+ return result;
+ } else {
+ if (c == null) {
+ Log.e(TAG, "Unknown query error for " + this);
+ } else {
+ if (DEBUG) {
+ Log.d(TAG, "Canceled query for " + this);
+ }
+ }
+ return null;
+ }
+ }
+ }
+
+ private String parseRecording(Uri uri) {
+ RecordedProgram recording = getRecordedProgram(uri);
+ if (recording != null) {
+ mRecordedProgramStartTimeMs = recording.getStartTime();
+ return recording.getDataUri();
+ }
+ return null;
+ }
+
+ @Override
+ public boolean handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_TUNE:
+ return handleMessageTune((Uri) msg.obj);
+ case MSG_STOP_TUNE:
+ return handleMessageStopTune();
+ case MSG_RELEASE:
+ return handleMessageRelease();
+ case MSG_RETRY_PLAYBACK:
+ return handleMessageRetryPlayback((int) msg.obj);
+ case MSG_RESET_PLAYBACK:
+ return handleMessageResetPlayback();
+ case MSG_START_PLAYBACK:
+ return handleMessageStartPlayback((int) msg.obj);
+ case MSG_UPDATE_PROGRAM:
+ return handleMessageUpdateProgram((EitItem) msg.obj);
+ case MSG_SCHEDULE_OF_PROGRAMS:
+ // TODO: fix the unchecked cast waring.
+ Pair<TunerChannel, List<EitItem>> pair =
+ (Pair<TunerChannel, List<EitItem>>) msg.obj;
+ return handleMessageScheduleOfPrograms(pair);
+ case MSG_UPDATE_CHANNEL_INFO:
+ return handleMessageUpdateChannelInfo((TunerChannel) msg.obj);
+ case MSG_PROGRAM_DATA_RESULT:
+ return handleMessageProgramDataResult(msg);
+ case MSG_TRICKPLAY_BY_SEEK:
+ return handleMessageTrickplayBySeek(msg.arg1);
+ case MSG_SMOOTH_TRICKPLAY_MONITOR:
+ return handleMessageSmoothTrickplayMonitor();
+ case MSG_RESCHEDULE_PROGRAMS:
+ return handleMessageReschedulePrograms();
+ case MSG_PARENTAL_CONTROLS:
+ return handleMessageParentalControl();
+ case MSG_UNBLOCKED_RATING:
+ return handleMessageUnblockedRating((TvContentRating) msg.obj);
+ case MSG_DISCOVER_CAPTION_SERVICE_NUMBER:
+ return handleMessageDiscoverCaptionServiceNumber((int) msg.obj);
+ case MSG_SELECT_TRACK:
+ return handleMessageSelectTrack(msg.arg1, (String) msg.obj);
+ case MSG_UPDATE_CAPTION_TRACK:
+ return handleMessageUpdateCaptionTrack();
+ case MSG_TIMESHIFT_PAUSE:
+ return handleMessageTimeshiftPause();
+ case MSG_TIMESHIFT_RESUME:
+ return handleMessageTimeshiftResume();
+ case MSG_TIMESHIFT_SEEK_TO:
+ return handleMessageTimeshiftSeekTo((long) msg.obj);
+ case MSG_TIMESHIFT_SET_PLAYBACKPARAMS:
+ return handleMessageTimeshiftSetPlaybackParams((PlaybackParams) msg.obj);
+ case MSG_SET_STREAM_VOLUME:
+ return handleMessageSetStreamVolume();
+ case MSG_TUNER_PREFERENCES_CHANGED:
+ return handleMessageTunerPreferencesChanged();
+ case MSG_BUFFER_START_TIME_CHANGED:
+ return handleMessageBufferStartTimeChanged((long) msg.obj);
+ case MSG_BUFFER_STATE_CHANGED:
+ return handleMessageBufferStateChanged((boolean) msg.obj);
+ case MSG_CHECK_SIGNAL:
+ return handleMessageCheckSignal();
+ case MSG_SET_SURFACE:
+ return handleMessageSetSurface();
+ case MSG_NOTIFY_AUDIO_TRACK_UPDATED:
+ return handleMessageAudioTrackUpdated();
+ case MSG_CHECK_SIGNAL_STRENGTH:
+ return handleMessageCheckSignalStrength();
+ default:
+ return unhandledMessage(msg);
+ }
+ }
+
+ private boolean handleMessageTune(Uri channelUri) {
+ if (DEBUG) {
+ Log.d(TAG, "MSG_TUNE");
+ }
+
+ // When sequential tuning messages arrived, it skips middle tuning messages in
+ // order
+ // to change to the last requested channel quickly.
+ if (mHandler.hasMessages(MSG_TUNE)) {
+ return true;
+ }
+ notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING);
+ if (!mIsActiveSession) {
+ // Wait until release is finished if there is a pending release.
+ try {
+ while (!sActiveSessionSemaphore.tryAcquire(
+ RELEASE_WAIT_INTERVAL_MS, TimeUnit.MILLISECONDS)) {
+ synchronized (mReleaseLock) {
+ if (mReleaseRequested) {
+ return true;
+ }
+ }
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ synchronized (mReleaseLock) {
+ if (mReleaseRequested) {
+ sActiveSessionSemaphore.release();
+ return true;
+ }
+ }
+ mIsActiveSession = true;
+ }
+ String recording = null;
+ long channelId = parseChannel(channelUri);
+ TunerChannel channel = (channelId == -1) ? null : mChannelDataManager.getChannel(channelId);
+ if (channelId == -1) {
+ recording = parseRecording(channelUri);
+ }
+ if (channel == null && recording == null) {
+ Log.w(TAG, "onTune() is failed. Can't find channel for " + channelUri);
+ stopTune();
+ notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN);
+ return true;
+ }
+ clearCallbacksAndMessagesSafely();
+ mChannelDataManager.removeAllCallbacksAndMessages();
+ if (channel != null) {
+ mChannelDataManager.requestProgramsData(channel);
+ }
+ prepareTune(channel, recording);
+ // TODO: Need to refactor. notifyContentAllowed() should not be called if
+ // parental
+ // control is turned on.
+ mSession.notifyContentAllowed();
+ resetTvTracks();
+ resetPlayback();
+ mHandler.sendEmptyMessageDelayed(
+ MSG_RESCHEDULE_PROGRAMS, RESCHEDULE_PROGRAMS_INITIAL_DELAY_MS);
+ return true;
+ }
+
+ private boolean handleMessageStopTune() {
+ if (DEBUG) {
+ Log.d(TAG, "MSG_STOP_TUNE");
+ }
+ mChannel = null;
+ stopPlayback(true);
+ stopCaptionTrack();
+ resetTvTracks();
+ notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN);
+ return true;
+ }
+
+ private boolean handleMessageRelease() {
+ if (DEBUG) {
+ Log.d(TAG, "MSG_RELEASE");
+ }
+ mHandler.removeCallbacksAndMessages(null);
+ stopPlayback(true);
+ stopCaptionTrack();
+ mSourceManager.release();
+ mHandler.getLooper().quitSafely();
+ if (mIsActiveSession) {
+ sActiveSessionSemaphore.release();
+ }
+ return true;
+ }
+
+ private boolean handleMessageRetryPlayback(int code) {
+ if (System.identityHashCode(mPlayer) == code) {
+ Log.i(TAG, "Retrying the playback for channel: " + mChannel);
+ mHandler.removeMessages(MSG_RETRY_PLAYBACK);
+ // When there is a request of retrying playback, don't reuse TunerHal.
+ mSourceManager.setKeepTuneStatus(false);
+ mRetryCount++;
+ if (DEBUG) {
+ Log.d(TAG, "MSG_RETRY_PLAYBACK " + mRetryCount);
+ }
+ mChannelDataManager.removeAllCallbacksAndMessages();
+ if (mRetryCount <= MAX_IMMEDIATE_RETRY_COUNT) {
+ resetPlayback();
+ } else {
+ // When it reaches this point, it may be due to an error that occurred
+ // in
+ // the tuner device. Calling stopPlayback() resets the tuner device
+ // to recover from the error.
+ stopPlayback(false);
+ stopCaptionTrack();
+
+ notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL);
+ Log.i(TAG, "Notify weak signal since fail to retry playback");
+
+ // After MAX_IMMEDIATE_RETRY_COUNT, give some delay of an empirically
+ // chosen
+ // value before recovering the playback.
+ mHandler.sendEmptyMessageDelayed(
+ MSG_RESET_PLAYBACK, RECOVER_STOPPED_PLAYBACK_PERIOD_MS);
+ }
+ }
+ return true;
+ }
+
+ private boolean handleMessageResetPlayback() {
+ if (DEBUG) {
+ Log.d(TAG, "MSG_RESET_PLAYBACK");
+ }
+ mChannelDataManager.removeAllCallbacksAndMessages();
+ resetPlayback();
+ return true;
+ }
+
+ private boolean handleMessageStartPlayback(int playerHashCode) {
+ if (DEBUG) {
+ Log.d(TAG, "MSG_START_PLAYBACK");
+ }
+ if (mChannel != null || mRecordingId != null) {
+ startPlayback(playerHashCode);
+ }
+ return true;
+ }
+
+ private boolean handleMessageUpdateProgram(EitItem program) {
+ if (mChannel != null) {
+ updateTvTracks(program, false);
+ mHandler.sendEmptyMessage(MSG_PARENTAL_CONTROLS);
+ }
+ return true;
+ }
+
+ private boolean handleMessageScheduleOfPrograms(Pair<TunerChannel, List<EitItem>> pair) {
+ mHandler.removeMessages(MSG_UPDATE_PROGRAM);
+ TunerChannel channel = pair.first;
+ if (mChannel == null) {
+ return true;
+ }
+ if (mChannel != null && mChannel.compareTo(channel) != 0) {
+ return true;
+ }
+ mPrograms = pair.second;
+ EitItem currentProgram = getCurrentProgram();
+ if (currentProgram == null) {
+ mProgram = null;
+ }
+ long currentTimeMs = getCurrentPosition();
+ if (mPrograms != null) {
+ for (EitItem item : mPrograms) {
+ if (currentProgram != null && currentProgram.compareTo(item) == 0) {
+ if (DEBUG) {
+ Log.d(TAG, "Update current TvTracks " + item);
+ }
+ if (mProgram != null && mProgram.compareTo(item) == 0) {
+ continue;
+ }
+ mProgram = item;
+ updateTvTracks(item, false);
+ } else if (item.getStartTimeUtcMillis() > currentTimeMs) {
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "Update next TvTracks "
+ + item
+ + " "
+ + (item.getStartTimeUtcMillis() - currentTimeMs));
+ }
+ mHandler.sendMessageDelayed(
+ mHandler.obtainMessage(MSG_UPDATE_PROGRAM, item),
+ item.getStartTimeUtcMillis() - currentTimeMs);
+ }
+ }
+ }
+ mHandler.sendEmptyMessage(MSG_PARENTAL_CONTROLS);
+ return true;
+ }
+
+ private boolean handleMessageUpdateChannelInfo(TunerChannel tunerChannel) {
+ if (mChannel != null && mChannel.compareTo(tunerChannel) == 0) {
+ updateChannelInfo(tunerChannel);
+ }
+ return true;
+ }
+
+ private boolean handleMessageProgramDataResult(Message msg) {
+ TunerChannel channel = (TunerChannel) ((Pair) msg.obj).first;
+
+ // If there already exists, skip it since real-time data is a top priority,
+ if (mChannel != null
+ && mChannel.compareTo(channel) == 0
+ && mPrograms == null
+ && mProgram == null) {
+ sendMessage(MSG_SCHEDULE_OF_PROGRAMS, msg.obj);
+ }
+ return true;
+ }
+
+ private boolean handleMessageTrickplayBySeek(int seekPositionMs) {
+ if (mPlayer == null) {
+ return true;
+ }
+ if (mRecordingId != null) {
+ long systemBufferTime =
+ System.currentTimeMillis() - SEEK_MARGIN_MS - mRecordedProgramStartTimeMs;
+ if (seekPositionMs > systemBufferTime) {
+ doTimeShiftResume();
+ return true;
+ }
+ }
+ doTrickplayBySeek(seekPositionMs);
+ return true;
+ }
+
+ private boolean handleMessageSmoothTrickplayMonitor() {
+ if (mPlayer == null) {
+ return true;
+ }
+ long systemCurrentTime = System.currentTimeMillis();
+ long position = getCurrentPosition();
+ if (mRecordingId == null) {
+ // Checks if the position exceeds the upper bound when forwarding,
+ // or exceed the lower bound when rewinding.
+ // If the direction is not checked, there can be some issues.
+ // (See b/29939781 for more details.)
+ if ((position > systemCurrentTime && mPlaybackParams.getSpeed() > 0L)
+ || (position < mBufferStartTimeMs && mPlaybackParams.getSpeed() < 0L)) {
+ doTimeShiftResume();
+ return true;
+ }
+ } else {
+ if (position > mRecordingDuration || position < 0) {
+ doTimeShiftPause();
+ return true;
+ }
+ long systemBufferTime =
+ systemCurrentTime - SEEK_MARGIN_MS - mRecordedProgramStartTimeMs;
+ if (position > systemBufferTime) {
+ doTimeShiftResume();
+ return true;
+ }
+ }
+ mHandler.sendEmptyMessageDelayed(
+ MSG_SMOOTH_TRICKPLAY_MONITOR, TRICKPLAY_MONITOR_INTERVAL_MS);
+ return true;
+ }
+
+ private boolean handleMessageReschedulePrograms() {
+ if (mHandler.hasMessages(MSG_SCHEDULE_OF_PROGRAMS)) {
+ mHandler.sendEmptyMessage(MSG_RESCHEDULE_PROGRAMS);
+ } else {
+ doReschedulePrograms();
+ }
+ return true;
+ }
+
+ private boolean handleMessageParentalControl() {
+ doParentalControls();
+ mHandler.removeMessages(MSG_PARENTAL_CONTROLS);
+ mHandler.sendEmptyMessageDelayed(MSG_PARENTAL_CONTROLS, PARENTAL_CONTROLS_INTERVAL_MS);
+ return true;
+ }
+
+ private boolean handleMessageUnblockedRating(TvContentRating unblockedContentRating) {
+ mUnblockedContentRating = unblockedContentRating;
+ return handleMessageParentalControl();
+ }
+
+ private boolean handleMessageDiscoverCaptionServiceNumber(int serviceNumber) {
+ doDiscoverCaptionServiceNumber(serviceNumber);
+ return true;
+ }
+
+ private boolean handleMessageSelectTrack(int type, String trackId) {
+ if (mPlayer == null) {
+ Log.w(TAG, "mPlayer is null when doselectTrack is called");
+ return false;
+ }
+ if (mChannel != null || mRecordingId != null) {
+ doSelectTrack(type, trackId);
+ }
+ return true;
+ }
+
+ private boolean handleMessageUpdateCaptionTrack() {
+ if (mCaptionEnabled) {
+ startCaptionTrack();
+ } else {
+ stopCaptionTrack();
+ }
+ return true;
+ }
+
+ private boolean handleMessageTimeshiftPause() {
+ if (DEBUG) {
+ Log.d(TAG, "MSG_TIMESHIFT_PAUSE");
+ }
+ if (mPlayer == null) {
+ return true;
+ }
+ setTrickplayEnabledIfNeeded();
+ doTimeShiftPause();
+ return true;
+ }
+
+ private boolean handleMessageTimeshiftResume() {
+ if (DEBUG) {
+ Log.d(TAG, "MSG_TIMESHIFT_RESUME");
+ }
+ if (mPlayer == null) {
+ return true;
+ }
+ setTrickplayEnabledIfNeeded();
+ doTimeShiftResume();
+ return true;
+ }
+
+ private boolean handleMessageTimeshiftSeekTo(long timeMs) {
+ if (DEBUG) {
+ Log.d(TAG, "MSG_TIMESHIFT_SEEK_TO (position=" + timeMs + ")");
+ }
+ if (mPlayer == null) {
+ return true;
+ }
+ setTrickplayEnabledIfNeeded();
+ doTimeShiftSeekTo(timeMs);
+ return true;
+ }
+
+ private boolean handleMessageTimeshiftSetPlaybackParams(PlaybackParams playbackParams) {
+ if (mPlayer == null) {
+ return true;
+ }
+ setTrickplayEnabledIfNeeded();
+ doTimeShiftSetPlaybackParams(playbackParams);
+ return true;
+ }
+
+ private boolean handleMessageAudioCapabilitiesChanged(AudioCapabilities audioCapabilities) {
+ if (DEBUG) {
+ Log.d(TAG, "MSG_AUDIO_CAPABILITIES_CHANGED " + audioCapabilities);
+ }
+ if (audioCapabilities == null) {
+ return true;
+ }
+ if (!audioCapabilities.equals(mAudioCapabilities)) {
+ // HDMI supported encodings are changed. restart player.
+ mAudioCapabilities = audioCapabilities;
+ resetPlayback();
+ }
+ return true;
+ }
+
+ private boolean handleMessageSetStreamVolume() {
+ if (mPlayer != null && mPlayer.isPlaying()) {
+ mPlayer.setVolume(mVolume);
+ }
+ return true;
+ }
+
+ private boolean handleMessageTunerPreferencesChanged() {
+ mHandler.removeMessages(MSG_TUNER_PREFERENCES_CHANGED);
+ @TrickplaySetting int trickplaySetting = TunerPreferences.getTrickplaySetting(mContext);
+ if (trickplaySetting != mTrickplaySetting) {
+ boolean wasTrcikplayEnabled =
+ mTrickplaySetting != TunerPreferences.TRICKPLAY_SETTING_DISABLED;
+ boolean isTrickplayEnabled =
+ trickplaySetting != TunerPreferences.TRICKPLAY_SETTING_DISABLED;
+ mTrickplaySetting = trickplaySetting;
+ if (isTrickplayEnabled != wasTrcikplayEnabled) {
+ sendMessage(MSG_RESET_PLAYBACK, System.identityHashCode(mPlayer));
+ }
+ }
+ return true;
+ }
+
+ private boolean handleMessageBufferStartTimeChanged(long bufferStartTimeMs) {
+ if (mPlayer == null) {
+ return true;
+ }
+ mBufferStartTimeMs = bufferStartTimeMs;
+ if (!hasEnoughBackwardBuffer()
+ && (!mPlayer.isPlaying() || mPlaybackParams.getSpeed() < 1.0f)) {
+ mPlayer.setPlayWhenReady(true);
+ mPlayer.setAudioTrackAndClosedCaption(true);
+ mPlaybackParams.setSpeed(1.0f);
+ }
+ return true;
+ }
+
+ private boolean handleMessageBufferStateChanged(boolean available) {
+ mSession.notifyTimeShiftStatusChanged(
+ available
+ ? TvInputManager.TIME_SHIFT_STATUS_AVAILABLE
+ : TvInputManager.TIME_SHIFT_STATUS_UNAVAILABLE);
+ return true;
+ }
+
+ private boolean handleMessageCheckSignal() {
+ if (mChannel == null || mPlayer == null) {
+ return true;
+ }
+ TsDataSource source = mPlayer.getDataSource();
+ long limitInBytes = source != null ? source.getBufferedPosition() : 0L;
+ if (TunerDebug.ENABLED) {
+ TunerDebug.calculateDiff();
+ mTunerSessionOverlay.sendUiMessage(
+ TunerSessionOverlay.MSG_UI_SET_STATUS_TEXT,
+ Html.fromHtml(
+ StatusTextUtils.getStatusWarningInHTML(
+ (limitInBytes - mLastLimitInBytes) / TS_PACKET_SIZE,
+ TunerDebug.getVideoFrameDrop(),
+ TunerDebug.getBytesInQueue(),
+ TunerDebug.getAudioPositionUs(),
+ TunerDebug.getAudioPositionUsRate(),
+ TunerDebug.getAudioPtsUs(),
+ TunerDebug.getAudioPtsUsRate(),
+ TunerDebug.getVideoPtsUs(),
+ TunerDebug.getVideoPtsUsRate())));
+ }
+ mTunerSessionOverlay.sendUiMessage(TunerSessionOverlay.MSG_UI_HIDE_MESSAGE);
+ long currentTime = SystemClock.elapsedRealtime();
+ long bufferingTimeMs =
+ mBufferingStartTimeMs != INVALID_TIME
+ ? currentTime - mBufferingStartTimeMs
+ : mBufferingStartTimeMs;
+ long preparingTimeMs =
+ mPreparingStartTimeMs != INVALID_TIME
+ ? currentTime - mPreparingStartTimeMs
+ : mPreparingStartTimeMs;
+ boolean isBufferingTooLong = bufferingTimeMs > PLAYBACK_STATE_CHANGED_WAITING_THRESHOLD_MS;
+ boolean isPreparingTooLong = preparingTimeMs > PLAYBACK_STATE_CHANGED_WAITING_THRESHOLD_MS;
+ boolean isWeakSignal =
+ source != null
+ && mChannel.getType() != Channel.TunerType.TYPE_FILE
+ && (isBufferingTooLong || isPreparingTooLong);
+ if (isWeakSignal && !mReportedWeakSignal) {
+ if (!mHandler.hasMessages(MSG_RETRY_PLAYBACK)) {
+ mHandler.sendMessageDelayed(
+ mHandler.obtainMessage(
+ MSG_RETRY_PLAYBACK, System.identityHashCode(mPlayer)),
+ PLAYBACK_RETRY_DELAY_MS);
+ }
+ if (mPlayer != null) {
+ mPlayer.setAudioTrackAndClosedCaption(false);
+ }
+ notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL);
+ if (CommonFeatures.TUNER_SIGNAL_STRENGTH.isEnabled(mContext)) {
+ mSession.notifySignalStrength(0);
+ mHandler.removeMessages(MSG_CHECK_SIGNAL_STRENGTH);
+ }
+ Log.i(
+ TAG,
+ "Notify weak signal due to signal check, "
+ + String.format(
+ "packetsPerSec:%d, bufferingTimeMs:%d, preparingTimeMs:%d, "
+ + "videoFrameDrop:%d",
+ (limitInBytes - mLastLimitInBytes) / TS_PACKET_SIZE,
+ bufferingTimeMs,
+ preparingTimeMs,
+ TunerDebug.getVideoFrameDrop()));
+ } else if (!isWeakSignal && mReportedWeakSignal) {
+ boolean isPlaybackStable =
+ mReadyStartTimeMs != INVALID_TIME
+ && currentTime - mReadyStartTimeMs
+ > PLAYBACK_STATE_CHANGED_WAITING_THRESHOLD_MS;
+ if (!isPlaybackStable) {
+ // Wait until playback becomes stable.
+ } else if (mReportedDrawnToSurface) {
+ mHandler.removeMessages(MSG_RETRY_PLAYBACK);
+ notifyVideoAvailable();
+ mPlayer.setAudioTrackAndClosedCaption(true);
+ if (mHandler.hasMessages(MSG_CHECK_SIGNAL_STRENGTH)) {
+ mHandler.removeMessages(MSG_CHECK_SIGNAL_STRENGTH);
+ }
+ if (CommonFeatures.TUNER_SIGNAL_STRENGTH.isEnabled(mContext)) {
+ sendMessage(MSG_CHECK_SIGNAL_STRENGTH);
+ }
+ }
+ }
+ mLastLimitInBytes = limitInBytes;
+ mHandler.sendEmptyMessageDelayed(MSG_CHECK_SIGNAL, CHECK_NO_SIGNAL_PERIOD_MS);
+ return true;
+ }
+
+ private boolean handleMessageSetSurface() {
+ if (mPlayer != null) {
+ mPlayer.setSurface(mSurface);
+ } else {
+ // TODO: Since surface is dynamically set, we can remove the dependency of
+ // playback start on mSurface nullity.
+ resetPlayback();
+ }
+ return true;
+ }
+
+ private boolean handleMessageAudioTrackUpdated() {
+ notifyAudioTracksUpdated();
+ return true;
+ }
+
+ private boolean handleMessageCheckSignalStrength() {
+ if (CommonFeatures.TUNER_SIGNAL_STRENGTH.isEnabled(mContext)) {
+ int signal;
+ if (mPlayer != null) {
+ TsDataSource source = mPlayer.getDataSource();
+ if (source != null) {
+ signal = source.getSignalStrength();
+ return handleSignal(signal);
+ }
+ }
+ }
+ return false;
+ }
+
+ @VisibleForTesting
+ protected boolean handleSignal(int signal) {
+ if (signal == TvInputConstantCompat.SIGNAL_STRENGTH_NOT_USED
+ || signal == TvInputConstantCompat.SIGNAL_STRENGTH_ERROR) {
+ notifySignal(signal);
+ return true;
+ }
+ if (signal != mSignalStrength && signal >= 0) {
+ notifySignal(signal);
+ }
+ mHandler.sendEmptyMessageDelayed(
+ MSG_CHECK_SIGNAL_STRENGTH, CHECK_SIGNAL_STRENGTH_INTERVAL_MS);
+ return true;
+ }
+
+ @VisibleForTesting
+ protected void notifySignal(int signal) {
+ mSession.notifySignalStrength(signal);
+ mSignalStrength = signal;
+ }
+
+ private boolean unhandledMessage(Message msg) {
+ Log.w(TAG, "Unhandled message code: " + msg.what);
+ return false;
+ }
+
+ // Private methods
+ private void doSelectTrack(int type, String trackId) {
+ int numTrackId =
+ trackId != null ? Integer.parseInt(trackId.substring(TRACK_PREFIX_SIZE)) : -1;
+ if (type == TvTrackInfo.TYPE_AUDIO) {
+ if (trackId == null) {
+ return;
+ }
+ if (numTrackId != mPlayer.getSelectedTrack(MpegTsPlayer.TRACK_TYPE_AUDIO)) {
+ mPlayer.setSelectedTrack(MpegTsPlayer.TRACK_TYPE_AUDIO, numTrackId);
+ }
+ mSession.notifyTrackSelected(type, trackId);
+ } else if (type == TvTrackInfo.TYPE_SUBTITLE) {
+ if (trackId == null) {
+ mSession.notifyTrackSelected(type, null);
+ mCaptionTrack = null;
+ stopCaptionTrack();
+ return;
+ }
+ for (TvTrackInfo track : mTvTracks) {
+ if (track.getId().equals(trackId)) {
+ // The service number of the caption service is used for track id of a
+ // subtitle track. Passes the following track id on to TsParser.
+ mSession.notifyTrackSelected(type, trackId);
+ mCaptionTrack = mCaptionTrackMap.get(numTrackId);
+ startCaptionTrack();
+ return;
+ }
+ }
+ }
+ }
+
+ private void setTrickplayEnabledIfNeeded() {
+ if (mChannel == null
+ || mTrickplayModeCustomization != CustomizationManager.TRICKPLAY_MODE_ENABLED) {
+ return;
+ }
+ if (mTrickplaySetting == TunerPreferences.TRICKPLAY_SETTING_NOT_SET) {
+ mTrickplaySetting = TunerPreferences.TRICKPLAY_SETTING_ENABLED;
+ TunerPreferences.setTrickplaySetting(mContext, mTrickplaySetting);
+ }
+ }
+
+ @VisibleForTesting
+ protected MpegTsPlayer createPlayer(AudioCapabilities capabilities) {
+ if (capabilities == null) {
+ Log.w(TAG, "No Audio Capabilities");
+ }
+ mSourceManager.setKeepTuneStatus(true);
+ long now = System.currentTimeMillis();
+ if (mTrickplayModeCustomization == CustomizationManager.TRICKPLAY_MODE_ENABLED
+ && mTrickplaySetting == TunerPreferences.TRICKPLAY_SETTING_NOT_SET) {
+ if (mTrickplayExpiredMs == 0) {
+ mTrickplayExpiredMs = now + TRICKPLAY_OFF_DURATION_MS;
+ TunerPreferences.setTrickplayExpiredMs(mContext, mTrickplayExpiredMs);
+ } else {
+ if (mTrickplayExpiredMs < now) {
+ mTrickplaySetting = TunerPreferences.TRICKPLAY_SETTING_DISABLED;
+ TunerPreferences.setTrickplaySetting(mContext, mTrickplaySetting);
+ }
+ }
+ }
+ BufferManager bufferManager = null;
+ if (mRecordingId != null) {
+ StorageManager storageManager =
+ new DvrStorageManager(new File(getRecordingPath()), false);
+ bufferManager = new BufferManager(storageManager);
+ updateCaptionTracks(((DvrStorageManager) storageManager).readCaptionInfoFiles());
+ } else if (!mTrickplayDisabledByStorageIssue
+ && mTrickplaySetting != TunerPreferences.TRICKPLAY_SETTING_DISABLED
+ && mMaxTrickplayBufferSizeMb >= MIN_BUFFER_SIZE_DEF) {
+ bufferManager =
+ new BufferManager(
+ new TrickplayStorageManager(
+ mContext,
+ mTrickplayBufferDir,
+ 1024L * 1024 * mMaxTrickplayBufferSizeMb));
+ } else {
+ Log.w(TAG, "Trickplay is disabled.");
+ }
+ MpegTsPlayer player =
+ new MpegTsPlayer(
+ new MpegTsRendererBuilder(
+ mContext, bufferManager, this, mConcurrentDvrPlaybackFlags),
+ mHandler,
+ mSourceManager,
+ capabilities,
+ this);
+ Log.i(TAG, "Passthrough AC3 renderer");
+ if (DEBUG) {
+ Log.d(TAG, "ExoPlayer created");
+ }
+ player.setCaptionServiceNumber(Cea708Data.EMPTY_SERVICE_NUMBER);
+ player.setVideoEventListener(this);
+ player.setCaptionServiceNumber(
+ mCaptionTrack != null
+ ? mCaptionTrack.serviceNumber
+ : Cea708Data.EMPTY_SERVICE_NUMBER);
+ return player;
+ }
+
+ private void startCaptionTrack() {
+ if (mCaptionEnabled && mCaptionTrack != null) {
+ mTunerSessionOverlay.sendUiMessage(
+ TunerSessionOverlay.MSG_UI_START_CAPTION_TRACK, mCaptionTrack);
+ if (mPlayer != null) {
+ mPlayer.setCaptionServiceNumber(mCaptionTrack.serviceNumber);
+ }
+ }
+ }
+
+ private void stopCaptionTrack() {
+ if (mPlayer != null) {
+ mPlayer.setCaptionServiceNumber(Cea708Data.EMPTY_SERVICE_NUMBER);
+ }
+ mTunerSessionOverlay.sendUiMessage(TunerSessionOverlay.MSG_UI_STOP_CAPTION_TRACK);
+ }
+
+ private void resetTvTracks() {
+ mTvTracks.clear();
+ mAudioTrackMap.clear();
+ mCaptionTrackMap.clear();
+ mTunerSessionOverlay.sendUiMessage(TunerSessionOverlay.MSG_UI_RESET_CAPTION_TRACK);
+ mSession.notifyTracksChanged(mTvTracks);
+ }
+
+ private void updateTvTracks(TvTracksInterface tvTracksInterface, boolean fromPmt) {
+ synchronized (tvTracksInterface) {
+ if (DEBUG) {
+ Log.d(TAG, "UpdateTvTracks " + tvTracksInterface);
+ }
+ List<AtscAudioTrack> audioTracks = tvTracksInterface.getAudioTracks();
+ List<AtscCaptionTrack> captionTracks = tvTracksInterface.getCaptionTracks();
+ // According to ATSC A/69 chapter 6.9, both PMT and EIT should have descriptors for
+ // audio
+ // tracks, but in real world, we see some bogus audio track info in EIT, so, we trust
+ // audio
+ // track info in PMT more and use info in EIT only when we have nothing.
+ if (audioTracks != null
+ && !audioTracks.isEmpty()
+ && (mChannel == null || mChannel.getAudioTracks() == null || fromPmt)) {
+ updateAudioTracks(audioTracks);
+ }
+ if (captionTracks == null || captionTracks.isEmpty()) {
+ if (tvTracksInterface.hasCaptionTrack()) {
+ updateCaptionTracks(captionTracks);
+ }
+ } else {
+ updateCaptionTracks(captionTracks);
+ }
+ }
+ }
+
+ private void removeTvTracks(int trackType) {
+ Iterator<TvTrackInfo> iterator = mTvTracks.iterator();
+ while (iterator.hasNext()) {
+ TvTrackInfo tvTrackInfo = iterator.next();
+ if (tvTrackInfo.getType() == trackType) {
+ iterator.remove();
+ }
+ }
+ }
+
+ private void updateVideoTrack(int width, int height) {
+ removeTvTracks(TvTrackInfo.TYPE_VIDEO);
+ mTvTracks.add(
+ new TvTrackInfo.Builder(TvTrackInfo.TYPE_VIDEO, VIDEO_TRACK_ID)
+ .setVideoWidth(width)
+ .setVideoHeight(height)
+ .build());
+ mSession.notifyTracksChanged(mTvTracks);
+ mSession.notifyTrackSelected(TvTrackInfo.TYPE_VIDEO, VIDEO_TRACK_ID);
+ }
+
+ private void updateAudioTracks(List<AtscAudioTrack> audioTracks) {
+ if (DEBUG) {
+ Log.d(TAG, "Update AudioTracks " + audioTracks);
+ }
+ mAudioTrackMap.clear();
+ if (audioTracks != null) {
+ int index = 0;
+ for (AtscAudioTrack audioTrack : audioTracks) {
+ audioTrack.index = index;
+ mAudioTrackMap.put(index, audioTrack);
+ ++index;
+ }
+ }
+ mHandler.sendEmptyMessage(MSG_NOTIFY_AUDIO_TRACK_UPDATED);
+ }
+
+ private void notifyAudioTracksUpdated() {
+ if (mPlayer == null) {
+ // Audio tracks will be updated later once player initialization is done.
+ return;
+ }
+ int audioTrackCount = mPlayer.getTrackCount(MpegTsPlayer.TRACK_TYPE_AUDIO);
+ removeTvTracks(TvTrackInfo.TYPE_AUDIO);
+ for (int i = 0; i < audioTrackCount; i++) {
+ // We use language information from EIT/VCT only when the player does not provide
+ // languages.
+ com.google.android.exoplayer.MediaFormat infoFromPlayer =
+ mPlayer.getTrackFormat(MpegTsPlayer.TRACK_TYPE_AUDIO, i);
+ AtscAudioTrack infoFromEit = mAudioTrackMap.get(i);
+ AtscAudioTrack infoFromVct =
+ (mChannel != null
+ && mChannel.getAudioTracks().size() == mAudioTrackMap.size()
+ && i < mChannel.getAudioTracks().size())
+ ? mChannel.getAudioTracks().get(i)
+ : null;
+ String language =
+ !TextUtils.isEmpty(infoFromPlayer.language)
+ ? infoFromPlayer.language
+ : (infoFromEit != null && infoFromEit.language != null)
+ ? infoFromEit.language
+ : (infoFromVct != null && infoFromVct.language != null)
+ ? infoFromVct.language
+ : null;
+ TvTrackInfo.Builder builder =
+ new TvTrackInfo.Builder(TvTrackInfo.TYPE_AUDIO, AUDIO_TRACK_PREFIX + i);
+ builder.setLanguage(language);
+ builder.setAudioChannelCount(infoFromPlayer.channelCount);
+ builder.setAudioSampleRate(infoFromPlayer.sampleRate);
+ TvTrackInfo track = builder.build();
+ mTvTracks.add(track);
+ }
+ mSession.notifyTracksChanged(mTvTracks);
+ }
+
+ private void updateCaptionTracks(List<AtscCaptionTrack> captionTracks) {
+ if (DEBUG) {
+ Log.d(TAG, "Update CaptionTrack " + captionTracks);
+ }
+ removeTvTracks(TvTrackInfo.TYPE_SUBTITLE);
+ mCaptionTrackMap.clear();
+ if (captionTracks != null) {
+ for (AtscCaptionTrack captionTrack : captionTracks) {
+ if (mCaptionTrackMap.indexOfKey(captionTrack.serviceNumber) >= 0) {
+ continue;
+ }
+ String language = captionTrack.language;
+
+ // The service number of the caption service is used for track id of a subtitle.
+ // Later, when a subtitle is chosen, track id will be passed on to TsParser.
+ TvTrackInfo.Builder builder =
+ new TvTrackInfo.Builder(
+ TvTrackInfo.TYPE_SUBTITLE,
+ SUBTITLE_TRACK_PREFIX + captionTrack.serviceNumber);
+ builder.setLanguage(language);
+ mTvTracks.add(builder.build());
+ mCaptionTrackMap.put(captionTrack.serviceNumber, captionTrack);
+ }
+ }
+ mSession.notifyTracksChanged(mTvTracks);
+ }
+
+ private void updateChannelInfo(TunerChannel channel) {
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ String.format(
+ "Channel Info (old) videoPid: %d audioPid: %d " + "audioSize: %d",
+ mChannel.getVideoPid(),
+ mChannel.getAudioPid(),
+ mChannel.getAudioPids().size()));
+ }
+
+ // The list of the audio tracks resided in a channel is often changed depending on a
+ // program being on the air. So, we should update the streaming PIDs and types of the
+ // tuned channel according to the newly received channel data.
+ int oldVideoPid = mChannel.getVideoPid();
+ int oldAudioPid = mChannel.getAudioPid();
+ List<Integer> audioPids = channel.getAudioPids();
+ List<Integer> audioStreamTypes = channel.getAudioStreamTypes();
+ int size = audioPids.size();
+ mChannel.setVideoPid(channel.getVideoPid());
+ mChannel.setAudioPids(audioPids);
+ mChannel.setAudioStreamTypes(audioStreamTypes);
+ updateTvTracks(channel, true);
+ int index = audioPids.isEmpty() ? -1 : 0;
+ for (int i = 0; i < size; ++i) {
+ if (audioPids.get(i) == oldAudioPid) {
+ index = i;
+ break;
+ }
+ }
+ mChannel.selectAudioTrack(index);
+ mSession.notifyTrackSelected(
+ TvTrackInfo.TYPE_AUDIO, index == -1 ? null : AUDIO_TRACK_PREFIX + index);
+
+ // Reset playback if there is a change in the listening streaming PIDs.
+ if (oldVideoPid != mChannel.getVideoPid() || oldAudioPid != mChannel.getAudioPid()) {
+ // TODO: Implement a switching between tracks more smoothly.
+ resetPlayback();
+ }
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ String.format(
+ "Channel Info (new) videoPid: %d audioPid: %d " + " audioSize: %d",
+ mChannel.getVideoPid(),
+ mChannel.getAudioPid(),
+ mChannel.getAudioPids().size()));
+ }
+ }
+
+ private void stopPlayback(boolean removeChannelDataCallbacks) {
+ if (removeChannelDataCallbacks) {
+ mChannelDataManager.removeAllCallbacksAndMessages();
+ }
+ if (mPlayer != null) {
+ mPlayer.setPlayWhenReady(false);
+ mPlayer.release();
+ mPlayer = null;
+ mPlayerState = ExoPlayer.STATE_IDLE;
+ mPlaybackParams.setSpeed(1.0f);
+ mPlayerStarted = false;
+ mReportedDrawnToSurface = false;
+ mPreparingStartTimeMs = INVALID_TIME;
+ mBufferingStartTimeMs = INVALID_TIME;
+ mReadyStartTimeMs = INVALID_TIME;
+ mLastLimitInBytes = 0L;
+ mTunerSessionOverlay.sendUiMessage(TunerSessionOverlay.MSG_UI_HIDE_AUDIO_UNPLAYABLE);
+ mSession.notifyTimeShiftStatusChanged(TvInputManager.TIME_SHIFT_STATUS_UNAVAILABLE);
+ }
+ }
+
+ private void startPlayback(int playerHashCode) {
+ // TODO: provide hasAudio()/hasVideo() for play recordings.
+ if (mPlayer == null || System.identityHashCode(mPlayer) != playerHashCode) {
+ return;
+ }
+ if (mChannel != null && !mChannel.hasAudio()) {
+ if (DEBUG) {
+ Log.d(TAG, "Channel " + mChannel + " does not have audio.");
+ }
+ // Playbacks with video-only stream have not been tested yet.
+ // No video-only channel has been found.
+ notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN);
+ return;
+ }
+ if (mChannel != null
+ && ((mChannel.hasAudio() && !mPlayer.hasAudio())
+ || (mChannel.hasVideo() && !mPlayer.hasVideo()))
+ && mChannel.getType() != Channel.TunerType.TYPE_NETWORK) {
+ // If the channel is from network, skip this part since the video and audio tracks
+ // information for channels from network are more reliable in the extractor. Otherwise,
+ // tracks haven't been detected in the extractor. Try again.
+ sendMessage(MSG_RETRY_PLAYBACK, System.identityHashCode(mPlayer));
+ return;
+ }
+ // Since mSurface is volatile, we define a local variable surface to keep the same value
+ // inside this method.
+ Surface surface = mSurface;
+ if (surface != null && !mPlayerStarted) {
+ mPlayer.setSurface(surface);
+ mPlayer.setPlayWhenReady(true);
+ mPlayer.setVolume(mVolume);
+ if (mChannel != null && mPlayer.hasAudio() && !mPlayer.hasVideo()) {
+ notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_AUDIO_ONLY);
+ } else if (!mReportedWeakSignal) {
+ // Doesn't show buffering during weak signal.
+ notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING);
+ }
+ mTunerSessionOverlay.sendUiMessage(TunerSessionOverlay.MSG_UI_HIDE_MESSAGE);
+ mPlayerStarted = true;
+ }
+ }
+
+ @VisibleForTesting
+ protected void preparePlayback() {
+ MpegTsPlayer player = createPlayer(mAudioCapabilities);
+ if (!player.prepare(mContext, mChannel, mHasSoftwareAudioDecoder, this)) {
+ mSourceManager.setKeepTuneStatus(false);
+ player.release();
+ if (!mHandler.hasMessages(MSG_TUNE)) {
+ // When prepare failed, there may be some errors related to hardware. In that
+ // case, retry playback immediately may not help.
+ notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL);
+ Log.i(TAG, "Notify weak signal due to player preparation failure");
+ mHandler.sendMessageDelayed(
+ mHandler.obtainMessage(
+ MSG_RETRY_PLAYBACK, System.identityHashCode(mPlayer)),
+ PLAYBACK_RETRY_DELAY_MS);
+ }
+ } else {
+ mPlayer = player;
+ mPlayerStarted = false;
+ mHandler.removeMessages(MSG_CHECK_SIGNAL);
+ mHandler.sendEmptyMessageDelayed(MSG_CHECK_SIGNAL, CHECK_NO_SIGNAL_INITIAL_DELAY_MS);
+ if (mHandler.hasMessages(MSG_CHECK_SIGNAL_STRENGTH)) {
+ mHandler.removeMessages(MSG_CHECK_SIGNAL_STRENGTH);
+ }
+ if (CommonFeatures.TUNER_SIGNAL_STRENGTH.isEnabled(mContext)) {
+ mHandler.sendEmptyMessage(MSG_CHECK_SIGNAL_STRENGTH);
+ }
+ }
+ }
+
+ private void resetPlayback() {
+ long timestamp;
+ long oldTimestamp;
+ timestamp = SystemClock.elapsedRealtime();
+ stopPlayback(false);
+ stopCaptionTrack();
+ if (ENABLE_PROFILER) {
+ oldTimestamp = timestamp;
+ timestamp = SystemClock.elapsedRealtime();
+ Log.i(TAG, "[Profiler] stopPlayback() takes " + (timestamp - oldTimestamp) + " ms");
+ }
+ if (mChannelBlocked || mSurface == null || (mChannel == null && mRecordingId == null)) {
+ return;
+ }
+ SoftPreconditions.checkState(mPlayer == null);
+ preparePlayback();
+ }
+
+ private void prepareTune(TunerChannel channel, String recording) {
+ mChannelBlocked = false;
+ mUnblockedContentRating = null;
+ mRetryCount = 0;
+ mChannel = channel;
+ mRecordingId = recording;
+ mRecordingDuration = recording != null ? getDurationForRecording(recording) : null;
+ mProgram = null;
+ mPrograms = null;
+ if (mRecordingId != null) {
+ // Workaround of b/33298048: set it to 1 instead of 0.
+ mBufferStartTimeMs = mRecordStartTimeMs = 1;
+ } else {
+ mBufferStartTimeMs = mRecordStartTimeMs = System.currentTimeMillis();
+ }
+ mLastPositionMs = 0;
+ mCaptionTrack = null;
+ mSignalStrength = TvInputConstantCompat.SIGNAL_STRENGTH_UNKNOWN;
+ if (CommonFeatures.TUNER_SIGNAL_STRENGTH.isEnabled(mContext)) {
+ mSession.notifySignalStrength(mSignalStrength);
+ }
+ mHandler.sendEmptyMessage(MSG_PARENTAL_CONTROLS);
+ }
+
+ private void doReschedulePrograms() {
+ long currentPositionMs = getCurrentPosition();
+ long forwardDifference =
+ Math.abs(currentPositionMs - mLastPositionMs - RESCHEDULE_PROGRAMS_INTERVAL_MS);
+ mLastPositionMs = currentPositionMs;
+
+ // A gap is measured as the time difference between previous and next current position
+ // periodically. If the gap has a significant difference with an interval of a period,
+ // this means that there is a change of playback status and the programs of the current
+ // channel should be rescheduled to new playback timeline.
+ if (forwardDifference > RESCHEDULE_PROGRAMS_TOLERANCE_MS) {
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "reschedule programs size:"
+ + (mPrograms != null ? mPrograms.size() : 0)
+ + " current program: "
+ + getCurrentProgram());
+ }
+ mHandler.obtainMessage(MSG_SCHEDULE_OF_PROGRAMS, new Pair<>(mChannel, mPrograms))
+ .sendToTarget();
+ }
+ mHandler.removeMessages(MSG_RESCHEDULE_PROGRAMS);
+ mHandler.sendEmptyMessageDelayed(MSG_RESCHEDULE_PROGRAMS, RESCHEDULE_PROGRAMS_INTERVAL_MS);
+ }
+
+ private int getTrickPlaySeekIntervalMs() {
+ return Math.max(
+ EXPECTED_KEY_FRAME_INTERVAL_MS / (int) Math.abs(mPlaybackParams.getSpeed()),
+ MIN_TRICKPLAY_SEEK_INTERVAL_MS);
+ }
+
+ @SuppressWarnings("NarrowingCompoundAssignment")
+ private void doTrickplayBySeek(int seekPositionMs) {
+ mHandler.removeMessages(MSG_TRICKPLAY_BY_SEEK);
+ if (mPlaybackParams.getSpeed() == 1.0f || !mPlayer.isPrepared()) {
+ return;
+ }
+ if (seekPositionMs < mBufferStartTimeMs - mRecordStartTimeMs) {
+ if (mPlaybackParams.getSpeed() > 1.0f) {
+ // If fast forwarding, the seekPositionMs can be out of the buffered range
+ // because of chuck evictions.
+ seekPositionMs = (int) (mBufferStartTimeMs - mRecordStartTimeMs);
+ } else {
+ mPlayer.seekTo(mBufferStartTimeMs - mRecordStartTimeMs);
+ mPlaybackParams.setSpeed(1.0f);
+ mPlayer.setAudioTrackAndClosedCaption(true);
+ return;
+ }
+ } else if (seekPositionMs > System.currentTimeMillis() - mRecordStartTimeMs) {
+ // Stops trickplay when FF requested the position later than current position.
+ // If RW trickplay requested the position later than current position,
+ // continue trickplay.
+ if (mPlaybackParams.getSpeed() > 0.0f) {
+ mPlayer.seekTo(System.currentTimeMillis() - mRecordStartTimeMs);
+ mPlaybackParams.setSpeed(1.0f);
+ mPlayer.setAudioTrackAndClosedCaption(true);
+ return;
+ }
+ }
+
+ long delayForNextSeek = getTrickPlaySeekIntervalMs();
+ if (!mPlayer.isBuffering()) {
+ mPlayer.seekTo(seekPositionMs);
+ } else {
+ delayForNextSeek = MIN_TRICKPLAY_SEEK_INTERVAL_MS;
+ }
+ seekPositionMs += mPlaybackParams.getSpeed() * delayForNextSeek;
+ mHandler.sendMessageDelayed(
+ mHandler.obtainMessage(MSG_TRICKPLAY_BY_SEEK, seekPositionMs, 0), delayForNextSeek);
+ }
+
+ private void doTimeShiftPause() {
+ mHandler.removeMessages(MSG_SMOOTH_TRICKPLAY_MONITOR);
+ mHandler.removeMessages(MSG_TRICKPLAY_BY_SEEK);
+ if (!hasEnoughBackwardBuffer()) {
+ return;
+ }
+ mPlaybackParams.setSpeed(1.0f);
+ mPlayer.setPlayWhenReady(false);
+ mPlayer.setAudioTrackAndClosedCaption(true);
+ }
+
+ private void doTimeShiftResume() {
+ mHandler.removeMessages(MSG_SMOOTH_TRICKPLAY_MONITOR);
+ mHandler.removeMessages(MSG_TRICKPLAY_BY_SEEK);
+ mPlaybackParams.setSpeed(1.0f);
+ mPlayer.setPlayWhenReady(true);
+ mPlayer.setAudioTrackAndClosedCaption(true);
+ }
+
+ private void doTimeShiftSeekTo(long timeMs) {
+ mHandler.removeMessages(MSG_SMOOTH_TRICKPLAY_MONITOR);
+ mHandler.removeMessages(MSG_TRICKPLAY_BY_SEEK);
+ mPlayer.seekTo((int) (timeMs - mRecordStartTimeMs));
+ }
+
+ private void doTimeShiftSetPlaybackParams(PlaybackParams params) {
+ if (!hasEnoughBackwardBuffer() && params.getSpeed() < 1.0f) {
+ return;
+ }
+ mPlaybackParams = params;
+ float speed = mPlaybackParams.getSpeed();
+ if (speed == 1.0f) {
+ mHandler.removeMessages(MSG_SMOOTH_TRICKPLAY_MONITOR);
+ mHandler.removeMessages(MSG_TRICKPLAY_BY_SEEK);
+ doTimeShiftResume();
+ } else if (mPlayer.supportSmoothTrickPlay(speed)) {
+ mHandler.removeMessages(MSG_TRICKPLAY_BY_SEEK);
+ mPlayer.setAudioTrackAndClosedCaption(false);
+ mPlayer.startSmoothTrickplay(mPlaybackParams);
+ mHandler.sendEmptyMessageDelayed(
+ MSG_SMOOTH_TRICKPLAY_MONITOR, TRICKPLAY_MONITOR_INTERVAL_MS);
+ } else {
+ mHandler.removeMessages(MSG_SMOOTH_TRICKPLAY_MONITOR);
+ if (!mHandler.hasMessages(MSG_TRICKPLAY_BY_SEEK)) {
+ mPlayer.setAudioTrackAndClosedCaption(false);
+ mPlayer.setPlayWhenReady(false);
+ // Initiate trickplay
+ mHandler.sendMessage(
+ mHandler.obtainMessage(
+ MSG_TRICKPLAY_BY_SEEK,
+ (int)
+ (mPlayer.getCurrentPosition()
+ + speed * getTrickPlaySeekIntervalMs()),
+ 0));
+ }
+ }
+ }
+
+ private EitItem getCurrentProgram() {
+ if (mPrograms == null || mPrograms.isEmpty()) {
+ return null;
+ }
+ if (mChannel.getType() == Channel.TunerType.TYPE_FILE) {
+ // For the playback from the local file, we use the first one from the given program.
+ EitItem first = mPrograms.get(0);
+ if (first != null
+ && (mProgram == null
+ || first.getStartTimeUtcMillis() < mProgram.getStartTimeUtcMillis())) {
+ return first;
+ }
+ return null;
+ }
+ long currentTimeMs = getCurrentPosition();
+ for (EitItem item : mPrograms) {
+ if (item.getStartTimeUtcMillis() <= currentTimeMs
+ && item.getEndTimeUtcMillis() >= currentTimeMs) {
+ return item;
+ }
+ }
+ return null;
+ }
+
+ private void doParentalControls() {
+ boolean isParentalControlsEnabled = mTvInputManager.isParentalControlsEnabled();
+ if (isParentalControlsEnabled) {
+ TvContentRating blockContentRating = getContentRatingOfCurrentProgramBlocked();
+ if (DEBUG) {
+ if (blockContentRating != null) {
+ Log.d(
+ TAG,
+ "Check parental controls: blocked by content rating - "
+ + blockContentRating);
+ } else {
+ Log.d(TAG, "Check parental controls: available");
+ }
+ }
+ updateChannelBlockStatus(blockContentRating != null, blockContentRating);
+ } else {
+ if (DEBUG) {
+ Log.d(TAG, "Check parental controls: available");
+ }
+ updateChannelBlockStatus(false, null);
+ }
+ }
+
+ private void doDiscoverCaptionServiceNumber(int serviceNumber) {
+ int index = mCaptionTrackMap.indexOfKey(serviceNumber);
+ if (index < 0) {
+ AtscCaptionTrack captionTrack = new AtscCaptionTrack();
+ captionTrack.serviceNumber = serviceNumber;
+ captionTrack.wideAspectRatio = false;
+ captionTrack.easyReader = false;
+ mCaptionTrackMap.put(serviceNumber, captionTrack);
+ mTvTracks.add(
+ new TvTrackInfo.Builder(
+ TvTrackInfo.TYPE_SUBTITLE,
+ SUBTITLE_TRACK_PREFIX + serviceNumber)
+ .build());
+ mSession.notifyTracksChanged(mTvTracks);
+ }
+ }
+
+ private TvContentRating getContentRatingOfCurrentProgramBlocked() {
+ EitItem currentProgram = getCurrentProgram();
+ if (currentProgram == null) {
+ return null;
+ }
+ ImmutableList<TvContentRating> ratings =
+ mTvContentRatingCache.getRatings(currentProgram.getContentRating());
+ if ((ratings == null || ratings.isEmpty())) {
+ if (Experiments.ENABLE_UNRATED_CONTENT_SETTINGS.get()) {
+ ratings = ImmutableList.of(TvContentRating.UNRATED);
+ } else {
+ ratings = NO_CONTENT_RATINGS;
+ }
+ }
+ for (TvContentRating rating : ratings) {
+ if (!Objects.equals(mUnblockedContentRating, rating)
+ && mTvInputManager.isRatingBlocked(rating)) {
+ return rating;
+ }
+ }
+ return null;
+ }
+
+ private void updateChannelBlockStatus(boolean channelBlocked, TvContentRating contentRating) {
+ if (mChannelBlocked == channelBlocked) {
+ return;
+ }
+ mChannelBlocked = channelBlocked;
+ if (mChannelBlocked) {
+ clearCallbacksAndMessagesSafely();
+ stopPlayback(true);
+ resetTvTracks();
+ if (contentRating != null) {
+ mSession.notifyContentBlocked(contentRating);
+ }
+ mHandler.sendEmptyMessageDelayed(MSG_PARENTAL_CONTROLS, PARENTAL_CONTROLS_INTERVAL_MS);
+ } else {
+ clearCallbacksAndMessagesSafely();
+ resetPlayback();
+ mSession.notifyContentAllowed();
+ mHandler.sendEmptyMessageDelayed(
+ MSG_RESCHEDULE_PROGRAMS, RESCHEDULE_PROGRAMS_INITIAL_DELAY_MS);
+ mHandler.removeMessages(MSG_CHECK_SIGNAL);
+ mHandler.sendEmptyMessageDelayed(MSG_CHECK_SIGNAL, CHECK_NO_SIGNAL_INITIAL_DELAY_MS);
+ }
+ }
+
+ @WorkerThread
+ private void clearCallbacksAndMessagesSafely() {
+ synchronized (mReleaseLock) {
+ if (!mReleaseRequested) {
+ // This check prevents removing MSG_RELEASE from the queue, which would prevent this
+ // session worker from being released.
+ mHandler.removeCallbacksAndMessages(null);
+ }
+ }
+ }
+
+ private boolean hasEnoughBackwardBuffer() {
+ return mPlayer.getCurrentPosition() + BUFFER_UNDERFLOW_BUFFER_MS
+ >= mBufferStartTimeMs - mRecordStartTimeMs;
+ }
+
+ private void notifyVideoUnavailable(final int reason) {
+ mReportedWeakSignal = (reason == TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL);
+ if (mSession != null) {
+ mSession.notifyVideoUnavailable(reason);
+ }
+ }
+
+ private void notifyVideoAvailable() {
+ mReportedWeakSignal = false;
+ if (mSession != null) {
+ mSession.notifyVideoAvailable();
+ }
+ }
+}
diff --git a/tuner/src/com/android/tv/tuner/tvinput/TunerStorageCleanUpService.java b/tuner/src/com/android/tv/tuner/tvinput/TunerStorageCleanUpService.java
index cdcc00d5..321c7ba9 100644
--- a/tuner/src/com/android/tv/tuner/tvinput/TunerStorageCleanUpService.java
+++ b/tuner/src/com/android/tv/tuner/tvinput/TunerStorageCleanUpService.java
@@ -156,7 +156,9 @@ public class TunerStorageCleanUpService extends JobService {
if (lastModified != 0 && lastModified < now - ELAPSED_MILLIS_TO_DELETE) {
// To prevent current recordings from being deleted,
// deletes recordings which was not modified for long enough time.
- CommonUtils.deleteDirOrFile(recordingDir);
+ if (!CommonUtils.deleteDirOrFile(recordingDir)) {
+ Log.w(TAG, "Unable to delete recording data at " + recordingDir);
+ }
}
}
} catch (IOException | SecurityException e) {
diff --git a/tuner/src/com/android/tv/tuner/tvinput/ChannelDataManager.java b/tuner/src/com/android/tv/tuner/tvinput/datamanager/ChannelDataManager.java
index c1d8f278..585b28bc 100644
--- a/tuner/src/com/android/tv/tuner/tvinput/ChannelDataManager.java
+++ b/tuner/src/com/android/tv/tuner/tvinput/datamanager/ChannelDataManager.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.tv.tuner.tvinput;
+package com.android.tv.tuner.tvinput.datamanager;
import android.content.ContentProviderOperation;
import android.content.ContentUris;
@@ -32,15 +32,15 @@ import android.os.RemoteException;
import android.support.annotation.Nullable;
import android.text.format.DateUtils;
import android.util.Log;
-import com.android.tv.common.BaseApplication;
+import com.android.tv.common.singletons.HasSingletons;
+import com.android.tv.common.singletons.HasTvInputId;
import com.android.tv.common.util.PermissionUtils;
-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.prefs.TunerPreferences;
import com.android.tv.tuner.util.ConvertUtils;
import java.util.ArrayList;
import java.util.Collections;
-import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -100,7 +100,7 @@ public class ChannelDataManager implements Handler.Callback {
private final Context mContext;
private final String mInputId;
private ProgramInfoListener mListener;
- private ChannelScanListener mChannelScanListener;
+ private ChannelHandlingDoneListener mChannelHandlingDoneListener;
private Handler mChannelScanHandler;
private final HandlerThread mHandlerThread;
private final Handler mHandler;
@@ -140,14 +140,15 @@ public class ChannelDataManager implements Handler.Callback {
void onRescanNeeded();
}
- public interface ChannelScanListener {
+ /** Listens for all channel handling to be done. */
+ public interface ChannelHandlingDoneListener {
/** Invoked when all pending channels have been handled. */
void onChannelHandlingDone();
}
public ChannelDataManager(Context context) {
mContext = context;
- mInputId = BaseApplication.getSingletons(context).getEmbeddedTunerInputId();
+ mInputId = HasSingletons.get(HasTvInputId.class, context).getEmbeddedTunerInputId();
mChannelsUri = TvContract.buildChannelsUriForInput(mInputId);
mTunerChannelMap = new ConcurrentHashMap<>();
mTunerChannelIdMap = new ConcurrentSkipListMap<>();
@@ -185,8 +186,8 @@ public class ChannelDataManager implements Handler.Callback {
mListener = listener;
}
- public void setChannelScanListener(ChannelScanListener listener, Handler handler) {
- mChannelScanListener = listener;
+ public void setChannelScanListener(ChannelHandlingDoneListener listener, Handler handler) {
+ mChannelHandlingDoneListener = listener;
mChannelScanHandler = handler;
}
@@ -198,7 +199,7 @@ public class ChannelDataManager implements Handler.Callback {
public void releaseSafely() {
mHandlerThread.quitSafely();
mListener = null;
- mChannelScanListener = null;
+ mChannelHandlingDoneListener = null;
mChannelScanHandler = null;
}
@@ -305,16 +306,10 @@ public class ChannelDataManager implements Handler.Callback {
Log.e(TAG, "Error deleting obsolete channels", e);
}
}
- if (mChannelScanListener != null && mChannelScanHandler != null) {
- mChannelScanHandler.post(
- new Runnable() {
- @Override
- public void run() {
- mChannelScanListener.onChannelHandlingDone();
- }
- });
+ if (mChannelHandlingDoneListener != null && mChannelScanHandler != null) {
+ mChannelScanHandler.post(() -> mChannelHandlingDoneListener.onChannelHandlingDone());
} else {
- Log.e(TAG, "Error. mChannelScanListener is null.");
+ Log.e(TAG, "Error. mChannelHandlingDoneListener is null.");
}
}
@@ -441,14 +436,10 @@ public class ChannelDataManager implements Handler.Callback {
Collections.binarySearch(
oldItems,
newItem,
- new Comparator<EitItem>() {
- @Override
- public int compare(EitItem lhs, EitItem rhs) {
- return Long.compare(
+ (EitItem lhs, EitItem rhs) ->
+ Long.compare(
lhs.getStartTimeUtcMillis(),
- rhs.getStartTimeUtcMillis());
- }
- });
+ rhs.getStartTimeUtcMillis()));
if (pos >= 0) {
// Same start Time found. Overlapped.
continue;
diff --git a/tuner/src/com/android/tv/tuner/tvinput/TunerDebug.java b/tuner/src/com/android/tv/tuner/tvinput/debug/TunerDebug.java
index 1df0b5c3..a92bc59f 100644
--- a/tuner/src/com/android/tv/tuner/tvinput/TunerDebug.java
+++ b/tuner/src/com/android/tv/tuner/tvinput/debug/TunerDebug.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.tv.tuner.tvinput;
+package com.android.tv.tuner.tvinput.debug;
import android.os.SystemClock;
import android.util.Log;
diff --git a/tuner/src/com/android/tv/tuner/tvinput/factory/TunerSessionFactory.java b/tuner/src/com/android/tv/tuner/tvinput/factory/TunerSessionFactory.java
new file mode 100644
index 00000000..a27cb22a
--- /dev/null
+++ b/tuner/src/com/android/tv/tuner/tvinput/factory/TunerSessionFactory.java
@@ -0,0 +1,25 @@
+package com.android.tv.tuner.tvinput.factory;
+
+import android.content.Context;
+import android.media.tv.TvInputService.Session;
+import com.android.tv.tuner.tvinput.datamanager.ChannelDataManager;
+
+/** {@link android.media.tv.TvInputService.Session} factory */
+public interface TunerSessionFactory {
+
+ /** Called when a session is released */
+ interface SessionReleasedCallback {
+
+ /**
+ * Called when the given session is released.
+ *
+ * @param session The session that has been released.
+ */
+ void onReleased(Session session);
+ }
+
+ Session create(
+ Context context,
+ ChannelDataManager channelDataManager,
+ SessionReleasedCallback releasedCallback);
+}
diff --git a/tuner/src/com/android/tv/tuner/tvinput/factory/TunerSessionFactoryImpl.java b/tuner/src/com/android/tv/tuner/tvinput/factory/TunerSessionFactoryImpl.java
new file mode 100644
index 00000000..54e959e6
--- /dev/null
+++ b/tuner/src/com/android/tv/tuner/tvinput/factory/TunerSessionFactoryImpl.java
@@ -0,0 +1,49 @@
+package com.android.tv.tuner.tvinput.factory;
+
+import android.content.Context;
+import android.media.tv.TvInputService.Session;
+import com.android.tv.tuner.source.TsDataSourceManager;
+import com.android.tv.tuner.tvinput.TunerSession;
+import com.android.tv.tuner.tvinput.TunerSessionExoV2;
+import com.android.tv.tuner.tvinput.datamanager.ChannelDataManager;
+import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags;
+import com.android.tv.common.flags.TunerFlags;
+import javax.inject.Inject;
+
+/** Creates a {@link TunerSessionFactory}. */
+public class TunerSessionFactoryImpl implements TunerSessionFactory {
+
+ private final TunerFlags mTunerFlags;
+ private final ConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags;
+ private final TsDataSourceManager.Factory mTsDataSourceManagerFactory;
+
+ @Inject
+ public TunerSessionFactoryImpl(
+ TunerFlags tunerFlags,
+ ConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlags,
+ TsDataSourceManager.Factory tsDataSourceManagerFactory) {
+ mTunerFlags = tunerFlags;
+ mConcurrentDvrPlaybackFlags = concurrentDvrPlaybackFlags;
+ mTsDataSourceManagerFactory = tsDataSourceManagerFactory;
+ }
+
+ @Override
+ public Session create(
+ Context context,
+ ChannelDataManager channelDataManager,
+ SessionReleasedCallback releasedCallback) {
+ return mTunerFlags.useExoplayerV2()
+ ? new TunerSessionExoV2(
+ context,
+ channelDataManager,
+ releasedCallback,
+ mConcurrentDvrPlaybackFlags,
+ mTsDataSourceManagerFactory)
+ : new TunerSession(
+ context,
+ channelDataManager,
+ releasedCallback,
+ mConcurrentDvrPlaybackFlags,
+ mTsDataSourceManagerFactory);
+ }
+}
diff --git a/tuner/src/com/android/tv/tuner/util/TunerInputInfoUtils.java b/tuner/src/com/android/tv/tuner/util/TunerInputInfoUtils.java
deleted file mode 100644
index fad71335..00000000
--- a/tuner/src/com/android/tv/tuner/util/TunerInputInfoUtils.java
+++ /dev/null
@@ -1,112 +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.util;
-
-import android.annotation.TargetApi;
-import android.content.ComponentName;
-import android.content.Context;
-import android.media.tv.TvInputInfo;
-import android.media.tv.TvInputManager;
-import android.os.AsyncTask;
-import android.os.Build;
-import android.support.annotation.Nullable;
-import android.util.Log;
-import android.util.Pair;
-import com.android.tv.common.BaseApplication;
-import com.android.tv.common.BuildConfig;
-import com.android.tv.common.feature.CommonFeatures;
-import com.android.tv.tuner.R;
-import com.android.tv.tuner.TunerHal;
-
-/** Utility class for providing tuner input info. */
-public class TunerInputInfoUtils {
- private static final String TAG = "TunerInputInfoUtils";
- private static final boolean DEBUG = false;
-
- /** Builds tuner input's info. */
- @Nullable
- @TargetApi(Build.VERSION_CODES.N)
- public static TvInputInfo buildTunerInputInfo(Context context) {
- Pair<Integer, Integer> tunerTypeAndCount = TunerHal.getTunerTypeAndCount(context);
- if (tunerTypeAndCount.first == null || tunerTypeAndCount.second == 0) {
- return null;
- }
- int inputLabelId = 0;
- switch (tunerTypeAndCount.first) {
- case TunerHal.TUNER_TYPE_BUILT_IN:
- inputLabelId = R.string.bt_app_name;
- break;
- case TunerHal.TUNER_TYPE_USB:
- inputLabelId = R.string.ut_app_name;
- break;
- case TunerHal.TUNER_TYPE_NETWORK:
- inputLabelId = R.string.nt_app_name;
- break;
- }
- try {
- String inputId = BaseApplication.getSingletons(context).getEmbeddedTunerInputId();
- TvInputInfo.Builder builder =
- new TvInputInfo.Builder(context, ComponentName.unflattenFromString(inputId));
- return builder.setLabel(inputLabelId)
- .setCanRecord(CommonFeatures.DVR.isEnabled(context))
- .setTunerCount(tunerTypeAndCount.second)
- .build();
- } catch (IllegalArgumentException | NullPointerException e) {
- // BaseTunerTvInputService is not enabled.
- return null;
- }
- }
-
- /**
- * Updates tuner input's info.
- *
- * @param context {@link Context} instance
- */
- public static void updateTunerInputInfo(Context context) {
- final Context appContext = context.getApplicationContext();
- if (!BuildConfig.NO_JNI_TEST && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
- new AsyncTask<Void, Void, TvInputInfo>() {
- @Override
- protected TvInputInfo doInBackground(Void... params) {
- if (DEBUG) Log.d(TAG, "updateTunerInputInfo()");
- return buildTunerInputInfo(appContext);
- }
-
- @Override
- @TargetApi(Build.VERSION_CODES.N)
- protected void onPostExecute(TvInputInfo info) {
- if (info != null) {
- ((TvInputManager) appContext.getSystemService(Context.TV_INPUT_SERVICE))
- .updateTvInputInfo(info);
- if (DEBUG) {
- Log.d(
- TAG,
- "TvInputInfo ["
- + info.loadLabel(appContext)
- + "] updated: "
- + info.toString());
- }
- } else {
- if (DEBUG) {
- Log.d(TAG, "Updating tuner input info failed. Input is not ready yet.");
- }
- }
- }
- }.execute();
- }
- }
-}
diff --git a/tuner/tests/testing/Android.mk b/tuner/tests/testing/Android.mk
index c0d5dda9..79e35e5a 100644
--- a/tuner/tests/testing/Android.mk
+++ b/tuner/tests/testing/Android.mk
@@ -8,11 +8,10 @@ LOCAL_SRC_FILES := \
LOCAL_STATIC_JAVA_LIBRARIES := \
android-support-annotations \
- android-support-test \
- guava \
+ androidx.test.runner \
+ tv-guava-android-jar \
mockito-target \
- platform-robolectric-3.6.2-prebuilt \
- truth-0-36-prebuilt-jar \
+ tv-lib-truth \
ub-uiautomator \
# Link tv-common as shared library to avoid the problem of initialization of the constants
diff --git a/tuner/tests/testing/AndroidManifest.xml b/tuner/tests/testing/AndroidManifest.xml
index f244ae7b..7e07a52a 100644
--- a/tuner/tests/testing/AndroidManifest.xml
+++ b/tuner/tests/testing/AndroidManifest.xml
@@ -18,6 +18,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.tv.tuner.testing"
android:versionCode="1">
- <uses-sdk android:targetSdkVersion="26" android:minSdkVersion="21"/>
+ <uses-sdk android:targetSdkVersion="27" android:minSdkVersion="23"/>
<application />
</manifest>
diff --git a/tuner/tests/unittests/javatests/AndroidManifest.xml b/tuner/tests/unittests/javatests/AndroidManifest.xml
index 8a5fda8f..62caefa1 100644
--- a/tuner/tests/unittests/javatests/AndroidManifest.xml
+++ b/tuner/tests/unittests/javatests/AndroidManifest.xml
@@ -18,7 +18,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.tv.tuner.layout.tests" >
- <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="26"/>
+ <uses-sdk android:minSdkVersion="23" android:targetSdkVersion="27"/>
<instrumentation
android:name="android.test.InstrumentationTestRunner"
diff --git a/tuner/tests/unittests/javatests/com/android/tv/tuner/AndroidManifest.xml b/tuner/tests/unittests/javatests/com/android/tv/tuner/AndroidManifest.xml
index 9c815600..6fe0b85a 100644
--- a/tuner/tests/unittests/javatests/com/android/tv/tuner/AndroidManifest.xml
+++ b/tuner/tests/unittests/javatests/com/android/tv/tuner/AndroidManifest.xml
@@ -18,10 +18,10 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.tv.tuner.tests" >
- <uses-sdk android:minSdkVersion="23" android:targetSdkVersion="26" />
+ <uses-sdk android:minSdkVersion="23" android:targetSdkVersion="27" />
<instrumentation
- android:name="android.support.test.runner.AndroidJUnitRunner"
+ android:name="androidx.test.runner.AndroidJUnitRunner"
android:targetPackage="com.android.tv" />
<application android:label="TunerTvInputTests" >
diff --git a/tuner/tests/unittests/javatests/com/android/tv/tuner/FakeTunerHal.java b/tuner/tests/unittests/javatests/com/android/tv/tuner/FakeTunerHal.java
index cc4f6fde..6d113b0f 100644
--- a/tuner/tests/unittests/javatests/com/android/tv/tuner/FakeTunerHal.java
+++ b/tuner/tests/unittests/javatests/com/android/tv/tuner/FakeTunerHal.java
@@ -25,19 +25,19 @@ public class FakeTunerHal extends TunerHal {
}
@Override
- protected boolean openFirstAvailable() {
+ public boolean openFirstAvailable() {
mDeviceOpened = true;
getDeliverySystemTypeFromDevice();
return true;
}
@Override
- protected boolean isDeviceOpen() {
+ public boolean isDeviceOpen() {
return mDeviceOpened;
}
@Override
- protected long getDeviceId() {
+ public long getDeviceId() {
return 0;
}
diff --git a/tuner/tests/unittests/javatests/com/android/tv/tuner/FileTunerHal.java b/tuner/tests/unittests/javatests/com/android/tv/tuner/FileTunerHal.java
index 73d234e0..cb464839 100644
--- a/tuner/tests/unittests/javatests/com/android/tv/tuner/FileTunerHal.java
+++ b/tuner/tests/unittests/javatests/com/android/tv/tuner/FileTunerHal.java
@@ -76,7 +76,7 @@ public class FileTunerHal extends TunerHal {
}
@Override
- protected boolean openFirstAvailable() {
+ public boolean openFirstAvailable() {
sIsDeviceOpen = true;
getDeliverySystemTypeFromDevice();
return true;
@@ -86,12 +86,12 @@ public class FileTunerHal extends TunerHal {
public void close() {}
@Override
- protected boolean isDeviceOpen() {
+ public boolean isDeviceOpen() {
return sIsDeviceOpen;
}
@Override
- protected long getDeviceId() {
+ public long getDeviceId() {
return DEVICE_ID;
}
diff --git a/tuner/tests/unittests/javatests/com/android/tv/tuner/ZappingTimeTest.java b/tuner/tests/unittests/javatests/com/android/tv/tuner/ZappingTimeTest.java
index 0e9bd357..ef653f86 100644
--- a/tuner/tests/unittests/javatests/com/android/tv/tuner/ZappingTimeTest.java
+++ b/tuner/tests/unittests/javatests/com/android/tv/tuner/ZappingTimeTest.java
@@ -21,10 +21,11 @@ import android.graphics.SurfaceTexture;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Message;
-import android.support.test.filters.LargeTest;
import android.test.InstrumentationTestCase;
import android.util.Log;
import android.view.Surface;
+import androidx.test.filters.LargeTest;
+import com.android.tv.common.flags.impl.DefaultConcurrentDvrPlaybackFlags;
import com.android.tv.tuner.data.Cea708Data;
import com.android.tv.tuner.data.PsiData;
import com.android.tv.tuner.data.PsipData;
@@ -33,10 +34,10 @@ import com.android.tv.tuner.data.nano.Channel;
import com.android.tv.tuner.exoplayer.MpegTsPlayer;
import com.android.tv.tuner.exoplayer.MpegTsRendererBuilder;
import com.android.tv.tuner.exoplayer.buffer.BufferManager;
+import com.android.tv.tuner.exoplayer.buffer.PlaybackBufferListener;
import com.android.tv.tuner.exoplayer.buffer.TrickplayStorageManager;
import com.android.tv.tuner.source.TsDataSourceManager;
-import com.android.tv.tuner.tvinput.EventDetector;
-import com.android.tv.tuner.tvinput.PlaybackBufferListener;
+import com.android.tv.tuner.ts.EventDetector.EventListener;
import com.google.android.exoplayer.ExoPlayer;
import java.io.File;
import java.io.FileOutputStream;
@@ -86,7 +87,9 @@ public class ZappingTimeTest extends InstrumentationTestCase {
private AtomicLong mOnDrawnToSurfaceTimeMs = new AtomicLong(0);
private MockMpegTsPlayerListener mMpegTsPlayerListener = new MockMpegTsPlayerListener();
private MockPlaybackBufferListener mPlaybackBufferListener = new MockPlaybackBufferListener();
- private MockEventListener mEventListener = new MockEventListener();
+ private MockChannelScanListener mEventListener = new MockChannelScanListener();
+ private DefaultConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags =
+ new DefaultConcurrentDvrPlaybackFlags();
@Override
protected void setUp() throws Exception {
@@ -152,7 +155,8 @@ public class ZappingTimeTest extends InstrumentationTestCase {
new MpegTsRendererBuilder(
mTargetContext,
bufferManager,
- mPlaybackBufferListener),
+ mPlaybackBufferListener,
+ mConcurrentDvrPlaybackFlags),
mHandler,
mSourceManager,
null,
@@ -388,7 +392,7 @@ public class ZappingTimeTest extends InstrumentationTestCase {
}
}
- private static class MockEventListener implements EventDetector.EventListener {
+ private static class MockChannelScanListener implements EventListener {
@Override
public void onChannelDetected(TunerChannel channel, boolean channelArrivedAtFirstTime) {
if (DEBUG) {
diff --git a/tuner/tests/unittests/javatests/com/android/tv/tuner/layout/tests/AndroidManifest.xml b/tuner/tests/unittests/javatests/com/android/tv/tuner/layout/tests/AndroidManifest.xml
index 3e6946a9..77c7f40a 100644
--- a/tuner/tests/unittests/javatests/com/android/tv/tuner/layout/tests/AndroidManifest.xml
+++ b/tuner/tests/unittests/javatests/com/android/tv/tuner/layout/tests/AndroidManifest.xml
@@ -17,7 +17,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.tv.tuner"
android:versionCode="1">
- <uses-sdk android:targetSdkVersion="26" android:minSdkVersion="23"/>
+ <uses-sdk android:targetSdkVersion="27" android:minSdkVersion="23"/>
<application android:label="TunerTvInputLayoutTests" >
<activity android:name="com.android.tv.tuner.layout.tests.ScaledLayoutActivity"
android:label="ScaledLayout Test" />
diff --git a/tuner/tests/unittests/javatests/com/android/tv/tuner/layout/tests/ScaledLayoutTest.java b/tuner/tests/unittests/javatests/com/android/tv/tuner/layout/tests/ScaledLayoutTest.java
index 214b0631..c2a23f2f 100644
--- a/tuner/tests/unittests/javatests/com/android/tv/tuner/layout/tests/ScaledLayoutTest.java
+++ b/tuner/tests/unittests/javatests/com/android/tv/tuner/layout/tests/ScaledLayoutTest.java
@@ -20,11 +20,11 @@ import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertNotNull;
import android.content.Intent;
-import android.support.test.filters.SmallTest;
-import android.support.test.rule.ActivityTestRule;
-import android.support.test.runner.AndroidJUnit4;
import android.view.View;
import android.widget.FrameLayout;
+import androidx.test.filters.SmallTest;
+import androidx.test.rule.ActivityTestRule;
+import androidx.test.runner.AndroidJUnit4;
import com.android.tv.tuner.layout.ScaledLayout;
import org.junit.After;
import org.junit.Before;
diff --git a/tuner/tests/unittests/javatests/com/android/tv/tuner/setup/TunerHalFactoryTest.java b/tuner/tests/unittests/javatests/com/android/tv/tuner/setup/TunerHalCreatorTest.java
index 2354c827..a3a32084 100644
--- a/tuner/tests/unittests/javatests/com/android/tv/tuner/setup/TunerHalFactoryTest.java
+++ b/tuner/tests/unittests/javatests/com/android/tv/tuner/setup/TunerHalCreatorTest.java
@@ -21,27 +21,27 @@ import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import android.os.AsyncTask;
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
-import com.android.tv.tuner.TunerHal;
-import com.android.tv.tuner.setup.BaseTunerSetupActivity.TunerHalFactory;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+import com.android.tv.tuner.api.Tuner;
+import com.android.tv.tuner.setup.BaseTunerSetupActivity.TunerHalCreator;
import java.util.concurrent.Executor;
import org.junit.Test;
import org.junit.runner.RunWith;
-/** Tests for {@link TunerHalFactory}. */
+/** Tests for {@link TunerHalCreator}. */
@SmallTest
@RunWith(AndroidJUnit4.class)
-public class TunerHalFactoryTest {
+public class TunerHalCreatorTest {
private final FakeExecutor mFakeExecutor = new FakeExecutor();
- private static class TestTunerHalFactory extends TunerHalFactory {
- private TestTunerHalFactory(Executor executor) {
+ private static class TestTunerHalCreator extends TunerHalCreator {
+ private TestTunerHalCreator(Executor executor) {
super(null, executor);
}
@Override
- protected TunerHal createInstance() {
+ protected Tuner createInstance() {
return new com.android.tv.tuner.FakeTunerHal() {};
}
}
@@ -61,29 +61,29 @@ public class TunerHalFactoryTest {
@Test
public void test_asyncGet() {
- TunerHalFactory tunerHalFactory = new TestTunerHalFactory(mFakeExecutor);
- assertNull(tunerHalFactory.mTunerHal);
- tunerHalFactory.generate();
- assertNull(tunerHalFactory.mTunerHal);
+ TunerHalCreator tunerHalCreator = new TestTunerHalCreator(mFakeExecutor);
+ assertNull(tunerHalCreator.mTunerHal);
+ tunerHalCreator.generate();
+ assertNull(tunerHalCreator.mTunerHal);
mFakeExecutor.executeActually();
- TunerHal tunerHal = tunerHalFactory.getOrCreate();
+ Tuner tunerHal = tunerHalCreator.getOrCreate();
assertNotNull(tunerHal);
- assertSame(tunerHal, tunerHalFactory.getOrCreate());
- tunerHalFactory.clear();
+ assertSame(tunerHal, tunerHalCreator.getOrCreate());
+ tunerHalCreator.clear();
}
@Test
public void test_syncGet() {
- TunerHalFactory tunerHalFactory = new TestTunerHalFactory(AsyncTask.SERIAL_EXECUTOR);
- assertNull(tunerHalFactory.mTunerHal);
- tunerHalFactory.generate();
- assertNotNull(tunerHalFactory.getOrCreate());
+ TunerHalCreator tunerHalCreator = new TestTunerHalCreator(AsyncTask.SERIAL_EXECUTOR);
+ assertNull(tunerHalCreator.mTunerHal);
+ tunerHalCreator.generate();
+ assertNotNull(tunerHalCreator.getOrCreate());
}
@Test
public void test_syncGetWithoutGenerate() {
- TunerHalFactory tunerHalFactory = new TestTunerHalFactory(mFakeExecutor);
- assertNull(tunerHalFactory.mTunerHal);
- assertNotNull(tunerHalFactory.getOrCreate());
+ TunerHalCreator tunerHalCreator = new TestTunerHalCreator(mFakeExecutor);
+ assertNull(tunerHalCreator.mTunerHal);
+ assertNotNull(tunerHalCreator.getOrCreate());
}
}