aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2024-05-10 15:47:15 +0000
committerAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2024-05-10 15:47:15 +0000
commit6e976661997d2829b766910906658e315c8a89e5 (patch)
treea2f548af9bfbefbe887606448ca59d539f94faa6
parent8a80e13bf0712d1a797404f1b0e794d92de282a1 (diff)
parentd27efb23f9ae7de26edec28926e66e51a7ae7068 (diff)
downloadTV-busytown-mac-infra-release.tar.gz
Snap for 11819167 from d27efb23f9ae7de26edec28926e66e51a7ae7068 to busytown-mac-infra-releasebusytown-mac-infra-release
Change-Id: I08654cda6982045afca779a52558bf6aba9609c0
-rw-r--r--Android.bp4
-rw-r--r--AndroidManifest.xml15
-rw-r--r--OWNERS1
-rw-r--r--common/Android.bp28
-rw-r--r--common/src/com/android/tv/common/feature/ResourceConfigFeature.java39
-rw-r--r--common/src/com/android/tv/common/feature/Sdk.java2
-rw-r--r--common/tests/robotests/Android.bp48
-rw-r--r--common/tests/robotests/Android.mk54
-rw-r--r--interactive/SampleTvInteractiveAppService/Android.bp49
-rw-r--r--interactive/SampleTvInteractiveAppService/AndroidManifest.xml51
-rw-r--r--interactive/SampleTvInteractiveAppService/build.gradle31
-rw-r--r--interactive/SampleTvInteractiveAppService/res/layout/sample_layout.xml66
-rw-r--r--interactive/SampleTvInteractiveAppService/res/mipmap-hdpi/ic_launcher.webpbin0 -> 1404 bytes
-rw-r--r--interactive/SampleTvInteractiveAppService/res/mipmap-mdpi/ic_launcher.webpbin0 -> 982 bytes
-rw-r--r--interactive/SampleTvInteractiveAppService/res/mipmap-xhdpi/ic_launcher.webpbin0 -> 1900 bytes
-rw-r--r--interactive/SampleTvInteractiveAppService/res/mipmap-xxhdpi/ic_launcher.webpbin0 -> 2884 bytes
-rw-r--r--interactive/SampleTvInteractiveAppService/res/mipmap-xxxhdpi/ic_launcher.webpbin0 -> 3844 bytes
-rw-r--r--interactive/SampleTvInteractiveAppService/res/values/colors.xml21
-rw-r--r--interactive/SampleTvInteractiveAppService/res/values/strings.xml27
-rw-r--r--interactive/SampleTvInteractiveAppService/res/values/styles.xml28
-rw-r--r--interactive/SampleTvInteractiveAppService/res/xml/tviappservice.xml19
-rw-r--r--interactive/SampleTvInteractiveAppService/src/com/android/tv/samples/sampletvinteractiveappservice/SampleTvInteractiveAppService.java39
-rw-r--r--interactive/SampleTvInteractiveAppService/src/com/android/tv/samples/sampletvinteractiveappservice/TiasSessionImpl.java863
-rw-r--r--lint-baseline.xml315
-rw-r--r--res/drawable/empty_input_background.pngbin0 -> 1304602 bytes
-rw-r--r--res/drawable/ic_empty_input_hdmi.xml10
-rw-r--r--res/drawable/ic_empty_input_tuner.xml10
-rw-r--r--res/drawable/input_banner_v2_background.xml21
-rwxr-xr-xres/drawable/tv_iapp_dialog_background.xml21
-rw-r--r--res/layout/activity_tv.xml6
-rw-r--r--res/layout/block_screen.xml7
-rw-r--r--res/layout/empty_input_status_block.xml61
-rw-r--r--res/layout/input_banner_v2.xml64
-rwxr-xr-xres/layout/tv_app_dialog.xml71
-rw-r--r--res/values/arrays-custom.xml13
-rw-r--r--res/values/colors.xml9
-rw-r--r--res/values/configs.xml19
-rw-r--r--res/values/dimens.xml9
-rw-r--r--res/values/strings-custom.xml3
-rw-r--r--res/values/strings.xml22
-rw-r--r--src/com/android/tv/ChannelTuner.java2
-rw-r--r--src/com/android/tv/InputSessionManager.java7
-rw-r--r--src/com/android/tv/MainActivity.java106
-rw-r--r--src/com/android/tv/SetupPassthroughActivity.java2
-rw-r--r--src/com/android/tv/TvApplication.java14
-rw-r--r--src/com/android/tv/audiotvservice/AudioOnlyTvService.java7
-rw-r--r--src/com/android/tv/data/ChannelDataManager.java16
-rw-r--r--src/com/android/tv/data/ChannelImpl.java14
-rw-r--r--src/com/android/tv/data/StreamInfo.java2
-rwxr-xr-xsrc/com/android/tv/dialog/InteractiveAppDialogFragment.java138
-rw-r--r--src/com/android/tv/features/TvFeatures.java10
-rw-r--r--src/com/android/tv/interactive/IAppManager.java428
-rw-r--r--src/com/android/tv/onboarding/OnboardingActivity.java8
-rw-r--r--src/com/android/tv/onboarding/SetupSourcesFragment.java2
-rw-r--r--src/com/android/tv/receiver/AudioCapabilitiesReceiver.java3
-rw-r--r--src/com/android/tv/setup/SystemSetupActivity.java3
-rw-r--r--src/com/android/tv/ui/BlockScreenView.java15
-rw-r--r--src/com/android/tv/ui/EmptyInputStatusBlockView.java68
-rw-r--r--src/com/android/tv/ui/InputBannerView.java38
-rw-r--r--src/com/android/tv/ui/InputBannerViewBase.java69
-rw-r--r--src/com/android/tv/ui/InputBannerViewV2.java84
-rw-r--r--src/com/android/tv/ui/SelectInputView.java12
-rw-r--r--src/com/android/tv/ui/TunableTvView.java64
-rw-r--r--src/com/android/tv/ui/TvOverlayManager.java13
-rw-r--r--src/com/android/tv/ui/TvTransitionManager.java8
-rwxr-xr-xsrc/com/android/tv/ui/sidepanel/InteractiveAppSettingsFragment.java54
-rw-r--r--src/com/android/tv/ui/sidepanel/SettingsFragment.java17
-rw-r--r--src/com/android/tv/util/SetupUtils.java66
-rw-r--r--src/com/android/tv/util/TvSettings.java15
-rw-r--r--tests/Android.bp45
-rw-r--r--tests/Android.mk1
-rw-r--r--tests/common/Android.bp28
-rw-r--r--tests/common/Android.mk28
-rw-r--r--tests/common/src/com/android/tv/testing/uihelper/BaseUiDeviceHelper.java2
-rw-r--r--tests/common/src/com/android/tv/testing/uihelper/ByResource.java4
-rw-r--r--tests/common/src/com/android/tv/testing/uihelper/Constants.java4
-rw-r--r--tests/common/src/com/android/tv/testing/uihelper/DialogHelper.java6
-rw-r--r--tests/common/src/com/android/tv/testing/uihelper/LiveChannelsUiDeviceHelper.java8
-rw-r--r--tests/common/src/com/android/tv/testing/uihelper/MenuHelper.java12
-rw-r--r--tests/common/src/com/android/tv/testing/uihelper/SidePanelHelper.java10
-rw-r--r--tests/common/src/com/android/tv/testing/uihelper/UiDeviceAsserts.java14
-rw-r--r--tests/common/src/com/android/tv/testing/uihelper/UiDeviceUtils.java4
-rw-r--r--tests/common/src/com/android/tv/testing/uihelper/UiObject2Asserts.java4
-rw-r--r--tests/common/src/com/android/tv/testing/uihelper/UiObject2Utils.java4
-rw-r--r--tests/func/Android.bp2
-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.java4
-rw-r--r--tests/func/src/com/android/tv/tests/ui/LiveChannelsAppTest.java4
-rw-r--r--tests/func/src/com/android/tv/tests/ui/LiveChannelsTestController.java10
-rw-r--r--tests/func/src/com/android/tv/tests/ui/ParentalControlsTest.java6
-rw-r--r--tests/func/src/com/android/tv/tests/ui/PlayControlsRowViewTest.java6
-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.java6
-rw-r--r--tests/func/src/com/android/tv/tests/ui/sidepanel/CustomizeChannelListFragmentTest.java8
-rw-r--r--tests/jank/Android.bp2
-rw-r--r--tests/jank/src/com/android/tv/tests/jank/LiveChannelsTestCase.java2
-rw-r--r--tests/jank/src/com/android/tv/tests/jank/ProgramGuideJankTest.java2
-rw-r--r--tests/jank/src/com/android/tv/tests/jank/Utils.java2
-rw-r--r--tests/robotests/Android.bp53
-rw-r--r--tests/robotests/Android.mk78
-rw-r--r--tests/robotests/src/com/android/tv/ShadowTvView.java4
-rw-r--r--tests/robotests/src/com/android/tv/testing/TvRobolectricTestRunner.java1
-rw-r--r--tests/unit/Android.mk32
-rw-r--r--tuner/Android.bp3
-rw-r--r--tuner/lint-baseline.xml18
-rw-r--r--tuner/sampletunertvinput/src/com/android/tv/samples/sampletunertvinput/SampleTunerTvInputSectionParser.java341
-rw-r--r--tuner/sampletunertvinput/src/com/android/tv/samples/sampletunertvinput/SampleTunerTvInputService.java450
-rw-r--r--tuner/sampletunertvinput/src/com/android/tv/samples/sampletunertvinput/SampleTunerTvInputSetupActivity.java158
-rw-r--r--tuner/sampletunertvinput/src/com/android/tv/samples/sampletunertvinput/SampleTunerTvInputUtils.java174
-rw-r--r--tuner/tests/robotests/Android.bp47
-rw-r--r--tuner/tests/robotests/Android.mk72
-rw-r--r--tuner/tests/robotests/javatests/com/android/tv/tuner/testing/TvTunerRobolectricTestRunner.java1
-rw-r--r--tuner/tests/testing/Android.bp41
-rw-r--r--tuner/tests/testing/Android.mk30
115 files changed, 4093 insertions, 975 deletions
diff --git a/Android.bp b/Android.bp
index 951d6b0e..463c5e2c 100644
--- a/Android.bp
+++ b/Android.bp
@@ -62,6 +62,7 @@ android_app {
libs: ["tv-guava-android-jar"],
static_libs: [
+ "androidx-constraintlayout_constraintlayout",
"android-support-annotations",
"android-support-compat",
"android-support-v7-recyclerview",
@@ -112,6 +113,9 @@ android_app {
"--extra-packages",
"com.android.tv.common",
],
+ lint: {
+ baseline_filename: "lint-baseline.xml",
+ },
}
prebuilt_etc {
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 6a2d435f..5d9492a3 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -20,10 +20,23 @@
xmlns:tools="http://schemas.android.com/tools"
package="com.android.tv">
+ <queries>
+ <intent>
+ <action android:name="android.intent.action.MAIN" />
+ </intent>
+ <intent>
+ <action android:name="android.media.tv.TvInputService" />
+ </intent>
+ <intent>
+ <action android:name="android.media.tv.action.QUERY_CONTENT_RATING_SYSTEMS" />
+ </intent>
+ </queries>
+
<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.FOREGROUND_SERVICE"/>
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/>
<uses-permission android:name="android.permission.HDMI_CEC"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.MODIFY_PARENTAL_CONTROLS"/>
@@ -166,6 +179,7 @@
</activity>
<activity android:name="com.android.tv.SelectInputActivity"
android:exported="true"
+ android:label="@string/select_inputs"
android:configChanges="keyboard|keyboardHidden"
android:launchMode="singleTask"
android:theme="@style/Theme.SelectInputActivity">
@@ -278,6 +292,7 @@
</intent-filter>
</activity> <!-- DVR -->
<service android:name="com.android.tv.dvr.recorder.DvrRecordingService"
+ android:foregroundServiceType="mediaPlayback"
android:exported="false"
android:label="@string/dvr_service_name"/>
diff --git a/OWNERS b/OWNERS
index a386cdaf..cbf326d0 100644
--- a/OWNERS
+++ b/OWNERS
@@ -1,2 +1,3 @@
shubang@google.com
quxiangfang@google.com
+qingxun@google.com
diff --git a/common/Android.bp b/common/Android.bp
index f6ba940c..acc614a3 100644
--- a/common/Android.bp
+++ b/common/Android.bp
@@ -41,18 +41,18 @@ android_library {
],
static_libs: [
- "androidx.legacy_legacy-support-core-ui",
- "androidx.appcompat_appcompat",
- "androidx.preference_preference",
- "androidx.leanback_leanback",
- "androidx.tvprovider_tvprovider",
- "tv-guava-android-jar",
- "tv-guava-failureaccess-jar",
- "jsr330",
- "tv-lib-dagger",
- "tv-lib-exoplayer",
- "tv-lib-exoplayer-v2-core",
- "tv-lib-dagger-android",
+ "androidx.legacy_legacy-support-core-ui",
+ "androidx.appcompat_appcompat",
+ "androidx.preference_preference",
+ "androidx.leanback_leanback",
+ "androidx.tvprovider_tvprovider",
+ "tv-guava-android-jar",
+ "tv-guava-failureaccess-jar",
+ "jsr330",
+ "tv-lib-dagger",
+ "tv-lib-exoplayer",
+ "tv-lib-exoplayer-v2-core",
+ "tv-lib-dagger-android",
],
plugins: [
@@ -62,8 +62,10 @@ android_library {
"tv-lib-dagger-compiler",
],
-
min_sdk_version: "23",
+ lint: {
+ baseline_filename: "lint-baseline.xml",
+ },
// TODO(b/77284273): generate build config after dagger supports libraries
//include $(LOCAL_PATH)/buildconfig.mk
diff --git a/common/src/com/android/tv/common/feature/ResourceConfigFeature.java b/common/src/com/android/tv/common/feature/ResourceConfigFeature.java
new file mode 100644
index 00000000..7519a350
--- /dev/null
+++ b/common/src/com/android/tv/common/feature/ResourceConfigFeature.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.feature;
+
+import android.content.Context;
+import android.content.res.Resources;
+
+public class ResourceConfigFeature implements Feature{
+
+ private final int mResId;
+ private final boolean mDefaultValue;
+
+ public ResourceConfigFeature(int resId, boolean defaultValue) {
+ mResId = resId;
+ mDefaultValue = defaultValue;
+ }
+ @Override
+ public boolean isEnabled(Context context) {
+ try {
+ return context.getResources().getBoolean(mResId);
+ } catch (Resources.NotFoundException e) {
+ return mDefaultValue;
+ }
+ }
+}
diff --git a/common/src/com/android/tv/common/feature/Sdk.java b/common/src/com/android/tv/common/feature/Sdk.java
index e59bcd60..bf76c9ce 100644
--- a/common/src/com/android/tv/common/feature/Sdk.java
+++ b/common/src/com/android/tv/common/feature/Sdk.java
@@ -27,6 +27,8 @@ public final class Sdk {
public static final Feature AT_LEAST_O = new AtLeast(VERSION_CODES.O);
+ public static final Feature AT_LEAST_T = new AtLeast(VERSION_CODES.TIRAMISU);
+
private static final class AtLeast implements Feature {
private final int versionCode;
diff --git a/common/tests/robotests/Android.bp b/common/tests/robotests/Android.bp
new file mode 100644
index 00000000..4ce4e3df
--- /dev/null
+++ b/common/tests/robotests/Android.bp
@@ -0,0 +1,48 @@
+//
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+ // See: http://go/android-license-faq
+ default_applicable_licenses: [
+ "Android-Apache-2.0",
+ ],
+}
+
+android_robolectric_test {
+ name: "TvCommonRoboTests",
+ srcs: [
+ "src/**/*.java",
+ ],
+ java_resource_dirs: ["config"],
+ libs: [
+ "diffutils-prebuilt-jar",
+ ],
+ static_libs: [
+ "tv-lib-truth",
+ "androidx.test.ext.truth",
+ "tv-test-common",
+ "tv-test-common-robo",
+ ],
+ plugins: [
+ "tv-lib-dagger-android-processor",
+ "tv-lib-dagger-compiler",
+ ],
+ instrumentation_for: "LiveTv",
+ test_options: {
+ timeout: 36000,
+ },
+ upstream: true,
+}
diff --git a/common/tests/robotests/Android.mk b/common/tests/robotests/Android.mk
deleted file mode 100644
index 85512fb6..00000000
--- a/common/tests/robotests/Android.mk
+++ /dev/null
@@ -1,54 +0,0 @@
-#############################################################
-# Tv Common Robolectric test target. #
-#############################################################
-LOCAL_PATH := $(call my-dir)
-include $(CLEAR_VARS)
-
-LOCAL_MODULE := TvCommonRoboTests
-LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
-LOCAL_LICENSE_CONDITIONS := notice
-LOCAL_MODULE_CLASS := JAVA_LIBRARIES
-
-LOCAL_SRC_FILES := $(call all-java-files-under, src)
-
-LOCAL_JAVA_LIBRARIES := \
- Robolectric_all-target \
- mockito-robolectric-prebuilt \
- robolectric_android-all-stub \
- diffutils-prebuilt-jar \
-
-LOCAL_STATIC_JAVA_LIBRARIES := \
- tv-lib-truth \
-
-LOCAL_STATIC_ANDROID_LIBRARIES := \
- androidx.test.ext.truth \
- tv-test-common \
- tv-test-common-robo \
-
-LOCAL_INSTRUMENTATION_FOR := LiveTv
-
-LOCAL_MODULE_TAGS := optional
-
-include $(BUILD_STATIC_JAVA_LIBRARY)
-
-#############################################################
-# Tv common runner target to run the previous target. #
-#############################################################
-include $(CLEAR_VARS)
-LOCAL_MODULE := RunTvCommonRoboTests
-LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
-LOCAL_LICENSE_CONDITIONS := notice
-
-LOCAL_ROBOTEST_FILES := $(call find-files-in-subdirs,$(LOCAL_PATH)/src,*Test.java,.)
-
-LOCAL_JAVA_LIBRARIES := \
- TvCommonRoboTests \
- Robolectric_all-target \
- mockito-robolectric-prebuilt \
- robolectric_android-all-stub \
-
-LOCAL_TEST_PACKAGE := LiveTv
-
-LOCAL_ROBOTEST_TIMEOUT := 36000
-
-include external/robolectric-shadows/run_robotests.mk
diff --git a/interactive/SampleTvInteractiveAppService/Android.bp b/interactive/SampleTvInteractiveAppService/Android.bp
new file mode 100644
index 00000000..eada4ded
--- /dev/null
+++ b/interactive/SampleTvInteractiveAppService/Android.bp
@@ -0,0 +1,49 @@
+//
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_app {
+ name: "SampleTvInteractiveAppService",
+
+ srcs: ["src/**/*.java"],
+ optimize: {
+ enabled: false,
+ },
+
+ privileged: true,
+ product_specific: true,
+ sdk_version: "system_current",
+ min_sdk_version: "33", // T
+
+ resource_dirs: [
+ "res",
+ ],
+
+ static_libs: [
+ "androidx.leanback_leanback",
+ ],
+
+ aaptflags: [
+ "--version-name",
+ version_name,
+
+ "--version-code",
+ version_code,
+ ],
+}
diff --git a/interactive/SampleTvInteractiveAppService/AndroidManifest.xml b/interactive/SampleTvInteractiveAppService/AndroidManifest.xml
new file mode 100644
index 00000000..72cd22f9
--- /dev/null
+++ b/interactive/SampleTvInteractiveAppService/AndroidManifest.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2022 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT 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"
+ xmlns:tools="http://schemas.android.com/tools"
+ package="com.android.tv.samples.sampletvinteractiveappservice"
+ tools:ignore="MissingLeanbackLauncher">
+
+ <uses-permission android:name="com.google.android.dtvprovider.permission.READ" />
+ <uses-permission android:name="android.permission.INTERNET" />
+ <uses-permission android:name="android.permission.START_ACTIVITIES_FROM_BACKGROUND"/>
+
+ <uses-feature android:name="android.hardware.touchscreen" android:required="false" />
+ <uses-feature android:name="android.software.leanback" android:required="false" />
+
+ <application
+ android:allowBackup="true"
+ android:icon="@mipmap/ic_launcher"
+ android:label="@string/sample_tias"
+ android:supportsRtl="true"
+ android:theme="@style/Theme.Leanback">
+ <service
+ android:name=".SampleTvInteractiveAppService"
+ android:enabled="true"
+ android:exported="true"
+ android:isolatedProcess="false"
+ android:permission="android.permission.BIND_TV_INTERACTIVE_APP"
+ android:process=":rte">
+ <intent-filter>
+ <action android:name="android.media.tv.interactive.TvInteractiveAppService" />
+ </intent-filter>
+ <meta-data
+ android:name="android.media.tv.interactive.app"
+ android:resource="@xml/tviappservice" />
+ </service>
+ </application>
+
+</manifest>
diff --git a/interactive/SampleTvInteractiveAppService/build.gradle b/interactive/SampleTvInteractiveAppService/build.gradle
new file mode 100644
index 00000000..a51bc56a
--- /dev/null
+++ b/interactive/SampleTvInteractiveAppService/build.gradle
@@ -0,0 +1,31 @@
+plugins {
+ id 'com.android.application'
+}
+
+android {
+ compileSdk 31
+ compileSdkVersion rootProject.ext.compileSdkVersion
+ buildToolsVersion rootProject.ext.buildToolsVersion
+
+ defaultConfig {
+ applicationId "com.android.tv.samples.sampletvinteractiveappservice"
+ minSdkVersion rootProject.ext.minSdkVersion
+ targetSdkVersion rootProject.ext.targetSdkVersion
+ versionCode rootProject.ext.versionCode
+ versionName rootProject.ext.versionName
+ }
+ android.applicationVariants.all { variant ->
+ variant.outputs.all {
+ outputFileName = "SampleTvInteractiveAppService-v${defaultConfig.versionName}.apk"
+ }
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+}
+dependencies {
+ implementation 'androidx.leanback:leanback:1.0.0'
+} \ No newline at end of file
diff --git a/interactive/SampleTvInteractiveAppService/res/layout/sample_layout.xml b/interactive/SampleTvInteractiveAppService/res/layout/sample_layout.xml
new file mode 100644
index 00000000..915c3526
--- /dev/null
+++ b/interactive/SampleTvInteractiveAppService/res/layout/sample_layout.xml
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2022 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:padding="50dp">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:background="@color/overlay_background_color">
+
+ <TextView
+ android:layout_gravity="center_horizontal"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="20dp"
+ android:text="@string/overlay_title_string"
+ android:textColor="@color/overlay_text_color"
+ android:textSize="32sp"/>
+ <TextView
+ style="@style/overlay_text_item"
+ android:text="@string/red_button_string"
+ android:textStyle="bold"/>
+ <Space
+ android:layout_width="match_parent"
+ android:layout_height="20dp"/>
+ <TextView
+ style="@style/overlay_text_item"
+ android:id="@+id/app_service_id"/>
+ <TextView
+ style="@style/overlay_text_item"
+ android:id="@+id/tv_input_id"/>
+ <TextView
+ style="@style/overlay_text_item"
+ android:id="@+id/channel_uri"/>
+ <TextView
+ style="@style/overlay_text_item"
+ android:id="@+id/video_track_selected"/>
+ <TextView
+ style="@style/overlay_text_item"
+ android:id="@+id/audio_track_selected"/>
+ <TextView
+ style="@style/overlay_text_item"
+ android:id="@+id/subtitle_track_selected"/>
+ <TextView
+ style="@style/overlay_text_item"
+ android:id="@+id/log_text"/>
+ </LinearLayout>
+</RelativeLayout> \ No newline at end of file
diff --git a/interactive/SampleTvInteractiveAppService/res/mipmap-hdpi/ic_launcher.webp b/interactive/SampleTvInteractiveAppService/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 00000000..c209e78e
--- /dev/null
+++ b/interactive/SampleTvInteractiveAppService/res/mipmap-hdpi/ic_launcher.webp
Binary files differ
diff --git a/interactive/SampleTvInteractiveAppService/res/mipmap-mdpi/ic_launcher.webp b/interactive/SampleTvInteractiveAppService/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 00000000..4f0f1d64
--- /dev/null
+++ b/interactive/SampleTvInteractiveAppService/res/mipmap-mdpi/ic_launcher.webp
Binary files differ
diff --git a/interactive/SampleTvInteractiveAppService/res/mipmap-xhdpi/ic_launcher.webp b/interactive/SampleTvInteractiveAppService/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 00000000..948a3070
--- /dev/null
+++ b/interactive/SampleTvInteractiveAppService/res/mipmap-xhdpi/ic_launcher.webp
Binary files differ
diff --git a/interactive/SampleTvInteractiveAppService/res/mipmap-xxhdpi/ic_launcher.webp b/interactive/SampleTvInteractiveAppService/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 00000000..28d4b77f
--- /dev/null
+++ b/interactive/SampleTvInteractiveAppService/res/mipmap-xxhdpi/ic_launcher.webp
Binary files differ
diff --git a/interactive/SampleTvInteractiveAppService/res/mipmap-xxxhdpi/ic_launcher.webp b/interactive/SampleTvInteractiveAppService/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 00000000..aa7d6427
--- /dev/null
+++ b/interactive/SampleTvInteractiveAppService/res/mipmap-xxxhdpi/ic_launcher.webp
Binary files differ
diff --git a/interactive/SampleTvInteractiveAppService/res/values/colors.xml b/interactive/SampleTvInteractiveAppService/res/values/colors.xml
new file mode 100644
index 00000000..d2a0a25a
--- /dev/null
+++ b/interactive/SampleTvInteractiveAppService/res/values/colors.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2022 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources>
+ <color name="overlay_background_color">#CCCCCCCC</color>
+ <color name="overlay_text_color">#FF000000</color>
+</resources> \ No newline at end of file
diff --git a/interactive/SampleTvInteractiveAppService/res/values/strings.xml b/interactive/SampleTvInteractiveAppService/res/values/strings.xml
new file mode 100644
index 00000000..d0c33d7f
--- /dev/null
+++ b/interactive/SampleTvInteractiveAppService/res/values/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2022 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources>
+ <string name="sample_tias">SampleTvInteractiveAppService</string>
+ <string-array name="sub_iapp_service_types">
+ <item>hbbtv</item>
+ <item>ginga</item>
+ <item>atsc</item>
+ </string-array>
+ <string name="overlay_title_string">Sample TV Interactive App Service</string>
+ <string name="red_button_string">Press the Red Interactive Button to tune to the next channel</string>
+</resources> \ No newline at end of file
diff --git a/interactive/SampleTvInteractiveAppService/res/values/styles.xml b/interactive/SampleTvInteractiveAppService/res/values/styles.xml
new file mode 100644
index 00000000..d207c99e
--- /dev/null
+++ b/interactive/SampleTvInteractiveAppService/res/values/styles.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2022 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+ <style name="overlay_text_item">
+ <item name="android:layout_gravity">left</item>
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:layout_marginLeft">25dp</item>
+ <item name="android:layout_marginRight">25dp</item>
+ <item name="android:layout_marginBottom">5dp</item>
+ <item name="android:textColor">@color/overlay_text_color</item>
+ <item name="android:textSize">20sp</item>
+ </style>
+</resources> \ No newline at end of file
diff --git a/interactive/SampleTvInteractiveAppService/res/xml/tviappservice.xml b/interactive/SampleTvInteractiveAppService/res/xml/tviappservice.xml
new file mode 100644
index 00000000..87020f26
--- /dev/null
+++ b/interactive/SampleTvInteractiveAppService/res/xml/tviappservice.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2022 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT 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-interactive-app xmlns:android="http://schemas.android.com/apk/res/android"
+ android:supportedTypes="@array/sub_iapp_service_types" /> \ No newline at end of file
diff --git a/interactive/SampleTvInteractiveAppService/src/com/android/tv/samples/sampletvinteractiveappservice/SampleTvInteractiveAppService.java b/interactive/SampleTvInteractiveAppService/src/com/android/tv/samples/sampletvinteractiveappservice/SampleTvInteractiveAppService.java
new file mode 100644
index 00000000..c53748eb
--- /dev/null
+++ b/interactive/SampleTvInteractiveAppService/src/com/android/tv/samples/sampletvinteractiveappservice/SampleTvInteractiveAppService.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.samples.sampletvinteractiveappservice;
+
+import android.media.tv.interactive.TvInteractiveAppService;
+import android.util.Log;
+
+public class SampleTvInteractiveAppService extends TvInteractiveAppService {
+ private static final String TAG = "SampleTvInteractiveAppService";
+ private static final boolean DEBUG = true;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ }
+
+ @Override
+ public Session onCreateSession(String iAppServiceId, int type) {
+ if (DEBUG) {
+ Log.d(TAG, "onCreateSession iAppServiceId=" + iAppServiceId + "type=" + type);
+ }
+ TiasSessionImpl session = new TiasSessionImpl(this, iAppServiceId, type);
+ session.prepare(this);
+ return session;
+ }
+}
diff --git a/interactive/SampleTvInteractiveAppService/src/com/android/tv/samples/sampletvinteractiveappservice/TiasSessionImpl.java b/interactive/SampleTvInteractiveAppService/src/com/android/tv/samples/sampletvinteractiveappservice/TiasSessionImpl.java
new file mode 100644
index 00000000..d85ab776
--- /dev/null
+++ b/interactive/SampleTvInteractiveAppService/src/com/android/tv/samples/sampletvinteractiveappservice/TiasSessionImpl.java
@@ -0,0 +1,863 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.samples.sampletvinteractiveappservice;
+
+import android.annotation.TargetApi;
+import android.app.Presentation;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.drawable.ColorDrawable;
+import android.hardware.display.DisplayManager;
+import android.hardware.display.VirtualDisplay;
+import android.media.MediaPlayer;
+import android.media.tv.AdRequest;
+import android.media.tv.AdResponse;
+import android.media.tv.BroadcastInfoRequest;
+import android.media.tv.BroadcastInfoResponse;
+import android.media.tv.SectionRequest;
+import android.media.tv.SectionResponse;
+import android.media.tv.StreamEventRequest;
+import android.media.tv.StreamEventResponse;
+import android.media.tv.TableRequest;
+import android.media.tv.TableResponse;
+import android.media.tv.TvTrackInfo;
+import android.media.tv.interactive.AppLinkInfo;
+import android.media.tv.interactive.TvInteractiveAppManager;
+import android.media.tv.interactive.TvInteractiveAppService;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.ParcelFileDescriptor;
+import android.text.TextUtils;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.Surface;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import android.widget.VideoView;
+
+import androidx.annotation.NonNull;
+
+import java.io.RandomAccessFile;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+public class TiasSessionImpl extends TvInteractiveAppService.Session {
+ private static final String TAG = "SampleTvInteractiveAppService";
+ private static final boolean DEBUG = true;
+
+ private static final String VIRTUAL_DISPLAY_NAME = "sample_tias_display";
+
+ // For testing purposes, limit the number of response for a single request
+ private static final int MAX_HANDLED_RESPONSE = 3;
+
+ private final Context mContext;
+ private TvInteractiveAppManager mTvIAppManager;
+ private final Handler mHandler;
+ private final String mAppServiceId;
+ private final int mType;
+ private final ViewGroup mViewContainer;
+ private Surface mSurface;
+ private VirtualDisplay mVirtualDisplay;
+ private List<TvTrackInfo> mTracks;
+
+ private TextView mTvInputIdView;
+ private TextView mChannelUriView;
+ private TextView mVideoTrackView;
+ private TextView mAudioTrackView;
+ private TextView mSubtitleTrackView;
+ private TextView mLogView;
+
+ private VideoView mVideoView;
+ private SurfaceView mAdSurfaceView;
+ private Surface mAdSurface;
+ private ParcelFileDescriptor mAdFd;
+ private FrameLayout mMediaContainer;
+ private int mAdState;
+ private int mWidth;
+ private int mHeight;
+ private int mScreenWidth;
+ private int mScreenHeight;
+ private String mCurrentTvInputId;
+ private Uri mCurrentChannelUri;
+ private String mSelectingAudioTrackId;
+ private String mFirstAudioTrackId;
+ private int mGeneratedRequestId = 0;
+ private boolean mRequestStreamEventFinished = false;
+ private int mSectionReceived = 0;
+ private List<String> mStreamDataList = new ArrayList<>();
+ private boolean mIsFullScreen = true;
+
+ public TiasSessionImpl(Context context, String iAppServiceId, int type) {
+ super(context);
+ if (DEBUG) {
+ Log.d(TAG, "Constructing service with iAppServiceId=" + iAppServiceId
+ + " type=" + type);
+ }
+ mContext = context;
+ mAppServiceId = iAppServiceId;
+ mType = type;
+ mHandler = new Handler(context.getMainLooper());
+ mTvIAppManager = (TvInteractiveAppManager) mContext.getSystemService(
+ Context.TV_INTERACTIVE_APP_SERVICE);
+
+ mViewContainer = new LinearLayout(context);
+ mViewContainer.setBackground(new ColorDrawable(0));
+ }
+
+ @Override
+ public View onCreateMediaView() {
+ mAdSurfaceView = new SurfaceView(mContext);
+ if (DEBUG) {
+ Log.d(TAG, "create surfaceView");
+ }
+ mAdSurfaceView.getHolder().setFormat(PixelFormat.TRANSLUCENT);
+ mAdSurfaceView
+ .getHolder()
+ .addCallback(
+ new SurfaceHolder.Callback() {
+ @Override
+ public void surfaceCreated(SurfaceHolder holder) {
+ mAdSurface = holder.getSurface();
+ }
+
+ @Override
+ public void surfaceChanged(
+ SurfaceHolder holder, int format, int width, int height) {
+ mAdSurface = holder.getSurface();
+ }
+
+ @Override
+ public void surfaceDestroyed(SurfaceHolder holder) {}
+ });
+ mAdSurfaceView.setVisibility(View.INVISIBLE);
+ ViewGroup.LayoutParams layoutParams =
+ new ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
+ mAdSurfaceView.setLayoutParams(layoutParams);
+ mMediaContainer.addView(mVideoView);
+ mMediaContainer.addView(mAdSurfaceView);
+ return mMediaContainer;
+ }
+
+ @Override
+ public void onAdResponse(AdResponse adResponse) {
+ mAdState = adResponse.getResponseType();
+ switch (mAdState) {
+ case AdResponse.RESPONSE_TYPE_PLAYING:
+ long time = adResponse.getElapsedTimeMillis();
+ updateLogText("AD is playing. " + time);
+ break;
+ case AdResponse.RESPONSE_TYPE_STOPPED:
+ updateLogText("AD is stopped.");
+ mAdSurfaceView.setVisibility(View.INVISIBLE);
+ break;
+ case AdResponse.RESPONSE_TYPE_FINISHED:
+ updateLogText("AD is play finished.");
+ mAdSurfaceView.setVisibility(View.INVISIBLE);
+ break;
+ }
+ }
+
+ @Override
+ public void onRelease() {
+ if (DEBUG) {
+ Log.d(TAG, "onRelease");
+ }
+ if (mSurface != null) {
+ mSurface.release();
+ mSurface = null;
+ }
+ if (mVirtualDisplay != null) {
+ mVirtualDisplay.release();
+ mVirtualDisplay = null;
+ }
+ }
+
+ @Override
+ public boolean onSetSurface(Surface surface) {
+ if (DEBUG) {
+ Log.d(TAG, "onSetSurface");
+ }
+ if (mSurface != null) {
+ mSurface.release();
+ }
+ updateSurface(surface, mWidth, mHeight);
+ mSurface = surface;
+ return true;
+ }
+
+ @Override
+ public void onSurfaceChanged(int format, int width, int height) {
+ if (DEBUG) {
+ Log.d(TAG, "onSurfaceChanged format=" + format + " width=" + width +
+ " height=" + height);
+ }
+ if (mSurface != null) {
+ updateSurface(mSurface, width, height);
+ mWidth = width;
+ mHeight = height;
+ }
+ }
+
+ @Override
+ public void onStartInteractiveApp() {
+ if (DEBUG) {
+ Log.d(TAG, "onStartInteractiveApp");
+ }
+ mHandler.post(
+ () -> {
+ initSampleView();
+ setMediaViewEnabled(true);
+ requestCurrentTvInputId();
+ requestCurrentChannelUri();
+ requestTrackInfoList();
+ }
+ );
+ }
+
+ @Override
+ public void onStopInteractiveApp() {
+ if (DEBUG) {
+ Log.d(TAG, "onStopInteractiveApp");
+ }
+ }
+
+ public void prepare(TvInteractiveAppService serviceCaller) {
+ // Slightly delay our post to ensure the Manager has had time to register our Session
+ mHandler.postDelayed(
+ () -> {
+ if (serviceCaller != null) {
+ serviceCaller.notifyStateChanged(mType,
+ TvInteractiveAppManager.SERVICE_STATE_READY,
+ TvInteractiveAppManager.ERROR_NONE);
+ }
+ },
+ 100);
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, @NonNull KeyEvent event) {
+ // TODO: use a menu view instead of key events for the following tests
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_PROG_RED:
+ tuneToNextChannel();
+ return true;
+ case KeyEvent.KEYCODE_A:
+ updateLogText("stop video broadcast begin");
+ tuneChannelByType(
+ TvInteractiveAppService.PLAYBACK_COMMAND_TYPE_STOP,
+ mCurrentTvInputId,
+ null);
+ updateLogText("stop video broadcast end");
+ return true;
+ case KeyEvent.KEYCODE_B:
+ updateLogText("resume video broadcast begin");
+ tuneChannelByType(
+ TvInteractiveAppService.PLAYBACK_COMMAND_TYPE_TUNE,
+ mCurrentTvInputId,
+ mCurrentChannelUri);
+ updateLogText("resume video broadcast end");
+ return true;
+ case KeyEvent.KEYCODE_C:
+ updateLogText("unselect audio track");
+ mSelectingAudioTrackId = null;
+ selectTrack(TvTrackInfo.TYPE_AUDIO, null);
+ return true;
+ case KeyEvent.KEYCODE_D:
+ updateLogText("select audio track " + mFirstAudioTrackId);
+ mSelectingAudioTrackId = mFirstAudioTrackId;
+ selectTrack(TvTrackInfo.TYPE_AUDIO, mFirstAudioTrackId);
+ return true;
+ case KeyEvent.KEYCODE_E:
+ if (mVideoView != null) {
+ if (mVideoView.isPlaying()) {
+ updateLogText("stop media");
+ mVideoView.stopPlayback();
+ mVideoView.setVisibility(View.GONE);
+ tuneChannelByType(
+ TvInteractiveAppService.PLAYBACK_COMMAND_TYPE_TUNE,
+ mCurrentTvInputId,
+ mCurrentChannelUri);
+ } else {
+ updateLogText("play media");
+ tuneChannelByType(
+ TvInteractiveAppService.PLAYBACK_COMMAND_TYPE_STOP,
+ mCurrentTvInputId,
+ null);
+ mVideoView.setVisibility(View.VISIBLE);
+ // TODO: put a file sample.mp4 in res/raw/ and use R.raw.sample for the URI
+ Uri uri = Uri.parse(
+ "android.resource://" + mContext.getPackageName() + "/");
+ mVideoView.setVideoURI(uri);
+ mVideoView.start();
+ updateLogText("media is playing");
+ }
+ }
+ return true;
+ case KeyEvent.KEYCODE_F:
+ updateLogText("request StreamEvent");
+ mRequestStreamEventFinished = false;
+ mStreamDataList.clear();
+ // TODO: build target URI instead of using channel URI
+ requestStreamEvent(
+ mCurrentChannelUri == null ? null : mCurrentChannelUri.toString(),
+ "event1");
+ return true;
+ case KeyEvent.KEYCODE_G:
+ updateLogText("change video bounds");
+ if (mIsFullScreen) {
+ setVideoBounds(new Rect(100, 150, 960, 540));
+ updateLogText("Change video broadcast size(100, 150, 960, 540)");
+ mIsFullScreen = false;
+ } else {
+ setVideoBounds(new Rect(0, 0, mScreenWidth, mScreenHeight));
+ updateLogText("Change video broadcast full screen");
+ mIsFullScreen = true;
+ }
+ return true;
+ case KeyEvent.KEYCODE_H:
+ updateLogText("request section");
+ mSectionReceived = 0;
+ requestSection(false, 0, 0x0, -1);
+ return true;
+ case KeyEvent.KEYCODE_I:
+ if (mTvIAppManager == null) {
+ updateLogText("TvIAppManager null");
+ return false;
+ }
+ List<AppLinkInfo> appLinks = getAppLinkInfoList();
+ if (appLinks.isEmpty()) {
+ updateLogText("Not found AppLink");
+ } else {
+ AppLinkInfo appLink = appLinks.get(0);
+ Intent intent = new Intent();
+ intent.setComponent(appLink.getComponentName());
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ mContext.getApplicationContext().startActivity(intent);
+ updateLogText("Launch " + appLink.getComponentName());
+ }
+ return true;
+ case KeyEvent.KEYCODE_J:
+ updateLogText("Request SI Tables ");
+ // Network Information Table (NIT)
+ requestTable(false, 0x40, /* TableRequest.TABLE_NAME_NIT */ 3, -1);
+ // Service Description Table (SDT)
+ requestTable(false, 0x42, /* TableRequest.TABLE_NAME_SDT */ 5, -1);
+ // Event Information Table (EIT)
+ requestTable(false, 0x4e, /* TableRequest.TABLE_NAME_EIT */ 6, -1);
+ return true;
+ case KeyEvent.KEYCODE_K:
+ updateLogText("Request Video Bounds");
+ requestCurrentVideoBoundsWrapper();
+ return true;
+ case KeyEvent.KEYCODE_L: {
+ updateLogText("stop video broadcast with blank mode");
+ Bundle params = new Bundle();
+ params.putInt(
+ /* TvInteractiveAppService.COMMAND_PARAMETER_KEY_STOP_MODE */
+ "command_stop_mode",
+ /* TvInteractiveAppService.COMMAND_PARAMETER_VALUE_STOP_MODE_BLANK */
+ 1);
+ tuneChannelByType(TvInteractiveAppService.PLAYBACK_COMMAND_TYPE_STOP,
+ mCurrentTvInputId, null, params);
+ return true;
+ }
+ case KeyEvent.KEYCODE_M: {
+ updateLogText("stop video broadcast with freeze mode");
+ Bundle params = new Bundle();
+ params.putInt(
+ /* TvInteractiveAppService.COMMAND_PARAMETER_KEY_STOP_MODE */
+ "command_stop_mode",
+ /* TvInteractiveAppService.COMMAND_PARAMETER_VALUE_STOP_MODE_FREEZE */
+ 2);
+ tuneChannelByType(TvInteractiveAppService.PLAYBACK_COMMAND_TYPE_STOP,
+ mCurrentTvInputId, null, params);
+ return true;
+ }
+ case KeyEvent.KEYCODE_N: {
+ updateLogText("request AD");
+ requestAd();
+ return true;
+ }
+ default:
+ return super.onKeyDown(keyCode, event);
+ }
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, @NonNull KeyEvent event) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_PROG_RED:
+ case KeyEvent.KEYCODE_A:
+ case KeyEvent.KEYCODE_B:
+ case KeyEvent.KEYCODE_C:
+ case KeyEvent.KEYCODE_D:
+ case KeyEvent.KEYCODE_E:
+ case KeyEvent.KEYCODE_F:
+ case KeyEvent.KEYCODE_G:
+ case KeyEvent.KEYCODE_H:
+ case KeyEvent.KEYCODE_I:
+ case KeyEvent.KEYCODE_J:
+ case KeyEvent.KEYCODE_K:
+ case KeyEvent.KEYCODE_L:
+ case KeyEvent.KEYCODE_M:
+ case KeyEvent.KEYCODE_N:
+ return true;
+ default:
+ return super.onKeyUp(keyCode, event);
+ }
+ }
+
+ public void updateLogText(String log) {
+ if (DEBUG) {
+ Log.d(TAG, log);
+ }
+ mLogView.setText(log);
+ }
+
+ private void updateSurface(Surface surface, int width, int height) {
+ mHandler.post(
+ () -> {
+ // Update our virtualDisplay if it already exists, create a new one otherwise
+ if (mVirtualDisplay != null) {
+ mVirtualDisplay.setSurface(surface);
+ mVirtualDisplay.resize(width, height, DisplayMetrics.DENSITY_DEFAULT);
+ } else {
+ DisplayManager displayManager =
+ mContext.getSystemService(DisplayManager.class);
+ if (displayManager == null) {
+ Log.e(TAG, "Failed to get DisplayManager");
+ return;
+ }
+ mVirtualDisplay = displayManager.createVirtualDisplay(VIRTUAL_DISPLAY_NAME,
+ width,
+ height,
+ DisplayMetrics.DENSITY_DEFAULT,
+ surface,
+ DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY);
+
+ Presentation presentation =
+ new Presentation(mContext, mVirtualDisplay.getDisplay());
+ presentation.setContentView(mViewContainer);
+ presentation.getWindow().setBackgroundDrawable(new ColorDrawable(0));
+ presentation.show();
+ }
+ });
+ }
+
+ private void initSampleView() {
+ View sampleView = LayoutInflater.from(mContext).inflate(R.layout.sample_layout, null);
+ TextView appServiceIdText = sampleView.findViewById(R.id.app_service_id);
+ appServiceIdText.setText("App Service ID: " + mAppServiceId);
+
+ mTvInputIdView = sampleView.findViewById(R.id.tv_input_id);
+ mChannelUriView = sampleView.findViewById(R.id.channel_uri);
+ mVideoTrackView = sampleView.findViewById(R.id.video_track_selected);
+ mAudioTrackView = sampleView.findViewById(R.id.audio_track_selected);
+ mSubtitleTrackView = sampleView.findViewById(R.id.subtitle_track_selected);
+ mLogView = sampleView.findViewById(R.id.log_text);
+ // Set default values for the selected tracks, since we cannot request data on them directly
+ mVideoTrackView.setText("No video track selected");
+ mAudioTrackView.setText("No audio track selected");
+ mSubtitleTrackView.setText("No subtitle track selected");
+
+ mVideoView = new VideoView(mContext);
+ mVideoView.setVisibility(View.GONE);
+ mVideoView.setOnCompletionListener(
+ new MediaPlayer.OnCompletionListener() {
+ @Override
+ public void onCompletion(MediaPlayer mediaPlayer) {
+ mVideoView.setVisibility(View.GONE);
+ mLogView.setText("MediaPlayer onCompletion");
+ tuneChannelByType(
+ TvInteractiveAppService.PLAYBACK_COMMAND_TYPE_TUNE,
+ mCurrentTvInputId,
+ mCurrentChannelUri);
+ }
+ });
+ mWidth = 0;
+ mHeight = 0;
+ WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
+ mScreenWidth = wm.getDefaultDisplay().getWidth();
+ mScreenHeight = wm.getDefaultDisplay().getHeight();
+
+ mViewContainer.addView(sampleView);
+ }
+
+ private void updateTrackSelectedView(int type, String trackId) {
+ mHandler.post(
+ () -> {
+ if (mTracks == null) {
+ return;
+ }
+ TvTrackInfo newSelectedTrack = null;
+ for (TvTrackInfo track : mTracks) {
+ if (track.getType() == type && track.getId().equals(trackId)) {
+ newSelectedTrack = track;
+ break;
+ }
+ }
+
+ if (newSelectedTrack == null) {
+ if (DEBUG) {
+ Log.d(TAG, "Did not find selected track within track list");
+ }
+ return;
+ }
+ switch (newSelectedTrack.getType()) {
+ case TvTrackInfo.TYPE_VIDEO:
+ mVideoTrackView.setText(
+ "Video Track: id= " + newSelectedTrack.getId()
+ + ", height=" + newSelectedTrack.getVideoHeight()
+ + ", width=" + newSelectedTrack.getVideoWidth()
+ + ", frame_rate=" + newSelectedTrack.getVideoFrameRate()
+ + ", pixel_ratio=" + newSelectedTrack.getVideoPixelAspectRatio()
+ );
+ break;
+ case TvTrackInfo.TYPE_AUDIO:
+ mAudioTrackView.setText(
+ "Audio Track: id=" + newSelectedTrack.getId()
+ + ", lang=" + newSelectedTrack.getLanguage()
+ + ", sample_rate=" + newSelectedTrack.getAudioSampleRate()
+ + ", channel_count=" + newSelectedTrack.getAudioChannelCount()
+ );
+ break;
+ case TvTrackInfo.TYPE_SUBTITLE:
+ mSubtitleTrackView.setText(
+ "Subtitle Track: id=" + newSelectedTrack.getId()
+ + ", lang=" + newSelectedTrack.getLanguage()
+ );
+ break;
+ }
+ }
+ );
+ }
+
+ private void tuneChannelByType(String type, String inputId, Uri channelUri, Bundle bundle) {
+ Bundle parameters = bundle == null ? new Bundle() : bundle;
+ if (TvInteractiveAppService.PLAYBACK_COMMAND_TYPE_TUNE.equals(type)) {
+ parameters.putString(
+ TvInteractiveAppService.COMMAND_PARAMETER_KEY_CHANNEL_URI,
+ channelUri == null ? null : channelUri.toString());
+ parameters.putString(TvInteractiveAppService.COMMAND_PARAMETER_KEY_INPUT_ID, inputId);
+ }
+ mHandler.post(() -> sendPlaybackCommandRequest(type, parameters));
+ // Delay request for new information to give time to tune
+ mHandler.postDelayed(
+ () -> {
+ requestCurrentTvInputId();
+ requestCurrentChannelUri();
+ requestTrackInfoList();
+ },
+ 1000
+ );
+ }
+
+ private void tuneChannelByType(String type, String inputId, Uri channelUri) {
+ tuneChannelByType(type, inputId, channelUri, new Bundle());
+ }
+
+ private void tuneToNextChannel() {
+ tuneChannelByType(TvInteractiveAppService.PLAYBACK_COMMAND_TYPE_TUNE_NEXT, null, null);
+ }
+
+ @Override
+ public void onCurrentChannelUri(Uri channelUri) {
+ if (DEBUG) {
+ Log.d(TAG, "onCurrentChannelUri uri=" + channelUri);
+ }
+ mCurrentChannelUri = channelUri;
+ mChannelUriView.setText("Channel URI: " + channelUri);
+ }
+
+ @Override
+ public void onTrackInfoList(List<TvTrackInfo> tracks) {
+ if (DEBUG) {
+ Log.d(TAG, "onTrackInfoList size=" + tracks.size());
+ for (int i = 0; i < tracks.size(); i++) {
+ TvTrackInfo trackInfo = tracks.get(i);
+ if (trackInfo != null) {
+ Log.d(TAG, "track " + i + ": type=" + trackInfo.getType() +
+ " id=" + trackInfo.getId());
+ }
+ }
+ }
+ for (TvTrackInfo info : tracks) {
+ if (info.getType() == TvTrackInfo.TYPE_AUDIO) {
+ mFirstAudioTrackId = info.getId();
+ break;
+ }
+ }
+ mTracks = tracks;
+ }
+
+ @Override
+ public void onTracksChanged(List<TvTrackInfo> tracks) {
+ if (DEBUG) {
+ Log.d(TAG, "onTracksChanged");
+ }
+ onTrackInfoList(tracks);
+ }
+
+ @Override
+ public void onTrackSelected(int type, String trackId) {
+ if (DEBUG) {
+ Log.d(TAG, "onTrackSelected type=" + type + " trackId=" + trackId);
+ }
+ updateTrackSelectedView(type, trackId);
+
+ if (TextUtils.equals(mSelectingAudioTrackId, trackId)) {
+ if (mSelectingAudioTrackId == null) {
+ updateLogText("unselect audio succeed");
+ } else {
+ updateLogText("select audio succeed");
+ }
+ }
+ }
+
+ @Override
+ public void onCurrentTvInputId(String inputId) {
+ if (DEBUG) {
+ Log.d(TAG, "onCurrentTvInputId id=" + inputId);
+ }
+ mCurrentTvInputId = inputId;
+ mTvInputIdView.setText("TV Input ID: " + inputId);
+ }
+
+ @Override
+ public void onTuned(Uri channelUri) {
+ mCurrentChannelUri = channelUri;
+ }
+
+ @Override
+ public void onCurrentVideoBounds(@NonNull Rect bounds) {
+ updateLogText("Received video Bounds " + bounds.toShortString());
+ }
+
+ @Override
+ public void onBroadcastInfoResponse(BroadcastInfoResponse response) {
+ if (mGeneratedRequestId == response.getRequestId()) {
+ if (!mRequestStreamEventFinished && response instanceof StreamEventResponse) {
+ handleStreamEventResponse((StreamEventResponse) response);
+ } else if (mSectionReceived < MAX_HANDLED_RESPONSE
+ && response instanceof SectionResponse) {
+ handleSectionResponse((SectionResponse) response);
+ } else if (response instanceof TableResponse) {
+ handleTableResponse((TableResponse) response);
+ }
+ }
+ }
+
+ private void handleSectionResponse(SectionResponse response) {
+ mSectionReceived++;
+ byte[] data = null;
+ Bundle params = response.getSessionData();
+ if (params != null) {
+ // TODO: define the key
+ data = params.getByteArray("key_raw_data");
+ }
+ int version = response.getVersion();
+ updateLogText(
+ "Received section data version = "
+ + version
+ + ", data = "
+ + Arrays.toString(data));
+ }
+
+ private void handleStreamEventResponse(StreamEventResponse response) {
+ updateLogText("Received stream event response");
+ byte[] rData = response.getData();
+ if (rData == null) {
+ mRequestStreamEventFinished = true;
+ updateLogText("Received stream event data is null");
+ return;
+ }
+ // TODO: convert to Hex instead
+ String data = Arrays.toString(rData);
+ if (mStreamDataList.contains(data)) {
+ return;
+ }
+ mStreamDataList.add(data);
+ updateLogText(
+ "Received stream event data("
+ + (mStreamDataList.size() - 1)
+ + "): "
+ + data);
+ if (mStreamDataList.size() >= MAX_HANDLED_RESPONSE) {
+ mRequestStreamEventFinished = true;
+ updateLogText("Received stream event data finished");
+ }
+ }
+
+ private void handleTableResponse(TableResponse response) {
+ updateLogText(
+ "Received table data version = "
+ + response.getVersion()
+ + ", size="
+ + response.getSize()
+ + ", requestId="
+ + response.getRequestId()
+ + ", data = "
+ + Arrays.toString(getTableByteArray(response)));
+ }
+
+ private void selectTrack(int type, String trackId) {
+ Bundle params = new Bundle();
+ params.putInt(TvInteractiveAppService.COMMAND_PARAMETER_KEY_TRACK_TYPE, type);
+ params.putString(TvInteractiveAppService.COMMAND_PARAMETER_KEY_TRACK_ID, trackId);
+ mHandler.post(
+ () ->
+ sendPlaybackCommandRequest(
+ TvInteractiveAppService.PLAYBACK_COMMAND_TYPE_SELECT_TRACK,
+ params));
+ }
+
+ private int generateRequestId() {
+ return ++mGeneratedRequestId;
+ }
+
+ private void requestStreamEvent(String targetUri, String eventName) {
+ if (targetUri == null) {
+ return;
+ }
+ int requestId = generateRequestId();
+ BroadcastInfoRequest request =
+ new StreamEventRequest(
+ requestId,
+ BroadcastInfoRequest.REQUEST_OPTION_AUTO_UPDATE,
+ Uri.parse(targetUri),
+ eventName);
+ requestBroadcastInfo(request);
+ }
+
+ private void requestSection(boolean repeat, int tsPid, int tableId, int version) {
+ int requestId = generateRequestId();
+ BroadcastInfoRequest request =
+ new SectionRequest(
+ requestId,
+ repeat ?
+ BroadcastInfoRequest.REQUEST_OPTION_REPEAT :
+ BroadcastInfoRequest.REQUEST_OPTION_AUTO_UPDATE,
+ tsPid,
+ tableId,
+ version);
+ requestBroadcastInfo(request);
+ }
+
+ private void requestTable(boolean repeat, int tableId, int tableName, int version) {
+ int requestId = generateRequestId();
+ BroadcastInfoRequest request =
+ new TableRequest(
+ requestId,
+ repeat
+ ? BroadcastInfoRequest.REQUEST_OPTION_REPEAT
+ : BroadcastInfoRequest.REQUEST_OPTION_AUTO_UPDATE,
+ tableId,
+ tableName,
+ version);
+ requestBroadcastInfo(request);
+ }
+
+ public void requestAd() {
+ try {
+ // TODO: add the AD file to this project
+ RandomAccessFile adiFile =
+ new RandomAccessFile(
+ mContext.getApplicationContext().getFilesDir() + "/ad.mp4", "r");
+ mAdFd = ParcelFileDescriptor.dup(adiFile.getFD());
+ } catch (Exception e) {
+ updateLogText("open advertisement file failed. " + e.getMessage());
+ return;
+ }
+ long startTime = 20000;
+ long stopTime = startTime + 25000;
+ long echoInterval = 1000;
+ String mediaFileType = "MP4";
+ mHandler.post(
+ () -> {
+ AdRequest adRequest;
+ if (mAdState == AdResponse.RESPONSE_TYPE_PLAYING) {
+ updateLogText("RequestAd stop");
+ adRequest =
+ new AdRequest(
+ mGeneratedRequestId,
+ AdRequest.REQUEST_TYPE_STOP,
+ null,
+ 0,
+ 0,
+ 0,
+ null,
+ null);
+ } else {
+ updateLogText("RequestAd start");
+ int requestId = generateRequestId();
+ mAdSurfaceView.getHolder().setFormat(PixelFormat.TRANSLUCENT);
+ mAdSurfaceView.setVisibility(View.VISIBLE);
+ Bundle bundle = new Bundle();
+ bundle.putParcelable("dai_surface", mAdSurface);
+ adRequest =
+ new AdRequest(
+ requestId,
+ AdRequest.REQUEST_TYPE_START,
+ mAdFd,
+ startTime,
+ stopTime,
+ echoInterval,
+ mediaFileType,
+ bundle);
+ }
+ requestAd(adRequest);
+ });
+ }
+
+ @TargetApi(34)
+ private List<AppLinkInfo> getAppLinkInfoList() {
+ if (Build.VERSION.SDK_INT < 34 || mTvIAppManager == null) {
+ return new ArrayList<>();
+ }
+ return mTvIAppManager.getAppLinkInfoList();
+ }
+
+ @TargetApi(34)
+ private void requestCurrentVideoBoundsWrapper() {
+ if (Build.VERSION.SDK_INT < 34) {
+ return;
+ }
+ requestCurrentVideoBounds();
+ }
+
+ @TargetApi(34)
+ private byte[] getTableByteArray(TableResponse response) {
+ if (Build.VERSION.SDK_INT < 34) {
+ return null;
+ }
+ return response.getTableByteArray();
+ }
+}
diff --git a/lint-baseline.xml b/lint-baseline.xml
index d91a1894..29aff212 100644
--- a/lint-baseline.xml
+++ b/lint-baseline.xml
@@ -25,28 +25,6 @@
<issue
id="NewApi"
- message="Call requires API level 24 (current min is 23): `updateAndStartServiceIfNeeded`"
- errorLine1=" scheduler.updateAndStartServiceIfNeeded();"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/apps/TV/src/com/android/tv/receiver/BootCompletedReceiver.java"
- line="90"
- column="23"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 24 (current min is 23): `android.media.tv.TvContract#isChannelUriForPassthroughInput`"
- errorLine1=" if (!TvContract.isChannelUriForPassthroughInput(uri)) {"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/apps/TV/src/com/android/tv/data/ChannelImpl.java"
- line="444"
- column="25"/>
- </issue>
-
- <issue
- id="NewApi"
message="Call requires API level 24 (current min is 23): `android.media.tv.TvInputInfo#canRecord`"
errorLine1=" if (info.canRecord()) {"
errorLine2=" ~~~~~~~~~">
@@ -80,17 +58,6 @@
<issue
id="NewApi"
- message="Call requires API level 24 (current min is 23): `android.media.tv.TvInputInfo#loadCustomLabel`"
- errorLine1=" CharSequence customLabel = input.loadCustomLabel(getContext());"
- errorLine2=" ~~~~~~~~~~~~~~~">
- <location
- file="packages/apps/TV/src/com/android/tv/ui/InputBannerView.java"
- line="75"
- column="42"/>
- </issue>
-
- <issue
- id="NewApi"
message="Call requires API level 24 (current min is 23): `android.media.tv.TvInputInfo#canRecord`"
errorLine1=" tunerCount = mInput.canRecord() ? mInput.getTunerCount() : 0;"
errorLine2=" ~~~~~~~~~">
@@ -113,193 +80,6 @@
<issue
id="NewApi"
- message="Call requires API level 24 (current min is 23): `android.media.tv.TvContract#isChannelUriForPassthroughInput`"
- errorLine1=" TvContract.isChannelUriForPassthroughInput(getIntent().getData());"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/apps/TV/src/com/android/tv/MainActivity.java"
- line="534"
- column="28"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 24 (current min is 23): `android.media.tv.TvContract#isChannelUriForPassthroughInput`"
- errorLine1=" if (TvContract.isChannelUriForPassthroughInput(mInitChannelUri)) {"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/apps/TV/src/com/android/tv/MainActivity.java"
- line="1002"
- column="28"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 24 (current min is 23): `android.media.tv.TvContract#isChannelUriForPassthroughInput`"
- errorLine1=" if ((channelUri == null || !TvContract.isChannelUriForPassthroughInput(channelUri))"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/apps/TV/src/com/android/tv/MainActivity.java"
- line="1029"
- column="48"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 24 (current min is 23): `android.media.tv.TvContract#isChannelUriForPassthroughInput`"
- errorLine1=" TvContract.isChannelUriForPassthroughInput(channelUri)"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/apps/TV/src/com/android/tv/MainActivity.java"
- line="1037"
- column="28"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 24 (current min is 23): `android.media.tv.TvContract#isChannelUriForPassthroughInput`"
- errorLine1=" if (TvContract.isChannelUriForPassthroughInput(channelUri)) {"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/apps/TV/src/com/android/tv/MainActivity.java"
- line="1065"
- column="28"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 24 (current min is 23): `android.media.tv.TvContract#isChannelUriForPassthroughInput`"
- errorLine1=" } else if (TvContract.isChannelUriForPassthroughInput(mInitChannelUri)) {"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/apps/TV/src/com/android/tv/MainActivity.java"
- line="1544"
- column="35"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Method reference requires API level 24 (current min is 23): `MainActivity.super::enterPictureInPictureMode`"
- errorLine1=" mHandler.post(MainActivity.super::enterPictureInPictureMode);"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/apps/TV/src/com/android/tv/MainActivity.java"
- line="2402"
- column="27"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 24 (current min is 23): `android.media.tv.TvContract#isChannelUriForPassthroughInput`"
- errorLine1=" return TvContract.isChannelUriForPassthroughInput(uri)"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/apps/TV/src/com/android/tv/MainActivity.java"
- line="2813"
- column="27"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 28 (current min is 23): `android.media.tv.TvInputManager#getBlockedRatings`"
- errorLine1=" for (TvContentRating tvContentRating : mTvInputManager.getBlockedRatings()) {"
- errorLine2=" ~~~~~~~~~~~~~~~~~">
- <location
- file="packages/apps/TV/src/com/android/tv/parental/ParentalControlSettings.java"
- line="74"
- column="68"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 28 (current min is 23): `android.media.tv.TvInputManager#getBlockedRatings`"
- errorLine1=" mRatings = new HashSet&lt;>(mTvInputManager.getBlockedRatings());"
- errorLine2=" ~~~~~~~~~~~~~~~~~">
- <location
- file="packages/apps/TV/src/com/android/tv/parental/ParentalControlSettings.java"
- line="89"
- column="50"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 28 (current min is 23): `android.media.tv.TvInputManager#getBlockedRatings`"
- errorLine1=" Set&lt;TvContentRating> removed = new HashSet&lt;>(mTvInputManager.getBlockedRatings());"
- errorLine2=" ~~~~~~~~~~~~~~~~~">
- <location
- file="packages/apps/TV/src/com/android/tv/parental/ParentalControlSettings.java"
- line="93"
- column="70"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 28 (current min is 23): `android.media.tv.TvInputManager#getBlockedRatings`"
- errorLine1=" added.removeAll(mTvInputManager.getBlockedRatings());"
- errorLine2=" ~~~~~~~~~~~~~~~~~">
- <location
- file="packages/apps/TV/src/com/android/tv/parental/ParentalControlSettings.java"
- line="100"
- column="41"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 24 (current min is 23): `android.media.tv.TvContract#isChannelUriForPassthroughInput`"
- errorLine1=" if (TvContract.isChannelUriForPassthroughInput(channelUri)) {"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/apps/TV/src/com/android/tv/SelectInputActivity.java"
- line="69"
- column="28"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 24 (current min is 23): `android.media.tv.TvInputInfo#isHidden`"
- errorLine1=" if (!input.isHidden(getContext())) {"
- errorLine2=" ~~~~~~~~">
- <location
- file="packages/apps/TV/src/com/android/tv/ui/SelectInputView.java"
- line="253"
- column="28"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 24 (current min is 23): `android.media.tv.TvInputInfo#loadCustomLabel`"
- errorLine1=" CharSequence customLabel = input.loadCustomLabel(getContext());"
- errorLine2=" ~~~~~~~~~~~~~~~">
- <location
- file="packages/apps/TV/src/com/android/tv/ui/SelectInputView.java"
- line="287"
- column="42"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 24 (current min is 23): `android.media.tv.TvView#tune`"
- errorLine1=" mTvView.tune(mInputInfo.getId(), mCurrentChannel.getUri(), params);"
- errorLine2=" ~~~~">
- <location
- file="packages/apps/TV/src/com/android/tv/ui/TunableTvView.java"
- line="671"
- column="21"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 24 (current min is 23): `android.media.tv.TvInputInfo#getTunerCount`"
- errorLine1=" input.getTunerCount(),"
- errorLine2=" ~~~~~~~~~~~~~">
- <location
- file="packages/apps/TV/src/com/android/tv/ui/TunableTvView.java"
- line="1174"
- column="39"/>
- </issue>
-
- <issue
- id="NewApi"
message="Call requires API level 24 (current min is 23): `createScheduler`"
errorLine1=" mRecordingScheduler = RecordingScheduler.createScheduler(this);"
errorLine2=" ~~~~~~~~~~~~~~~">
@@ -311,61 +91,6 @@
<issue
id="NewApi"
- message="Call requires API level 24 (current min is 23): `android.media.tv.TvInputInfo#isHidden`"
- errorLine1=" if (!input.isHidden(this)) {"
- errorLine2=" ~~~~~~~~">
- <location
- file="packages/apps/TV/src/com/android/tv/TvApplication.java"
- line="402"
- column="28"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 24 (current min is 23): `android.media.tv.TvInputInfo#loadCustomLabel`"
- errorLine1=" CharSequence inputCustomLabel = info.loadCustomLabel(mContext);"
- errorLine2=" ~~~~~~~~~~~~~~~">
- <location
- file="packages/apps/TV/src/com/android/tv/util/TvInputManagerHelper.java"
- line="216"
- column="62"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 24 (current min is 23): `android.media.tv.TvInputInfo#loadCustomLabel`"
- errorLine1=" CharSequence inputCustomLabel = info.loadCustomLabel(mContext);"
- errorLine2=" ~~~~~~~~~~~~~~~">
- <location
- file="packages/apps/TV/src/com/android/tv/util/TvInputManagerHelper.java"
- line="257"
- column="58"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 24 (current min is 23): `android.media.tv.TvInputManager.TvInputCallback#onInputUpdated`"
- errorLine1=" callback.onInputUpdated(inputId);"
- errorLine2=" ~~~~~~~~~~~~~~">
- <location
- file="packages/apps/TV/src/com/android/tv/util/TvInputManagerHelper.java"
- line="265"
- column="34"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 24 (current min is 23): `android.media.tv.TvInputInfo#loadCustomLabel`"
- errorLine1=" CharSequence inputCustomLabel = inputInfo.loadCustomLabel(mContext);"
- errorLine2=" ~~~~~~~~~~~~~~~">
- <location
- file="packages/apps/TV/src/com/android/tv/util/TvInputManagerHelper.java"
- line="279"
- column="63"/>
- </issue>
-
- <issue
- id="NewApi"
message="Call requires API level 24 (current min is 23): `android.media.tv.TvInputManager.TvInputCallback#onTvInputInfoUpdated`"
errorLine1=" callback.onTvInputInfoUpdated(inputInfo);"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
@@ -377,46 +102,26 @@
<issue
id="NewApi"
- message="Call requires API level 24 (current min is 23): `android.media.tv.TvInputInfo#loadCustomLabel`"
- errorLine1=" CharSequence customLabelCharSequence = info.loadCustomLabel(mContext);"
- errorLine2=" ~~~~~~~~~~~~~~~">
- <location
- file="packages/apps/TV/src/com/android/tv/util/TvInputManagerHelper.java"
- line="472"
- column="57"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 24 (current min is 23): `android.media.tv.TvInputInfo#loadCustomLabel`"
- errorLine1=" String customLabel = canonicalizeLabel(input.loadCustomLabel(mContext));"
- errorLine2=" ~~~~~~~~~~~~~~~">
+ message="Call requires API level 24 (current min is 23): `android.media.tv.TvInputInfo#getTunerCount`">
<location
- file="packages/apps/TV/src/com/android/tv/search/TvProviderSearch.java"
- line="510"
- column="58"/>
+ file="packages/apps/TV/src/com/android/tv/ui/TunableTvView.java"
+ line="1205"/>
</issue>
<issue
id="NewApi"
- message="Call requires API level 24 (current min is 23): `android.media.tv.TvInputInfo#loadCustomLabel`"
- errorLine1=" String customLabel = canonicalizeLabel(input.loadCustomLabel(mContext));"
- errorLine2=" ~~~~~~~~~~~~~~~">
+ message="Call requires API level 24 (current min is 23): `updateAndStartServiceIfNeeded`">
<location
- file="packages/apps/TV/src/com/android/tv/search/TvProviderSearch.java"
- line="535"
- column="58"/>
+ file="packages/apps/TV/src/com/android/tv/receiver/BootCompletedReceiver.java"
+ line="95"/>
</issue>
<issue
id="NewApi"
- message="Call requires API level 24 (current min is 23): `android.media.tv.TvContract#isChannelUriForPassthroughInput`"
- errorLine1=" return isChannelUriForTunerInput(uri) || TvContract.isChannelUriForPassthroughInput(uri);"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ message="Method reference requires API level 24 (current min is 23): `MainActivity.super::enterPictureInPictureMode`">
<location
- file="packages/apps/TV/src/com/android/tv/util/Utils.java"
- line="276"
- column="61"/>
+ file="packages/apps/TV/src/com/android/tv/MainActivity.java"
+ line="2435"/>
</issue>
-</issues>
+</issues> \ No newline at end of file
diff --git a/res/drawable/empty_input_background.png b/res/drawable/empty_input_background.png
new file mode 100644
index 00000000..2dc3970a
--- /dev/null
+++ b/res/drawable/empty_input_background.png
Binary files differ
diff --git a/res/drawable/ic_empty_input_hdmi.xml b/res/drawable/ic_empty_input_hdmi.xml
new file mode 100644
index 00000000..b2150a19
--- /dev/null
+++ b/res/drawable/ic_empty_input_hdmi.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="48dp"
+ android:height="48dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960"
+ android:tint="@color/empty_input_status_icon_tint_color">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M306,896L306,767L184,530L184,275L224,275L224,136Q224,108.05 245.84,86.03Q267.69,64 297,64L663,64Q692.05,64 714.03,86.03Q736,108.05 736,136L736,275L776,275L776,530L654,767L654,896L306,896ZM297,275L395,275L395,194L432,194L432,275L528,275L528,194L565,194L565,275L663,275L663,137Q663,137 663,137Q663,137 663,137L297,137Q297,137 297,137Q297,137 297,137L297,275ZM379,823L581,823L581,749L703,510L703,348L257,348L257,510L379,749L379,823ZM480,510L480,510L480,510L480,510L480,510L480,510L480,510L480,510L480,510Z"/>
+</vector> \ No newline at end of file
diff --git a/res/drawable/ic_empty_input_tuner.xml b/res/drawable/ic_empty_input_tuner.xml
new file mode 100644
index 00000000..9f694e98
--- /dev/null
+++ b/res/drawable/ic_empty_input_tuner.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="48dp"
+ android:height="48dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960"
+ android:tint="@color/empty_input_status_icon_tint_color">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M199,689Q138,630 106.5,552.5Q75,475 75,400Q75,325 105,248.5Q135,172 199,111L242,154Q191,205 163.5,271.5Q136,338 136,400Q136,462 163.5,528.5Q191,595 242,646L199,689ZM289,599Q247,560 227,506.5Q207,453 207,400Q207,351 226.5,296Q246,241 289,201L332,244Q302,274 285,318Q268,362 268,400Q268,434 285.5,479Q303,524 332,556L289,599ZM285,886L420,477Q403,464 393.5,444.5Q384,425 384,400Q384,359 411.5,331Q439,303 480,303Q521,303 549,331Q577,359 577,400Q577,425 567,444.5Q557,464 541,477L675,886L602,886L573,796L388,796L358,886L285,886ZM411,723L549,723L480,512L411,723ZM671,599L628,556Q658,526 675.5,482Q693,438 693,400Q693,366 675,321Q657,276 628,244L671,201Q714,241 734.5,296.5Q755,352 754,400Q754,448 733.5,503.5Q713,559 671,599ZM761,689L718,646Q769,595 797,528.5Q825,462 825,400Q825,338 797,271.5Q769,205 718,154L761,111Q823,171 854.5,248Q886,325 886,400Q886,476 856,552Q826,628 761,689Z"/>
+</vector> \ No newline at end of file
diff --git a/res/drawable/input_banner_v2_background.xml b/res/drawable/input_banner_v2_background.xml
new file mode 100644
index 00000000..ac93e32c
--- /dev/null
+++ b/res/drawable/input_banner_v2_background.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <corners android:radius="16dp" />
+ <solid android:color="#E6252A34"/>
+</shape> \ No newline at end of file
diff --git a/res/drawable/tv_iapp_dialog_background.xml b/res/drawable/tv_iapp_dialog_background.xml
new file mode 100755
index 00000000..3f6f8e6c
--- /dev/null
+++ b/res/drawable/tv_iapp_dialog_background.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2022 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <solid android:color="@color/tv_iapp_dialog_background"/>
+ <corners android:radius="2dp" />
+</shape>
diff --git a/res/layout/activity_tv.xml b/res/layout/activity_tv.xml
index b6a0a3a3..6347f897 100644
--- a/res/layout/activity_tv.xml
+++ b/res/layout/activity_tv.xml
@@ -28,6 +28,12 @@
android:layout_height="match_parent"
android:layout_gravity="start|center_vertical" />
+ <android.media.tv.interactive.TvInteractiveAppView
+ android:id="@+id/tv_app_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@android:color/transparent" />
+
<FrameLayout
android:id="@+id/scene_container"
android:layout_height="match_parent"
diff --git a/res/layout/block_screen.xml b/res/layout/block_screen.xml
index c564efd0..7edc9839 100644
--- a/res/layout/block_screen.xml
+++ b/res/layout/block_screen.xml
@@ -71,4 +71,11 @@
android:lineSpacingExtra="@dimen/tvview_block_line_spacing_extra"
android:textColor="@color/tvview_block_text_color" />
</LinearLayout>
+
+ <!-- b/302992748 new config-controlled no signal page -->
+ <com.android.tv.ui.EmptyInputStatusBlockView
+ android:id="@+id/empty_input_status_block_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:visibility="gone"/>
</com.android.tv.ui.BlockScreenView>
diff --git a/res/layout/empty_input_status_block.xml b/res/layout/empty_input_status_block.xml
new file mode 100644
index 00000000..d44a2f01
--- /dev/null
+++ b/res/layout/empty_input_status_block.xml
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/block_screen_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@drawable/empty_input_background">
+
+ <ImageView
+ android:id="@+id/empty_input_status_icon"
+ android:layout_width="@dimen/empty_input_status_block_icon_width"
+ android:layout_height="@dimen/empty_input_status_block_icon_height"
+ android:contentDescription="@null"
+ android:src="@drawable/ic_empty_input_hdmi"
+ android:layout_marginTop="@dimen/empty_input_status_block_icon_margin_top"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <TextView
+ android:id="@+id/empty_input_status_title_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:fontFamily="google-sans-text-regular"
+ android:textColor="@color/empty_input_status_title_text_color"
+ android:textSize="@dimen/empty_input_status_block_title_text_size"
+ android:text="@string/empty_input_status_title_format"
+ android:layout_marginTop="@dimen/empty_input_status_block_title_margin_top"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/empty_input_status_icon"/>
+
+ <TextView
+ android:id="@+id/empty_input_status_info_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:fontFamily="google-sans-text-regular"
+ android:textColor="@color/empty_input_status_info_text_color"
+ android:textSize="@dimen/empty_input_status_block_info_text_size"
+ android:text="@string/empty_input_status_info"
+ android:layout_marginTop="@dimen/empty_input_status_block_info_margin_top"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/empty_input_status_title_text" />
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/res/layout/input_banner_v2.xml b/res/layout/input_banner_v2.xml
new file mode 100644
index 00000000..8f897827
--- /dev/null
+++ b/res/layout/input_banner_v2.xml
@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<!-- All info banners have the same id for use by TvTransitionManager. -->
+<com.android.tv.ui.InputBannerViewV2 xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/scene_transition_common"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="start|top"
+ android:layout_marginStart="24dp"
+ android:layout_marginTop="24dp"
+ android:background="@drawable/input_banner_v2_background"
+ android:maxWidth="@dimen/input_banner_item_label_max_width"
+ android:minWidth="76dp"
+ android:minHeight="39dp"
+ android:orientation="vertical"
+ android:elevation="8dp"
+ android:focusable="false"
+ android:paddingStart="20dp"
+ android:paddingTop="16dp"
+ android:paddingEnd="20dp"
+ android:paddingBottom="16dp">
+
+ <TextView
+ android:id="@+id/input_info"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:ellipsize="end"
+ android:fontFamily="google-sans-text-medium"
+ android:gravity="start"
+ android:singleLine="true"
+ android:text="@string/input_banner_v2_active_input"
+ android:textColor="#CCD2E3FC"
+ android:textSize="12sp" />
+
+ <TextView
+ android:id="@+id/input_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:layout_marginTop="2dp"
+ android:ellipsize="end"
+ android:fontFamily="google-sans-text-medium"
+ android:gravity="start"
+ android:singleLine="true"
+ android:textColor="#E8F0FE"
+ android:textSize="18sp" />
+
+</com.android.tv.ui.InputBannerViewV2>
diff --git a/res/layout/tv_app_dialog.xml b/res/layout/tv_app_dialog.xml
new file mode 100755
index 00000000..e12e0bf7
--- /dev/null
+++ b/res/layout/tv_app_dialog.xml
@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2022 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT 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="@dimen/pin_dialog_width"
+ android:layout_height="wrap_content"
+ android:paddingTop="19dp"
+ android:paddingBottom="24dp"
+ android:paddingStart="24dp"
+ android:paddingEnd="24dp"
+ android:elevation="8dp"
+ android:background="@drawable/tv_iapp_dialog_background">
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="@dimen/pin_dialog_title_width"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="7dp"
+ android:layout_centerHorizontal="true"
+ android:lineSpacingExtra="@dimen/pin_dialog_text_line_spacing"
+ android:textSize="@dimen/pin_dialog_text_size"
+ android:textColor="@color/tv_iapp_dialog_text_color"
+ android:fontFamily="@string/font"
+ android:singleLine="false" />
+ <LinearLayout
+ android:layout_below="@id/title"
+ android:layout_marginTop="20dp"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerHorizontal="true"
+ android:orientation="horizontal"
+ >
+ <Button
+ android:id="@+id/ok"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerHorizontal="true"
+ android:gravity="center"
+ android:text="ok"
+ android:importantForAccessibility="yes"
+ android:paddingStart="24dp"
+ android:paddingEnd="24dp" />
+ <Button
+ android:id="@+id/cancel"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_centerHorizontal="true"
+ android:layout_marginLeft="30dp"
+ android:gravity="center"
+ android:text="cancel"
+ android:importantForAccessibility="yes"
+ android:paddingStart="24dp"
+ android:paddingEnd="24dp" />
+ </LinearLayout>
+ </RelativeLayout>
+</FrameLayout>
diff --git a/res/values/arrays-custom.xml b/res/values/arrays-custom.xml
index 252d6f4f..10f4402d 100644
--- a/res/values/arrays-custom.xml
+++ b/res/values/arrays-custom.xml
@@ -42,4 +42,17 @@
<item>Set up your newly installed channel sources to customize your channel list.
\nChoose the Channel sources within the Settings menu to get started.</item>
</string-array>
+
+ <!-- An array of input setup component names in the form of
+ <code>input_id + '#' + flattened_component_name</code>.
+ If one input's setup component is defined by this runtime resource overlay (RRO),
+ the LiveTv will use the defined component to set up the input,
+ instead of the setup Activity defined in the TvInputService apk.-->
+ <string-array translatable="false" name="setup_ComponentNames">
+ <!-- Example:
+ <item>"input_1#com.example.setup1/.SetupActivity1"</item>
+ <item>"input_2#com.example.setup1/com.example.setup1.SetupActivity2"</item>
+ <item>"input_3#com.example.setup2/com.example2.SetupActivity"</item>
+ -->
+ </string-array>
</resources>
diff --git a/res/values/colors.xml b/res/values/colors.xml
index b68feb13..e372c150 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -111,6 +111,11 @@
<color name="tvview_block_text_color">#80EEEEEE</color>
<color name="tvview_block_image_color_filter">#99000000</color>
+ <!-- Empty input status -->
+ <color name="empty_input_status_icon_tint_color">#FF445664</color>
+ <color name="empty_input_status_title_text_color">#FFE8F0FE</color>
+ <color name="empty_input_status_info_text_color">#99D2E3FC</color>
+
<!-- Channel banner -->
<color name="channel_banner_text_color">#FFEEEEEE</color>
<color name="channel_banner_episode_text_color">#B3EEEEEE</color>
@@ -160,4 +165,8 @@
<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>
+
+ <!-- TV IAPP dialog -->
+ <color name="tv_iapp_dialog_background">#384248</color>
+ <color name="tv_iapp_dialog_text_color">#C0EEEEEE</color>
</resources>
diff --git a/res/values/configs.xml b/res/values/configs.xml
new file mode 100644
index 00000000..15481b03
--- /dev/null
+++ b/res/values/configs.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <bool name="use_gtv_livetv_v2">false</bool>
+</resources>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 9d8941fa..8b1e9e25 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -248,6 +248,15 @@
<dimen name="shrunken_tvview_margin_start">56dp</dimen>
<dimen name="shrunken_tvview_margin_end">32dp</dimen>
+ <!-- Empty input status -->
+ <dimen name="empty_input_status_block_title_text_size">28sp</dimen>
+ <dimen name="empty_input_status_block_info_text_size">16sp</dimen>
+ <dimen name="empty_input_status_block_icon_width">170dp</dimen>
+ <dimen name="empty_input_status_block_icon_height">170dp</dimen>
+ <dimen name="empty_input_status_block_icon_margin_top">100dp</dimen>
+ <dimen name="empty_input_status_block_title_margin_top">44dp</dimen>
+ <dimen name="empty_input_status_block_info_margin_top">16dp</dimen>
+
<!-- Channel banner -->
<dimen name="channel_banner_width">696dp</dimen>
<dimen name="channel_banner_channel_number_large_text_size">54sp</dimen>
diff --git a/res/values/strings-custom.xml b/res/values/strings-custom.xml
index 5ecb8592..0055937e 100644
--- a/res/values/strings-custom.xml
+++ b/res/values/strings-custom.xml
@@ -27,5 +27,6 @@
meaning="Live TV version of setup_sources_description2"
><xliff:g id="app_name">Live TV</xliff:g> combines the experience of traditional TV channels with streaming channels provided by apps.
\n\nGet started by setting up the channel sources already installed. Or browse Google Play Store for more apps that offer live channels.</string>
-
+ <!-- Name of SelectInputActivity [CHAR LIMIT=NONE] -->
+ <string name="select_inputs">Inputs</string>
</resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index e272244d..a8393519 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -420,6 +420,20 @@
\n\nPress Right to adjust recording schedule.</item>
</plurals>
+ <!-- Empty input status -->
+
+ <!-- The text used when external input source is weak signal or not connected [CHAR LIMIT=NONE] -->
+ <string name="empty_input_status_title_format"><xliff:g id="input_label" example="HDMI 1">%1$s</xliff:g> - No signal</string>
+ <!-- The text used to indicate the action when external input source is weak signal or not connected [CHAR LIMIT=NONE] -->
+ <string name="empty_input_status_info">Check the connection or select a different input source</string>
+
+ <!-- Input Banner -->
+ <eat-comment />
+ <!-- The text used to show in the input banner [CHAR LIMIT=NONE] -->
+ <string name="input_banner_v2_active_input">Active input</string>
+ <!-- The text used when switching to a new input source. [CHAR LIMIT=NONE] -->
+ <string name="input_banner_v2_input_label_format"><xliff:g example="HDMI 1" id="label">%1$s</xliff:g> · <xliff:g example="Play Station" id="custom_label">%2$s</xliff:g></string>
+
<!-- Channel Banner -->
<eat-comment />
<!-- The text used when there is no program title. [CHAR LIMIT=NONE] -->
@@ -1044,4 +1058,12 @@
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>
+
+ <!-- Interactive Application Dialog-->
+ <string name="tv_app_dialog_title">An interactive app was found. Do you want to turn on interactive apps?</string>
+
+ <!-- Interactive Application Setting -->
+ <string name="interactive_app_settings">Interactive app settings</string>
+ <string name="tv_iapp_on">On</string>
+ <string name="tv_iapp_off">Off</string>
</resources>
diff --git a/src/com/android/tv/ChannelTuner.java b/src/com/android/tv/ChannelTuner.java
index fe138980..351f0010 100644
--- a/src/com/android/tv/ChannelTuner.java
+++ b/src/com/android/tv/ChannelTuner.java
@@ -36,7 +36,7 @@ import java.util.Map;
import java.util.Set;
/**
- * It manages the current tuned channel among browsable channels. And it determines the next channel
+ * Manages the current tuned channel among browsable channels, and determines the next channel
* by channel up/down. But, it doesn't actually tune through TvView.
*/
@MainThread
diff --git a/src/com/android/tv/InputSessionManager.java b/src/com/android/tv/InputSessionManager.java
index ea17751b..57fc883e 100644
--- a/src/com/android/tv/InputSessionManager.java
+++ b/src/com/android/tv/InputSessionManager.java
@@ -18,6 +18,7 @@ package com.android.tv;
import android.annotation.TargetApi;
import android.content.Context;
+import android.media.tv.AitInfo;
import android.media.tv.TvContentRating;
import android.media.tv.TvInputInfo;
import android.media.tv.TvTrackInfo;
@@ -582,6 +583,12 @@ public class InputSessionManager {
public void onSignalStrength(String inputId, int value) {
mDelegate.onSignalStrength(inputId, value);
}
+
+ @TargetApi(Build.VERSION_CODES.TIRAMISU)
+ @Override
+ public void onAitInfoUpdated(String inputId, AitInfo aitInfo) {
+ mDelegate.onAitInfoUpdated(inputId, aitInfo);
+ }
}
/** 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 374f9f7a..5b359483 100644
--- a/src/com/android/tv/MainActivity.java
+++ b/src/com/android/tv/MainActivity.java
@@ -18,6 +18,7 @@ package com.android.tv;
import static com.android.tv.common.feature.SystemAppFeature.SYSTEM_APP_FEATURE;
+import android.annotation.TargetApi;
import android.app.Activity;
import android.app.PendingIntent;
import android.app.SearchManager;
@@ -32,6 +33,7 @@ import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.database.Cursor;
import android.hardware.display.DisplayManager;
+import android.media.tv.AitInfo;
import android.media.tv.TvContentRating;
import android.media.tv.TvContract;
import android.media.tv.TvContract.Channels;
@@ -40,6 +42,8 @@ import android.media.tv.TvInputManager;
import android.media.tv.TvInputManager.TvInputCallback;
import android.media.tv.TvTrackInfo;
import android.media.tv.TvView.OnUnhandledInputEventListener;
+import android.media.tv.interactive.TvInteractiveAppManager;
+import android.media.tv.interactive.TvInteractiveAppView;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
@@ -105,6 +109,8 @@ import com.android.tv.dialog.HalfSizedDialogFragment;
import com.android.tv.dialog.PinDialogFragment;
import com.android.tv.dialog.PinDialogFragment.OnPinCheckedListener;
import com.android.tv.dialog.SafeDismissDialogFragment;
+import com.android.tv.dialog.InteractiveAppDialogFragment;
+import com.android.tv.dialog.InteractiveAppDialogFragment.OnInteractiveAppCheckedListener;
import com.android.tv.dvr.DvrManager;
import com.android.tv.dvr.data.ScheduledRecording;
import com.android.tv.dvr.recorder.ConflictChecker;
@@ -115,6 +121,7 @@ import com.android.tv.dvr.ui.DvrStopRecordingFragment;
import com.android.tv.dvr.ui.DvrUiHelper;
import com.android.tv.features.TvFeatures;
import com.android.tv.guide.ProgramItemView;
+import com.android.tv.interactive.IAppManager;
import com.android.tv.menu.Menu;
import com.android.tv.onboarding.OnboardingActivity;
import com.android.tv.parental.ContentRatingsManager;
@@ -127,7 +134,7 @@ 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.InputBannerViewBase;
import com.android.tv.ui.KeypadChannelSwitchView;
import com.android.tv.ui.SelectInputView;
import com.android.tv.ui.SelectInputView.OnInputSelectedCallback;
@@ -193,7 +200,8 @@ public class MainActivity extends Activity
OnPinCheckedListener,
ChannelChanger,
HasSingletons<MySingletons>,
- HasAndroidInjector {
+ HasAndroidInjector,
+ OnInteractiveAppCheckedListener {
private static final String TAG = "MainActivity";
private static final boolean DEBUG = false;
private AudioCapabilitiesReceiver mAudioCapabilitiesReceiver;
@@ -254,6 +262,9 @@ public class MainActivity extends Activity
SYSTEM_INTENT_FILTER.addAction(Intent.ACTION_SCREEN_OFF);
SYSTEM_INTENT_FILTER.addAction(Intent.ACTION_SCREEN_ON);
SYSTEM_INTENT_FILTER.addAction(Intent.ACTION_TIME_CHANGED);
+ if (Build.VERSION.SDK_INT > 33) { // TIRAMISU
+ SYSTEM_INTENT_FILTER.addAction(TvInteractiveAppManager.ACTION_APP_LINK_COMMAND);
+ }
}
private static final int REQUEST_CODE_START_SETUP_ACTIVITY = 1;
@@ -365,6 +376,8 @@ public class MainActivity extends Activity
private String mLastInputIdFromIntent;
+ private IAppManager mIAppManager;
+
private final Handler mHandler = new MainActivityHandler(this);
private final Set<OnActionClickListener> mOnActionClickListeners = new ArraySet<>();
@@ -406,6 +419,13 @@ public class MainActivity extends Activity
tune(true);
}
break;
+ case TvInteractiveAppManager.ACTION_APP_LINK_COMMAND:
+ if (DEBUG) {
+ Log.d(TAG, "Received action link command");
+ }
+ // TODO: handle the command
+ break;
+
default: // fall out
}
}
@@ -435,7 +455,7 @@ public class MainActivity extends Activity
public void onLoadFinished() {
Debug.getTimer(Debug.TAG_START_UP_TIMER)
.log("MainActivity.mChannelTunerListener.onLoadFinished");
- mSetupUtils.markNewChannelsBrowsable();
+ mSetupUtils.markNewChannelsBrowsableIfEnabled();
if (mActivityResumed) {
resumeTvIfNeeded();
}
@@ -545,8 +565,10 @@ public class MainActivity extends Activity
return;
}
setContentView(R.layout.activity_tv);
+ TvInteractiveAppView tvInteractiveAppView = findViewById(R.id.tv_app_view);
mTvView = findViewById(R.id.main_tunable_tv_view);
- mTvView.initialize(mProgramDataManager, mTvInputManagerHelper, mLegacyFlags);
+ mTvView.initialize(
+ mProgramDataManager, mTvInputManagerHelper, mLegacyFlags, tvInteractiveAppView);
mTvView.setOnUnhandledInputEventListener(
new OnUnhandledInputEventListener() {
@Override
@@ -655,9 +677,13 @@ public class MainActivity extends Activity
(KeypadChannelSwitchView)
getLayoutInflater()
.inflate(R.layout.keypad_channel_switch, sceneContainer, false);
- InputBannerView inputBannerView =
- (InputBannerView)
- getLayoutInflater().inflate(R.layout.input_banner, sceneContainer, false);
+
+
+ boolean useV2 = TvFeatures.USE_GTV_LIVETV_V2.isEnabled(this);
+ int inputBannerLayoutId = useV2 ? R.layout.input_banner_v2 : R.layout.input_banner;
+ InputBannerViewBase inputBannerView =
+ (InputBannerViewBase)
+ getLayoutInflater().inflate(inputBannerLayoutId, sceneContainer, false);
SelectInputView selectInputView =
(SelectInputView)
getLayoutInflater().inflate(R.layout.select_input, sceneContainer, false);
@@ -732,9 +758,21 @@ public class MainActivity extends Activity
mDvrConflictChecker = new ConflictChecker(this);
}
initForTest();
+ if (TvFeatures.HAS_TIAF.isEnabled(this)) {
+ mIAppManager = new IAppManager(this, mTvView, mHandler);
+ }
Debug.getTimer(Debug.TAG_START_UP_TIMER).log("MainActivity.onCreate end");
}
+ @TargetApi(Build.VERSION_CODES.TIRAMISU)
+ @Override
+ public void onInteractiveAppChecked(boolean checked) {
+ TvSettings.setTvIAppOn(getApplicationContext(), checked);
+ if (checked) {
+ mIAppManager.processHeldAitInfo();
+ }
+ }
+
private void startOnboardingActivity() {
startActivity(OnboardingActivity.buildIntent(this, getIntent()));
finish();
@@ -833,7 +871,7 @@ public class MainActivity extends Activity
mMainDurationTimer.start();
applyParentalControlSettings();
- registerReceiver(mBroadcastReceiver, SYSTEM_INTENT_FILTER);
+ registerReceiver(mBroadcastReceiver, SYSTEM_INTENT_FILTER, Context.RECEIVER_EXPORTED);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
Intent notificationIntent = new Intent(this, NotificationService.class);
@@ -888,7 +926,7 @@ public class MainActivity extends Activity
}
if (mChannelTuner.areAllChannelsLoaded()) {
- mSetupUtils.markNewChannelsBrowsable();
+ mSetupUtils.markNewChannelsBrowsableIfEnabled();
resumeTvIfNeeded();
}
mOverlayManager.showMenuWithTimeShiftPauseIfNeeded();
@@ -1060,7 +1098,10 @@ public class MainActivity extends Activity
}
if (channelUri == null) {
- mChannelTuner.moveToChannel(mChannelTuner.findNearestBrowsableChannel(0));
+ if (!mChannelTuner.moveToChannel(mChannelTuner.findNearestBrowsableChannel(0))) {
+ Log.w(TAG, "No browsable channel, show setup");
+ showSettingsFragment();
+ }
} else {
if (TvContract.isChannelUriForPassthroughInput(channelUri)) {
ChannelImpl channel = ChannelImpl.createPassthroughChannel(channelUri);
@@ -1069,19 +1110,23 @@ public class MainActivity extends Activity
long channelId = ContentUris.parseId(channelUri);
Channel channel = mChannelDataManager.getChannel(channelId);
if (channel == null || !mChannelTuner.moveToChannel(channel)) {
- mChannelTuner.moveToChannel(mChannelTuner.findNearestBrowsableChannel(0));
Log.w(
TAG,
"The requested channel (id="
+ channelId
+ ") doesn't exist. "
+ "The first channel will be tuned to.");
+ if (!mChannelTuner.moveToChannel(
+ mChannelTuner.findNearestBrowsableChannel(0))) {
+ Log.w(TAG, "No browsable channel, show setup");
+ showSettingsFragment();
+ }
}
}
}
mTvView.start();
- mAudioManagerHelper.setVolumeByAudioFocusStatus();
+ mAudioManagerHelper.requestAudioFocus();
tune(true);
}
@@ -1126,6 +1171,9 @@ public class MainActivity extends Activity
private void stopAll(boolean keepVisibleBehind) {
mOverlayManager.hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_WITHOUT_ANIMATION);
stopTv("stopAll()", keepVisibleBehind);
+ if (mIAppManager != null) {
+ mIAppManager.stop();
+ }
}
public TvInputManagerHelper getTvInputManagerHelper() {
@@ -1138,7 +1186,7 @@ public class MainActivity extends Activity
* @param calledByPopup If true, startSetupActivity is invoked from the setup fragment.
*/
public void startSetupActivity(TvInputInfo input, boolean calledByPopup) {
- Intent intent = CommonUtils.createSetupIntent(input);
+ Intent intent = mSetupUtils.createSetupIntent(this, input);
if (intent == null) {
Toast.makeText(this, R.string.msg_no_setup_activity, Toast.LENGTH_SHORT).show();
return;
@@ -1425,6 +1473,9 @@ public class MainActivity extends Activity
if (DeveloperPreferences.LOG_KEYEVENT.get(this)) {
Log.d(TAG, "dispatchKeyEvent(" + event + ")");
}
+ if (mIAppManager != null && mIAppManager.dispatchKeyEvent(event)) {
+ return true;
+ }
// If an activity is closed on a back key down event, back key down events with none zero
// repeat count or a back key up event can be happened without the first back key down
// event which should be ignored in this activity.
@@ -1631,7 +1682,7 @@ public class MainActivity extends Activity
}
}
- private void stopTv() {
+ public void stopTv() {
stopTv(null, false);
}
@@ -1932,12 +1983,21 @@ public class MainActivity extends Activity
@VisibleForTesting
protected void applyMultiAudio(String trackId) {
+ applyMultiAudio(false, trackId);
+ }
+
+ @VisibleForTesting
+ protected void applyMultiAudio(boolean allowAutoSelection, String trackId) {
+ if (!allowAutoSelection && trackId == null) {
+ selectTrack(TvTrackInfo.TYPE_AUDIO, null, UNDEFINED_TRACK_INDEX);
+ mTvOptionsManager.onMultiAudioChanged(null);
+ return;
+ }
List<TvTrackInfo> tracks = getTracks(TvTrackInfo.TYPE_AUDIO);
if (tracks == null) {
mTvOptionsManager.onMultiAudioChanged(null);
return;
}
-
TvTrackInfo bestTrack = null;
if (trackId != null) {
for (TvTrackInfo track : tracks) {
@@ -2459,7 +2519,7 @@ public class MainActivity extends Activity
return handled;
}
- private boolean isKeyEventBlocked() {
+ public boolean isKeyEventBlocked() {
// If the current channel is a passthrough channel, we don't handle the key events in TV
// activity. Instead, the key event will be handled by the passthrough TV input.
return mChannelTuner.isCurrentChannelPassthrough();
@@ -2907,10 +2967,11 @@ public class MainActivity extends Activity
}
applyDisplayRefreshRate(info.getVideoFrameRate());
mTvViewUiManager.updateTvAspectRatio();
- applyMultiAudio(
+ applyMultiAudio(allowAutoSelectionOfTrack,
allowAutoSelectionOfTrack ? null : getSelectedTrack(TvTrackInfo.TYPE_AUDIO));
applyClosedCaption();
mOverlayManager.getMenu().onStreamInfoChanged();
+ mOverlayManager.updateInputBannerIfNeeded(info);
if (mTvView.isVideoAvailable()) {
mTvViewUiManager.fadeInTvView();
}
@@ -2989,6 +3050,14 @@ public class MainActivity extends Activity
TvOverlayManager.UPDATE_CHANNEL_BANNER_REASON_UPDATE_SIGNAL_STRENGTH);
}
}
+
+ @TargetApi(Build.VERSION_CODES.TIRAMISU)
+ @Override
+ public void onAitInfoUpdated(String inputId, AitInfo aitInfo) {
+ if (mIAppManager != null) {
+ mIAppManager.onAitInfoUpdated(aitInfo);
+ }
+ }
}
private class MySingletonsImpl implements MySingletons {
@@ -3047,5 +3116,8 @@ public class MainActivity extends Activity
@ContributesAndroidInjector
abstract DvrScheduleFragment contributesDvrScheduleFragment();
+
+ @ContributesAndroidInjector
+ abstract InteractiveAppDialogFragment contributesInteractiveAppDialogFragment();
}
}
diff --git a/src/com/android/tv/SetupPassthroughActivity.java b/src/com/android/tv/SetupPassthroughActivity.java
index e7f89108..2a4a556f 100644
--- a/src/com/android/tv/SetupPassthroughActivity.java
+++ b/src/com/android/tv/SetupPassthroughActivity.java
@@ -109,7 +109,6 @@ public class SetupPassthroughActivity extends Activity {
finish();
return;
}
- SetupUtils.grantEpgPermission(this, mTvInputInfo.getServiceInfo().packageName);
if (DEBUG) Log.d(TAG, "Activity after completion " + mActivityAfterCompletion);
// If EXTRA_SETUP_INTENT is not removed, an infinite recursion happens during
// setupIntent.putExtras(intent.getExtras()).
@@ -127,6 +126,7 @@ public class SetupPassthroughActivity extends Activity {
finish();
return;
}
+ SetupUtils.grantEpgPermission(this, mTvInputInfo.getServiceInfo().packageName);
startActivityForResult(setupIntent, REQUEST_START_SETUP_ACTIVITY);
} catch (ActivityNotFoundException e) {
Log.e(TAG, "Can't find activity: " + setupIntent.getComponent());
diff --git a/src/com/android/tv/TvApplication.java b/src/com/android/tv/TvApplication.java
index 5ab8f033..738759fc 100644
--- a/src/com/android/tv/TvApplication.java
+++ b/src/com/android/tv/TvApplication.java
@@ -255,7 +255,9 @@ public abstract class TvApplication extends BaseApplication implements TvSinglet
@Override
@Nullable
public DvrManager getDvrManager() {
- return (CommonFeatures.DVR.isEnabled(this)) ? mDvrManager.get() : null;
+ return (CommonFeatures.DVR.isEnabled(this) && mDvrScheduleManager != null)
+ ? mDvrManager.get()
+ : null;
}
/** Returns the {@link DvrScheduleManager}. */
@@ -407,9 +409,17 @@ public abstract class TvApplication extends BaseApplication implements TvSinglet
++inputCount;
}
}
- if (inputCount < 2) {
+
+ if (inputCount < 1) {
+ Log.w(TAG, "No available input to be selected.");
return;
}
+
+ if (mMainActivityWrapper.isResumed() && inputCount < 2) {
+ // if no other inputs can be switched to, keep in the same input
+ return;
+ }
+
Activity activityToHandle =
mMainActivityWrapper.isResumed()
? mMainActivityWrapper.getMainActivity()
diff --git a/src/com/android/tv/audiotvservice/AudioOnlyTvService.java b/src/com/android/tv/audiotvservice/AudioOnlyTvService.java
index 5d0e9c82..59e2406f 100644
--- a/src/com/android/tv/audiotvservice/AudioOnlyTvService.java
+++ b/src/com/android/tv/audiotvservice/AudioOnlyTvService.java
@@ -15,11 +15,14 @@
*/
package com.android.tv.audiotvservice;
+import android.annotation.TargetApi;
import android.app.Notification;
import android.app.Service;
import android.content.Intent;
import android.media.session.MediaSession;
+import android.media.tv.AitInfo;
import android.net.Uri;
+import android.os.Build;
import android.os.IBinder;
import android.support.annotation.Nullable;
import android.util.Log;
@@ -99,4 +102,8 @@ public class AudioOnlyTvService extends Service implements OnTuneListener {
@Override
public void onChannelSignalStrength() {}
+
+ @TargetApi(Build.VERSION_CODES.TIRAMISU)
+ @Override
+ public void onAitInfoUpdated(String inputId, AitInfo aitInfo) {}
}
diff --git a/src/com/android/tv/data/ChannelDataManager.java b/src/com/android/tv/data/ChannelDataManager.java
index 67c32309..fb7ab02a 100644
--- a/src/com/android/tv/data/ChannelDataManager.java
+++ b/src/com/android/tv/data/ChannelDataManager.java
@@ -301,6 +301,22 @@ public class ChannelDataManager {
}
/**
+ * Returns the total browsable channel count for a given input.
+ *
+ * @param inputId The ID of the input.
+ */
+ public int getBrowsableChannelCountForInput(String inputId) {
+ int count = 0;
+ for (Channel channel : mData.channels) {
+ if (channel.getInputId().equals(inputId) && channel.isBrowsable()) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+
+ /**
* Checks if the channel exists in DB.
*
* <p>Note that the channels of the removed inputs can not be obtained from {@link #getChannel}.
diff --git a/src/com/android/tv/data/ChannelImpl.java b/src/com/android/tv/data/ChannelImpl.java
index f31290d0..5be1179d 100644
--- a/src/com/android/tv/data/ChannelImpl.java
+++ b/src/com/android/tv/data/ChannelImpl.java
@@ -18,6 +18,7 @@ package com.android.tv.data;
import android.content.Context;
import android.content.Intent;
+import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.media.tv.TvContract;
@@ -673,7 +674,18 @@ public final class ChannelImpl implements Channel {
if (!TextUtils.isEmpty(mAppLinkText) && !TextUtils.isEmpty(mAppLinkIntentUri)) {
try {
Intent intent = Intent.parseUri(mAppLinkIntentUri, Intent.URI_INTENT_SCHEME);
- if (intent.resolveActivityInfo(pm, 0) != null) {
+ ActivityInfo activityInfo = intent.resolveActivityInfo(pm, 0);
+ if (activityInfo != null) {
+ String packageName = activityInfo.packageName;
+ // Prevent creation of App Links to private activities in this package
+ boolean isProtectedActivity = packageName != null
+ && (packageName.equals(CommonConstants.BASE_PACKAGE)
+ || packageName.startsWith(CommonConstants.BASE_PACKAGE + "."));
+ if (isProtectedActivity) {
+ Log.w(TAG,"Attempt to add app link to protected activity: "
+ + mAppLinkIntentUri);
+ return;
+ }
mAppLinkIntent = intent;
mAppLinkIntent.putExtra(
CommonConstants.EXTRA_APP_LINK_CHANNEL_URI, getUri().toString());
diff --git a/src/com/android/tv/data/StreamInfo.java b/src/com/android/tv/data/StreamInfo.java
index e4237bf4..f323423c 100644
--- a/src/com/android/tv/data/StreamInfo.java
+++ b/src/com/android/tv/data/StreamInfo.java
@@ -44,6 +44,8 @@ public interface StreamInfo {
int getAudioChannelCount();
+ float getStreamVolume();
+
boolean hasClosedCaption();
boolean isVideoAvailable();
diff --git a/src/com/android/tv/dialog/InteractiveAppDialogFragment.java b/src/com/android/tv/dialog/InteractiveAppDialogFragment.java
new file mode 100755
index 00000000..c5ffbaac
--- /dev/null
+++ b/src/com/android/tv/dialog/InteractiveAppDialogFragment.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.dialog;
+
+import android.annotation.TargetApi;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.os.Build;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+import android.widget.Button;
+import android.widget.TextView;
+import com.android.tv.R;
+import com.android.tv.common.SoftPreconditions;
+
+import java.util.function.Function;
+
+import dagger.android.AndroidInjection;
+
+@TargetApi(Build.VERSION_CODES.TIRAMISU)
+public class InteractiveAppDialogFragment extends SafeDismissDialogFragment {
+ private static final boolean DEBUG = false;
+
+ public static final String DIALOG_TAG = InteractiveAppDialogFragment.class.getName();
+ private static final String TRACKER_LABEL = "Interactive App Dialog";
+ private static final String TV_IAPP_NAME = "tv_iapp_name";
+ private boolean mIsChoseOK;
+ private String mIAppName;
+ private Function mUpdateAitInfo;
+
+ public static InteractiveAppDialogFragment create(String iappName) {
+ InteractiveAppDialogFragment fragment = new InteractiveAppDialogFragment();
+ Bundle args = new Bundle();
+ args.putString(TV_IAPP_NAME, iappName);
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ @Override
+ public void onAttach(Context context) {
+ AndroidInjection.inject(this);
+ super.onAttach(context);
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mIAppName = getArguments().getString(TV_IAPP_NAME);
+ setStyle(STYLE_NO_TITLE, 0);
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ Dialog dlg = super.onCreateDialog(savedInstanceState);
+ dlg.getWindow().getAttributes().windowAnimations = R.style.pin_dialog_animation;
+ mIsChoseOK = false;
+ return dlg;
+ }
+
+ @Override
+ public String getTrackerLabel() {
+ return TRACKER_LABEL;
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ // Dialog size is determined by its windows size, not inflated view size.
+ // So apply view size to window after the DialogFragment.onStart() where dialog is shown.
+ Dialog dlg = getDialog();
+ if (dlg != null) {
+ dlg.getWindow()
+ .setLayout(
+ getResources().getDimensionPixelSize(R.dimen.pin_dialog_width),
+ LayoutParams.WRAP_CONTENT);
+ }
+ }
+
+ @Override
+ public View onCreateView(
+ LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ final View v = inflater.inflate(R.layout.tv_app_dialog, container, false);
+ TextView mTitleView = (TextView) v.findViewById(R.id.title);
+ mTitleView.setText(getString(R.string.tv_app_dialog_title, mIAppName));
+ Button okButton = v.findViewById(R.id.ok);
+ okButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ exit(true);
+ }
+ });
+ Button cancelButton = v.findViewById(R.id.cancel);
+ cancelButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ exit(false);
+ }
+ });
+ return v;
+ }
+
+ private void exit(boolean isokclick) {
+ mIsChoseOK = isokclick;
+ dismiss();
+ }
+
+ @Override
+ public void onDismiss(DialogInterface dialog) {
+ super.onDismiss(dialog);
+ SoftPreconditions.checkState(getActivity() instanceof OnInteractiveAppCheckedListener);
+ if (getActivity() instanceof OnInteractiveAppCheckedListener) {
+ ((OnInteractiveAppCheckedListener) getActivity())
+ .onInteractiveAppChecked(mIsChoseOK);
+ }
+ }
+
+ public interface OnInteractiveAppCheckedListener {
+ void onInteractiveAppChecked(boolean checked);
+ }
+}
diff --git a/src/com/android/tv/features/TvFeatures.java b/src/com/android/tv/features/TvFeatures.java
index 5282c28c..c4e388a3 100644
--- a/src/com/android/tv/features/TvFeatures.java
+++ b/src/com/android/tv/features/TvFeatures.java
@@ -28,10 +28,13 @@ import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Build;
+import androidx.core.R;
+
import com.android.tv.common.feature.CommonFeatures;
import com.android.tv.common.feature.Feature;
import com.android.tv.common.feature.FeatureUtils;
import com.android.tv.common.feature.FlagFeature;
+import com.android.tv.common.feature.ResourceConfigFeature;
import com.android.tv.common.feature.Sdk;
import com.android.tv.common.feature.TestableFeature;
import com.android.tv.common.flags.has.HasUiFlags;
@@ -101,5 +104,12 @@ public final class TvFeatures extends CommonFeatures {
/** Use input blocklist to disable partner's tuner input. */
public static final Feature USE_PARTNER_INPUT_BLOCKLIST = ON;
+ /** Support for interactive applications using the TIAF **/
+ public static final Feature HAS_TIAF = Sdk.AT_LEAST_T;
+
+ /** Use new components that are consistent with other Google TV styles b/302992748 */
+ public static final Feature USE_GTV_LIVETV_V2 =
+ new ResourceConfigFeature(R.bool.use_gtv_livetv_v2, false);
+
private TvFeatures() {}
}
diff --git a/src/com/android/tv/interactive/IAppManager.java b/src/com/android/tv/interactive/IAppManager.java
new file mode 100644
index 00000000..682b35c6
--- /dev/null
+++ b/src/com/android/tv/interactive/IAppManager.java
@@ -0,0 +1,428 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.interactive;
+
+import static com.android.tv.util.CaptionSettings.OPTION_OFF;
+import static com.android.tv.util.CaptionSettings.OPTION_ON;
+
+import android.annotation.TargetApi;
+import android.graphics.Rect;
+import android.media.tv.TvTrackInfo;
+import android.media.tv.interactive.TvInteractiveAppManager;
+import android.media.tv.AitInfo;
+import android.media.tv.interactive.TvInteractiveAppService;
+import android.media.tv.interactive.TvInteractiveAppServiceInfo;
+import android.media.tv.interactive.TvInteractiveAppView;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.annotation.NonNull;
+import android.util.Log;
+import android.view.InputEvent;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.tv.MainActivity;
+import com.android.tv.R;
+import com.android.tv.common.SoftPreconditions;
+import com.android.tv.common.util.ContentUriUtils;
+import com.android.tv.data.api.Channel;
+import com.android.tv.dialog.InteractiveAppDialogFragment;
+import com.android.tv.features.TvFeatures;
+import com.android.tv.ui.TunableTvView;
+import com.android.tv.util.TvSettings;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+@TargetApi(Build.VERSION_CODES.TIRAMISU)
+public class IAppManager {
+ private static final String TAG = "IAppManager";
+ private static final boolean DEBUG = false;
+
+ private final MainActivity mMainActivity;
+ private final TvInteractiveAppManager mTvIAppManager;
+ private final TvInteractiveAppView mTvIAppView;
+ private final TunableTvView mTvView;
+ private final Handler mHandler;
+ private AitInfo mCurrentAitInfo;
+ private AitInfo mHeldAitInfo; // AIT info that has been held pending dialog confirmation
+ private boolean mTvAppDialogShown = false;
+
+ public IAppManager(@NonNull MainActivity parentActivity, @NonNull TunableTvView tvView,
+ @NonNull Handler handler) {
+ SoftPreconditions.checkFeatureEnabled(parentActivity, TvFeatures.HAS_TIAF, TAG);
+
+ mMainActivity = parentActivity;
+ mTvView = tvView;
+ mHandler = handler;
+ mTvIAppManager = mMainActivity.getSystemService(TvInteractiveAppManager.class);
+ mTvIAppView = mMainActivity.findViewById(R.id.tv_app_view);
+ if (mTvIAppManager == null || mTvIAppView == null) {
+ Log.e(TAG, "Could not find interactive app view or manager");
+ return;
+ }
+
+ ExecutorService executor = Executors.newSingleThreadExecutor();
+ mTvIAppManager.registerCallback(
+ executor,
+ new MyInteractiveAppManagerCallback()
+ );
+ mTvIAppView.setCallback(
+ executor,
+ new MyInteractiveAppViewCallback()
+ );
+ mTvIAppView.setOnUnhandledInputEventListener(executor,
+ inputEvent -> {
+ if (mMainActivity.isKeyEventBlocked()) {
+ return true;
+ }
+ if (inputEvent instanceof KeyEvent) {
+ KeyEvent keyEvent = (KeyEvent) inputEvent;
+ if (keyEvent.getAction() == KeyEvent.ACTION_DOWN
+ && keyEvent.isLongPress()) {
+ if (mMainActivity.onKeyLongPress(keyEvent.getKeyCode(), keyEvent)) {
+ return true;
+ }
+ }
+ if (keyEvent.getAction() == KeyEvent.ACTION_UP) {
+ return mMainActivity.onKeyUp(keyEvent.getKeyCode(), keyEvent);
+ } else if (keyEvent.getAction() == KeyEvent.ACTION_DOWN) {
+ return mMainActivity.onKeyDown(keyEvent.getKeyCode(), keyEvent);
+ }
+ }
+ return false;
+ });
+ }
+
+ public void stop() {
+ mTvIAppView.stopInteractiveApp();
+ mTvIAppView.reset();
+ mCurrentAitInfo = null;
+ }
+
+ /*
+ * Update current info based on ait info that was held when the dialog was shown.
+ */
+ public void processHeldAitInfo() {
+ if (mHeldAitInfo != null) {
+ onAitInfoUpdated(mHeldAitInfo);
+ }
+ }
+
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ if (mTvIAppView != null && mTvIAppView.getVisibility() == View.VISIBLE
+ && mTvIAppView.dispatchKeyEvent(event)){
+ return true;
+ }
+ return false;
+ }
+
+ public void onAitInfoUpdated(AitInfo aitInfo) {
+ if (mTvIAppManager == null || aitInfo == null) {
+ return;
+ }
+ if (mCurrentAitInfo != null && mCurrentAitInfo.getType() == aitInfo.getType()) {
+ if (DEBUG) {
+ Log.d(TAG, "Ignoring AIT update: Same type as current");
+ }
+ return;
+ }
+
+ List<TvInteractiveAppServiceInfo> tvIAppInfoList =
+ mTvIAppManager.getTvInteractiveAppServiceList();
+ if (tvIAppInfoList.isEmpty()) {
+ if (DEBUG) {
+ Log.d(TAG, "Ignoring AIT update: No interactive app services registered");
+ }
+ return;
+ }
+
+ // App Type ID numbers allocated by DVB Services
+ int type = -1;
+ switch (aitInfo.getType()) {
+ case 0x0010: // HBBTV
+ type = TvInteractiveAppServiceInfo.INTERACTIVE_APP_TYPE_HBBTV;
+ break;
+ case 0x0006: // DCAP-J: DCAP Java applications
+ case 0x0007: // DCAP-X: DCAP XHTML applications
+ type = TvInteractiveAppServiceInfo.INTERACTIVE_APP_TYPE_ATSC;
+ break;
+ case 0x0001: // Ginga-J
+ case 0x0009: // Ginga-NCL
+ case 0x000b: // Ginga-HTML5
+ type = TvInteractiveAppServiceInfo.INTERACTIVE_APP_TYPE_GINGA;
+ break;
+ default:
+ Log.e(TAG, "AIT info contained unknown type: " + aitInfo.getType());
+ return;
+ }
+
+ if (TvSettings.isTvIAppOn(mMainActivity.getApplicationContext())) {
+ mTvAppDialogShown = false;
+ for (TvInteractiveAppServiceInfo info : tvIAppInfoList) {
+ if ((info.getSupportedTypes() & type) > 0) {
+ mCurrentAitInfo = aitInfo;
+ if (mTvIAppView != null) {
+ mTvIAppView.setVisibility(View.VISIBLE);
+ mTvIAppView.prepareInteractiveApp(info.getId(), type);
+ }
+ break;
+ }
+ }
+ } else if (!mTvAppDialogShown) {
+ if (DEBUG) {
+ Log.d(TAG, "TV IApp is not enabled");
+ }
+
+ for (TvInteractiveAppServiceInfo info : tvIAppInfoList) {
+ if ((info.getSupportedTypes() & type) > 0) {
+ mMainActivity.getOverlayManager().showDialogFragment(
+ InteractiveAppDialogFragment.DIALOG_TAG,
+ InteractiveAppDialogFragment.create(info.getServiceInfo().packageName),
+ false);
+ mHeldAitInfo = aitInfo;
+ mTvAppDialogShown = true;
+ break;
+ }
+ }
+ }
+ }
+
+ private class MyInteractiveAppManagerCallback extends
+ TvInteractiveAppManager.TvInteractiveAppCallback {
+ @Override
+ public void onInteractiveAppServiceAdded(String iAppServiceId) {}
+
+ @Override
+ public void onInteractiveAppServiceRemoved(String iAppServiceId) {}
+
+ @Override
+ public void onInteractiveAppServiceUpdated(String iAppServiceId) {}
+
+ @Override
+ public void onTvInteractiveAppServiceStateChanged(String iAppServiceId, int type, int state,
+ int err) {
+ if (state == TvInteractiveAppManager.SERVICE_STATE_READY && mTvIAppView != null) {
+ mTvIAppView.startInteractiveApp();
+ mTvIAppView.setTvView(mTvView.getTvView());
+ if (mTvView.getTvView() != null) {
+ mTvView.getTvView().setInteractiveAppNotificationEnabled(true);
+ }
+ }
+ }
+ }
+
+ private class MyInteractiveAppViewCallback extends
+ TvInteractiveAppView.TvInteractiveAppCallback {
+ @Override
+ public void onPlaybackCommandRequest(String iAppServiceId, String cmdType,
+ Bundle parameters) {
+ if (mTvView == null || cmdType == null) {
+ return;
+ }
+ switch (cmdType) {
+ case TvInteractiveAppService.PLAYBACK_COMMAND_TYPE_TUNE:
+ if (parameters == null) {
+ return;
+ }
+ String uriString = parameters.getString(
+ TvInteractiveAppService.COMMAND_PARAMETER_KEY_CHANNEL_URI);
+ if (uriString != null) {
+ Uri channelUri = Uri.parse(uriString);
+ Channel channel = mMainActivity.getChannelDataManager().getChannel(
+ ContentUriUtils.safeParseId(channelUri));
+ if (channel != null) {
+ mHandler.post(() -> mMainActivity.tuneToChannel(channel));
+ }
+ }
+ break;
+ case TvInteractiveAppService.PLAYBACK_COMMAND_TYPE_SELECT_TRACK:
+ if (mTvView != null && parameters != null) {
+ int trackType = parameters.getInt(
+ TvInteractiveAppService.COMMAND_PARAMETER_KEY_TRACK_TYPE,
+ -1);
+ String trackId = parameters.getString(
+ TvInteractiveAppService.COMMAND_PARAMETER_KEY_TRACK_ID,
+ null);
+ switch (trackType) {
+ case TvTrackInfo.TYPE_AUDIO:
+ // When trackId is null, deselects current audio track.
+ mHandler.post(() -> mMainActivity.selectAudioTrack(trackId));
+ break;
+ case TvTrackInfo.TYPE_SUBTITLE:
+ // When trackId is null, turns off captions.
+ mHandler.post(() -> mMainActivity.selectSubtitleTrack(
+ trackId == null ? OPTION_OFF : OPTION_ON, trackId));
+ break;
+ }
+ }
+ break;
+ case TvInteractiveAppService.PLAYBACK_COMMAND_TYPE_SET_STREAM_VOLUME:
+ if (parameters == null) {
+ return;
+ }
+ float volume = parameters.getFloat(
+ TvInteractiveAppService.COMMAND_PARAMETER_KEY_VOLUME, -1);
+ if (volume >= 0.0 && volume <= 1.0) {
+ mHandler.post(() -> mTvView.setStreamVolume(volume));
+ }
+ break;
+ case TvInteractiveAppService.PLAYBACK_COMMAND_TYPE_TUNE_NEXT:
+ mHandler.post(mMainActivity::channelUp);
+ break;
+ case TvInteractiveAppService.PLAYBACK_COMMAND_TYPE_TUNE_PREV:
+ mHandler.post(mMainActivity::channelDown);
+ break;
+ case TvInteractiveAppService.PLAYBACK_COMMAND_TYPE_STOP:
+ int mode = 1; // TvInteractiveAppService.COMMAND_PARAMETER_VALUE_STOP_MODE_BLANK
+ if (parameters != null) {
+ mode = parameters.getInt(
+ /* TvInteractiveAppService.COMMAND_PARAMETER_KEY_STOP_MODE */
+ "command_stop_mode",
+ /*TvInteractiveAppService.COMMAND_PARAMETER_VALUE_STOP_MODE_BLANK*/
+ 1);
+ }
+ mHandler.post(mMainActivity::stopTv);
+ break;
+ default:
+ Log.e(TAG, "PlaybackCommandRequest had unknown cmdType:"
+ + cmdType);
+ break;
+ }
+ }
+
+ @Override
+ public void onStateChanged(String iAppServiceId, int state, int err) {
+ }
+
+ @Override
+ public void onBiInteractiveAppCreated(String iAppServiceId, Uri biIAppUri,
+ String biIAppId) {}
+
+ @Override
+ public void onTeletextAppStateChanged(String iAppServiceId, int state) {}
+
+ @Override
+ public void onSetVideoBounds(String iAppServiceId, Rect rect) {
+ if (mTvView != null) {
+ ViewGroup.MarginLayoutParams layoutParams = mTvView.getTvViewLayoutParams();
+ layoutParams.setMargins(rect.left, rect.top, rect.right, rect.bottom);
+ mTvView.setTvViewLayoutParams(layoutParams);
+ }
+ }
+
+ @Override
+ @TargetApi(34)
+ public void onRequestCurrentVideoBounds(@NonNull String iAppServiceId) {
+ mHandler.post(
+ () -> {
+ if (DEBUG) {
+ Log.d(TAG, "onRequestCurrentVideoBounds service ID = "
+ + iAppServiceId);
+ }
+ Rect bounds = new Rect(mTvView.getLeft(), mTvView.getTop(),
+ mTvView.getRight(), mTvView.getBottom());
+ mTvIAppView.sendCurrentVideoBounds(bounds);
+ });
+ }
+
+ @Override
+ public void onRequestCurrentChannelUri(String iAppServiceId) {
+ if (mTvIAppView == null) {
+ return;
+ }
+ Channel currentChannel = mMainActivity.getCurrentChannel();
+ Uri currentUri = (currentChannel == null)
+ ? null
+ : currentChannel.getUri();
+ mTvIAppView.sendCurrentChannelUri(currentUri);
+ }
+
+ @Override
+ public void onRequestCurrentChannelLcn(String iAppServiceId) {
+ if (mTvIAppView == null) {
+ return;
+ }
+ Channel currentChannel = mMainActivity.getCurrentChannel();
+ if (currentChannel == null || currentChannel.getDisplayNumber() == null) {
+ return;
+ }
+ // Expected format is major channel number, delimiter, minor channel number
+ String displayNumber = currentChannel.getDisplayNumber();
+ String format = "[0-9]+" + Channel.CHANNEL_NUMBER_DELIMITER + "[0-9]+";
+ if (!displayNumber.matches(format)) {
+ return;
+ }
+ // Major channel number is returned
+ String[] numbers = displayNumber.split(
+ String.valueOf(Channel.CHANNEL_NUMBER_DELIMITER));
+ mTvIAppView.sendCurrentChannelLcn(Integer.parseInt(numbers[0]));
+ }
+
+ @Override
+ public void onRequestStreamVolume(String iAppServiceId) {
+ if (mTvIAppView == null || mTvView == null) {
+ return;
+ }
+ mTvIAppView.sendStreamVolume(mTvView.getStreamVolume());
+ }
+
+ @Override
+ public void onRequestTrackInfoList(String iAppServiceId) {
+ if (mTvIAppView == null || mTvView == null) {
+ return;
+ }
+ List<TvTrackInfo> allTracks = new ArrayList<>();
+ int[] trackTypes = new int[] {TvTrackInfo.TYPE_AUDIO,
+ TvTrackInfo.TYPE_VIDEO, TvTrackInfo.TYPE_SUBTITLE};
+
+ for (int trackType : trackTypes) {
+ List<TvTrackInfo> currentTracks = mTvView.getTracks(trackType);
+ if (currentTracks == null) {
+ continue;
+ }
+ for (TvTrackInfo track : currentTracks) {
+ if (track != null) {
+ allTracks.add(track);
+ }
+ }
+ }
+ mTvIAppView.sendTrackInfoList(allTracks);
+ }
+
+ @Override
+ public void onRequestCurrentTvInputId(String iAppServiceId) {
+ if (mTvIAppView == null) {
+ return;
+ }
+ Channel currentChannel = mMainActivity.getCurrentChannel();
+ String currentInputId = (currentChannel == null)
+ ? null
+ : currentChannel.getInputId();
+ mTvIAppView.sendCurrentTvInputId(currentInputId);
+ }
+
+ @Override
+ public void onRequestSigning(String iAppServiceId, String signingId, String algorithm,
+ String alias, byte[] data) {}
+ }
+}
diff --git a/src/com/android/tv/onboarding/OnboardingActivity.java b/src/com/android/tv/onboarding/OnboardingActivity.java
index dd386d81..6eb1a894 100644
--- a/src/com/android/tv/onboarding/OnboardingActivity.java
+++ b/src/com/android/tv/onboarding/OnboardingActivity.java
@@ -66,7 +66,7 @@ public class OnboardingActivity extends SetupActivity {
@Override
public void onLoadFinished() {
mChannelDataManager.removeListener(this);
- mSetupUtils.markNewChannelsBrowsable();
+ mSetupUtils.markNewChannelsBrowsableIfEnabled();
}
@Override
@@ -97,9 +97,9 @@ public class OnboardingActivity extends SetupActivity {
mInputManager = singletons.getTvInputManagerHelper();
if (PermissionUtils.hasAccessAllEpg(this) || PermissionUtils.hasReadTvListings(this)) {
// Make the channels of the new inputs which have been setup outside TV app
- // browsable.
+ // browsable if enabled.
if (mChannelDataManager.isDbLoadFinished()) {
- mSetupUtils.markNewChannelsBrowsable();
+ mSetupUtils.markNewChannelsBrowsableIfEnabled();
} else {
mChannelDataManager.addListener(mChannelListener);
}
@@ -193,7 +193,7 @@ public class OnboardingActivity extends SetupActivity {
params.getString(
SetupSourcesFragment.ACTION_PARAM_KEY_INPUT_ID);
TvInputInfo input = mInputManager.getTvInputInfo(inputId);
- Intent intent = CommonUtils.createSetupIntent(input);
+ Intent intent = mSetupUtils.createSetupIntent(this, input);
if (intent == null) {
Toast.makeText(
this,
diff --git a/src/com/android/tv/onboarding/SetupSourcesFragment.java b/src/com/android/tv/onboarding/SetupSourcesFragment.java
index b97c7801..855fb274 100644
--- a/src/com/android/tv/onboarding/SetupSourcesFragment.java
+++ b/src/com/android/tv/onboarding/SetupSourcesFragment.java
@@ -313,7 +313,7 @@ public class SetupSourcesFragment extends SetupMultiPaneFragment {
TvInputInfo input = mInputs.get(i);
String inputId = input.getId();
String description;
- int channelCount = mChannelDataManager.getChannelCountForInput(inputId);
+ int channelCount = mChannelDataManager.getBrowsableChannelCountForInput(inputId);
if (mSetupUtils.isSetupDone(inputId) || channelCount > 0) {
if (channelCount == 0) {
description = getString(R.string.setup_input_no_channels);
diff --git a/src/com/android/tv/receiver/AudioCapabilitiesReceiver.java b/src/com/android/tv/receiver/AudioCapabilitiesReceiver.java
index 5fa7606d..9578e243 100644
--- a/src/com/android/tv/receiver/AudioCapabilitiesReceiver.java
+++ b/src/com/android/tv/receiver/AudioCapabilitiesReceiver.java
@@ -67,7 +67,8 @@ public final class AudioCapabilitiesReceiver {
}
public void register() {
- mContext.registerReceiver(mReceiver, new IntentFilter(AudioManager.ACTION_HDMI_AUDIO_PLUG));
+ mContext.registerReceiver(mReceiver, new IntentFilter(AudioManager.ACTION_HDMI_AUDIO_PLUG),
+ Context.RECEIVER_EXPORTED);
}
public void unregister() {
diff --git a/src/com/android/tv/setup/SystemSetupActivity.java b/src/com/android/tv/setup/SystemSetupActivity.java
index 7bf04692..b39ac4ea 100644
--- a/src/com/android/tv/setup/SystemSetupActivity.java
+++ b/src/com/android/tv/setup/SystemSetupActivity.java
@@ -53,6 +53,7 @@ public class SystemSetupActivity extends SetupActivity {
private static final int REQUEST_CODE_START_SETUP_ACTIVITY = 1;
@Inject TvInputManagerHelper mInputManager;
+ @Inject SetupUtils mSetupUtils;
@Inject UiFlags mUiFlags;
@Override
@@ -97,7 +98,7 @@ public class SystemSetupActivity extends SetupActivity {
params.getString(
SetupSourcesFragment.ACTION_PARAM_KEY_INPUT_ID);
TvInputInfo input = mInputManager.getTvInputInfo(inputId);
- Intent intent = CommonUtils.createSetupIntent(input);
+ Intent intent = mSetupUtils.createSetupIntent(this, input);
if (intent == null) {
Toast.makeText(
this,
diff --git a/src/com/android/tv/ui/BlockScreenView.java b/src/com/android/tv/ui/BlockScreenView.java
index b7a2dd95..9f61bf02 100644
--- a/src/com/android/tv/ui/BlockScreenView.java
+++ b/src/com/android/tv/ui/BlockScreenView.java
@@ -16,12 +16,15 @@
package com.android.tv.ui;
+import static com.android.tv.R.animator.tvview_block_screen_fade_out;
+
import android.animation.Animator;
import android.animation.Animator.AnimatorListener;
import android.animation.AnimatorInflater;
import android.animation.AnimatorListenerAdapter;
import android.content.Context;
import android.graphics.drawable.Drawable;
+import android.media.tv.TvInputInfo;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.AttributeSet;
@@ -30,6 +33,7 @@ import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.ImageView.ScaleType;
import android.widget.TextView;
+
import com.android.tv.R;
import com.android.tv.ui.TunableTvView.BlockScreenType;
@@ -42,6 +46,8 @@ public class BlockScreenView extends FrameLayout {
private TextView mBlockingInfoTextView;
private ImageView mBackgroundImageView;
+ private EmptyInputStatusBlockView mEmptyInputStatusBlockView;
+
private final int mSpacingNormal;
private final int mSpacingShrunken;
@@ -79,6 +85,7 @@ public class BlockScreenView extends FrameLayout {
mSpace = findViewById(R.id.space);
mBlockingInfoTextView = (TextView) findViewById(R.id.block_screen_text);
mBackgroundImageView = (ImageView) findViewById(R.id.background_image);
+ mEmptyInputStatusBlockView = findViewById(R.id.empty_input_status_block_view);
mFadeOut =
AnimatorInflater.loadAnimator(
getContext(), R.animator.tvview_block_screen_fade_out);
@@ -261,4 +268,12 @@ public class BlockScreenView extends FrameLayout {
public void setInfoTextClickable(boolean clickable) {
mBlockingInfoTextView.setClickable(clickable);
}
+
+ public void setEmptyInputStatusInputInfo(TvInputInfo inputInfo) {
+ mEmptyInputStatusBlockView.setIconAndLabelByInputInfo(inputInfo);
+ }
+
+ public void setEmptyInputStatusBlockVisibility(boolean visible) {
+ mEmptyInputStatusBlockView.setVisibility(visible ? View.VISIBLE : View.GONE);
+ }
}
diff --git a/src/com/android/tv/ui/EmptyInputStatusBlockView.java b/src/com/android/tv/ui/EmptyInputStatusBlockView.java
new file mode 100644
index 00000000..a7f91661
--- /dev/null
+++ b/src/com/android/tv/ui/EmptyInputStatusBlockView.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.media.tv.TvInputInfo;
+import android.util.AttributeSet;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.android.tv.R;
+
+public class EmptyInputStatusBlockView extends FrameLayout {
+ private ImageView mEmptyInputStatusIcon;
+ private TextView mEmptyInputTitleTextView;
+
+ public EmptyInputStatusBlockView(Context context) {
+ this(context, null, 0);
+ }
+
+ public EmptyInputStatusBlockView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public EmptyInputStatusBlockView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ inflate(context, R.layout.empty_input_status_block, this);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+
+ mEmptyInputStatusIcon = findViewById(R.id.empty_input_status_icon);
+ mEmptyInputTitleTextView = findViewById(R.id.empty_input_status_title_text);
+ }
+
+ public void setIconAndLabelByInputInfo(TvInputInfo inputInfo) {
+ CharSequence label = inputInfo.loadLabel(getContext());
+ String title = getResources().getString(R.string.empty_input_status_title_format, label);
+ mEmptyInputTitleTextView.setText(title);
+
+ if (inputInfo.isPassthroughInput()) {
+ mEmptyInputStatusIcon.setImageDrawable(
+ getResources().getDrawable(R.drawable.ic_empty_input_hdmi, null)
+ );
+ } else {
+ mEmptyInputStatusIcon.setImageDrawable(
+ getResources().getDrawable(R.drawable.ic_empty_input_tuner, null)
+ );
+ }
+ }
+}
diff --git a/src/com/android/tv/ui/InputBannerView.java b/src/com/android/tv/ui/InputBannerView.java
index d0609186..598d18ac 100644
--- a/src/com/android/tv/ui/InputBannerView.java
+++ b/src/com/android/tv/ui/InputBannerView.java
@@ -21,49 +21,38 @@ import android.media.tv.TvInputInfo;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.View;
-import android.widget.LinearLayout;
import android.widget.TextView;
+
import com.android.tv.MainActivity;
import com.android.tv.R;
import com.android.tv.data.api.Channel;
-public class InputBannerView extends LinearLayout implements TvTransitionManager.TransitionLayout {
- private final long mShowDurationMillis;
+public class InputBannerView extends InputBannerViewBase
+ implements TvTransitionManager.TransitionLayout {
- private final Runnable mHideRunnable =
- () ->
- ((MainActivity) getContext())
- .getOverlayManager()
- .hideOverlays(
- TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_DIALOG
- | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS
- | 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;
public InputBannerView(Context context) {
- this(context, null, 0);
+ super(context);
}
public InputBannerView(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
+ super(context, attrs);
}
public InputBannerView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
- mShowDurationMillis =
- context.getResources().getInteger(R.integer.select_input_show_duration);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
- mInputLabelTextView = (TextView) findViewById(R.id.input_label);
- mSecondaryInputLabelTextView = (TextView) findViewById(R.id.secondary_input_label);
+ mInputLabelTextView = findViewById(R.id.input_label);
+ mSecondaryInputLabelTextView = findViewById(R.id.secondary_input_label);
}
+ @Override
public void updateLabel() {
MainActivity mainActivity = (MainActivity) getContext();
Channel channel = mainActivity.getCurrentChannel();
@@ -83,15 +72,4 @@ public class InputBannerView extends LinearLayout implements TvTransitionManager
mSecondaryInputLabelTextView.setVisibility(View.VISIBLE);
}
}
-
- @Override
- public void onEnterAction(boolean fromEmptyScene) {
- removeCallbacks(mHideRunnable);
- postDelayed(mHideRunnable, mShowDurationMillis);
- }
-
- @Override
- public void onExitAction() {
- removeCallbacks(mHideRunnable);
- }
}
diff --git a/src/com/android/tv/ui/InputBannerViewBase.java b/src/com/android/tv/ui/InputBannerViewBase.java
new file mode 100644
index 00000000..c20e1da7
--- /dev/null
+++ b/src/com/android/tv/ui/InputBannerViewBase.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.util.AttributeSet;
+import android.widget.LinearLayout;
+
+import com.android.tv.MainActivity;
+import com.android.tv.R;
+import com.android.tv.data.StreamInfo;
+
+public abstract class InputBannerViewBase extends LinearLayout
+ implements TvTransitionManager.TransitionLayout {
+ protected final long mShowDurationMillis;
+ protected final Runnable mHideRunnable =
+ () ->
+ ((MainActivity) getContext())
+ .getOverlayManager()
+ .hideOverlays(
+ TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_DIALOG
+ | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS
+ | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE
+ | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_MENU
+ | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT);
+ public InputBannerViewBase(Context context) {
+ this(context, null, 0);
+ }
+
+ public InputBannerViewBase(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public InputBannerViewBase(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ mShowDurationMillis =
+ context.getResources().getInteger(R.integer.select_input_show_duration);
+
+ }
+
+ public abstract void updateLabel();
+
+ @Override
+ public void onEnterAction(boolean fromEmptyScene) {
+ removeCallbacks(mHideRunnable);
+ postDelayed(mHideRunnable, mShowDurationMillis);
+ }
+
+ @Override
+ public void onExitAction() {
+ removeCallbacks(mHideRunnable);
+ }
+
+ public void onStreamInfoUpdated(StreamInfo info) {}
+}
diff --git a/src/com/android/tv/ui/InputBannerViewV2.java b/src/com/android/tv/ui/InputBannerViewV2.java
new file mode 100644
index 00000000..e340819b
--- /dev/null
+++ b/src/com/android/tv/ui/InputBannerViewV2.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.media.tv.TvInputInfo;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.widget.TextView;
+
+import com.android.tv.MainActivity;
+import com.android.tv.R;
+import com.android.tv.data.StreamInfo;
+import com.android.tv.data.api.Channel;
+
+public class InputBannerViewV2 extends InputBannerViewBase
+ implements TvTransitionManager.TransitionLayout {
+ private static final String TAG = "InputBannerViewV2";
+
+ private TextView mInputLabelTextView;
+ public InputBannerViewV2(Context context) {
+ super(context);
+ }
+
+ public InputBannerViewV2(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public InputBannerViewV2(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mInputLabelTextView = findViewById(R.id.input_label);
+ }
+
+ @Override
+ public void updateLabel() {
+ MainActivity mainActivity = (MainActivity) getContext();
+ Channel channel = mainActivity.getCurrentChannel();
+ if (channel == null || !channel.isPassthrough()) {
+ return;
+ }
+
+ TvInputInfo input =
+ mainActivity.getTvInputManagerHelper().getTvInputInfo(channel.getInputId());
+ if (input == null) {
+ Log.e(TAG, "unable to get TvInputInfo of id " + channel.getInputId());
+ return;
+ }
+
+ updateInputLabel(input);
+ }
+
+ private void updateInputLabel(TvInputInfo input) {
+ CharSequence customLabel = input.loadCustomLabel(getContext());
+ CharSequence label = input.loadLabel(getContext());
+
+ if (customLabel == null) {
+ mInputLabelTextView.setText(label);
+ } else {
+ String inputLabel = getResources().getString(
+ R.string.input_banner_v2_input_label_format, label, customLabel);
+ mInputLabelTextView.setText(inputLabel);
+ }
+
+ }
+}
diff --git a/src/com/android/tv/ui/SelectInputView.java b/src/com/android/tv/ui/SelectInputView.java
index a0cfad32..8265d178 100644
--- a/src/com/android/tv/ui/SelectInputView.java
+++ b/src/com/android/tv/ui/SelectInputView.java
@@ -287,10 +287,18 @@ public class SelectInputView extends VerticalGridView
CharSequence customLabel = input.loadCustomLabel(getContext());
CharSequence label = input.loadLabel(getContext());
if (TextUtils.isEmpty(customLabel) || customLabel.equals(label)) {
- inputLabelView.setText(label);
+ if (input.isPassthroughInput()) {
+ inputLabelView.setText(label);
+ } else {
+ inputLabelView.setText(R.string.input_long_label_for_tuner);
+ }
secondaryInputLabelView.setVisibility(View.GONE);
} else {
- inputLabelView.setText(customLabel);
+ if (input.isPassthroughInput()) {
+ inputLabelView.setText(customLabel);
+ } else {
+ inputLabelView.setText(R.string.input_long_label_for_tuner);
+ }
secondaryInputLabelView.setText(label);
secondaryInputLabelView.setVisibility(View.VISIBLE);
}
diff --git a/src/com/android/tv/ui/TunableTvView.java b/src/com/android/tv/ui/TunableTvView.java
index a736e79d..76fb0a12 100644
--- a/src/com/android/tv/ui/TunableTvView.java
+++ b/src/com/android/tv/ui/TunableTvView.java
@@ -19,6 +19,7 @@ package com.android.tv.ui;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.TimeInterpolator;
+import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.content.pm.PackageManager;
@@ -28,12 +29,14 @@ import android.graphics.PorterDuff;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.media.PlaybackParams;
+import android.media.tv.AitInfo;
import android.media.tv.TvContentRating;
import android.media.tv.TvInputInfo;
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.interactive.TvInteractiveAppView;
import android.net.ConnectivityManager;
import android.net.Uri;
import android.os.AsyncTask;
@@ -194,6 +197,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV
private final InputSessionManager mInputSessionManager;
private int mChannelSignalStrength;
+ private TvInteractiveAppView mTvIAppView;
private final TvInputCallbackCompat mCallback =
new TvInputCallbackCompat() {
@@ -413,6 +417,25 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV
mOnTuneListener.onChannelSignalStrength();
}
}
+
+ @TargetApi(Build.VERSION_CODES.TIRAMISU)
+ @Override
+ public void onAitInfoUpdated(String inputId, AitInfo aitInfo) {
+ if (!TvFeatures.HAS_TIAF.isEnabled(getContext())) {
+ return;
+ }
+ if (DEBUG) {
+ Log.d(TAG,
+ "onAitInfoUpdated: {inputId="
+ + inputId
+ + ", AitInfo=("
+ + aitInfo
+ +")}");
+ }
+ if (mOnTuneListener != null) {
+ mOnTuneListener.onAitInfoUpdated(inputId, aitInfo);
+ }
+ }
};
public TunableTvView(Context context) {
@@ -476,18 +499,26 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV
});
mAccessibilityManager = context.getSystemService(AccessibilityManager.class);
}
+ public void initialize(
+ ProgramDataManager programDataManager,
+ TvInputManagerHelper tvInputManagerHelper,
+ LegacyFlags legacyFlags) {
+ initialize(programDataManager, tvInputManagerHelper, legacyFlags, null);
+ }
public void initialize(
ProgramDataManager programDataManager,
TvInputManagerHelper tvInputManagerHelper,
- LegacyFlags mLegacyFlags) {
+ LegacyFlags legacyFlags,
+ TvInteractiveAppView tvIAppView) {
mTvView = findViewById(R.id.tv_view);
- mTvView.setUseSecureSurface(!BuildConfig.ENG && !mLegacyFlags.enableDeveloperFeatures());
+ mTvView.setUseSecureSurface(!BuildConfig.ENG && !legacyFlags.enableDeveloperFeatures());
mProgramDataManager = programDataManager;
mInputManagerHelper = tvInputManagerHelper;
mContentRatingsManager = tvInputManagerHelper.getContentRatingsManager();
mParentalControlSettings = tvInputManagerHelper.getParentalControlSettings();
+ mTvIAppView = tvIAppView;
if (mInputSessionManager != null) {
mTvViewSession = mInputSessionManager.createTvViewSession(mTvView, this, mCallback);
} else {
@@ -715,6 +746,13 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV
}
}
+ @Override
+ public float getStreamVolume() {
+ return mIsMuted
+ ? 0
+ : mVolume;
+ }
+
/**
* Sets fixed size for the internal {@link android.view.Surface} of {@link
* android.media.tv.TvView}. If either {@code width} or {@code height} is non positive, the
@@ -773,6 +811,9 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV
void onContentAllowed();
void onChannelSignalStrength();
+
+ @TargetApi(Build.VERSION_CODES.TIRAMISU)
+ void onAitInfoUpdated(String inputId, AitInfo aitInfo);
}
public void unblockContent(TvContentRating rating) {
@@ -976,6 +1017,15 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV
return;
}
mBlockScreenView.setVisibility(VISIBLE);
+ if (shouldShowEmptyInputStatusBlock()){
+ mBlockScreenView.setEmptyInputStatusInputInfo(mInputInfo);
+ mBlockScreenView.setEmptyInputStatusBlockVisibility(true);
+ } else {
+ mBlockScreenView.setEmptyInputStatusBlockVisibility(false);
+ }
+ if (mTvIAppView != null) {
+ mTvIAppView.setVisibility(INVISIBLE);
+ }
mBlockScreenView.setBackgroundImage(null);
if (blockReason == VIDEO_UNAVAILABLE_REASON_SCREEN_BLOCKED) {
mBlockScreenView.setIconVisibility(true);
@@ -1007,6 +1057,9 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV
if (mBlockScreenView.getVisibility() == VISIBLE) {
mBlockScreenView.fadeOut();
}
+ if (mTvIAppView != null) {
+ mTvIAppView.setVisibility(VISIBLE);
+ }
}
}
@@ -1208,6 +1261,13 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV
}
}
+ private boolean shouldShowEmptyInputStatusBlock() {
+ return TvFeatures.USE_GTV_LIVETV_V2.isEnabled(getContext()) &&
+ (mVideoUnavailableReason == TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL ||
+ mVideoUnavailableReason ==
+ CommonConstants.VIDEO_UNAVAILABLE_REASON_NOT_CONNECTED);
+ }
+
private boolean isBundledInput() {
return mInputInfo != null
&& mInputInfo.getType() == TvInputInfo.TYPE_TUNER
diff --git a/src/com/android/tv/ui/TvOverlayManager.java b/src/com/android/tv/ui/TvOverlayManager.java
index cf1a9113..dc4a27a8 100644
--- a/src/com/android/tv/ui/TvOverlayManager.java
+++ b/src/com/android/tv/ui/TvOverlayManager.java
@@ -49,12 +49,14 @@ import com.android.tv.common.ui.setup.SetupFragment;
import com.android.tv.common.ui.setup.SetupMultiPaneFragment;
import com.android.tv.data.ChannelDataManager;
import com.android.tv.data.ProgramDataManager;
+import com.android.tv.data.StreamInfo;
import com.android.tv.dialog.DvrHistoryDialogFragment;
import com.android.tv.dialog.FullscreenDialogFragment;
import com.android.tv.dialog.HalfSizedDialogFragment;
import com.android.tv.dialog.PinDialogFragment;
import com.android.tv.dialog.RecentlyWatchedDialogFragment;
import com.android.tv.dialog.SafeDismissDialogFragment;
+import com.android.tv.dialog.InteractiveAppDialogFragment;
import com.android.tv.dvr.DvrDataManager;
import com.android.tv.dvr.ui.browse.DvrBrowseActivity;
import com.android.tv.guide.ProgramGuide;
@@ -198,6 +200,7 @@ public class TvOverlayManager implements AccessibilityStateChangeListener {
AVAILABLE_DIALOG_TAGS.add(LicenseDialogFragment.DIALOG_TAG);
AVAILABLE_DIALOG_TAGS.add(RatingsFragment.AttributionItem.DIALOG_TAG);
AVAILABLE_DIALOG_TAGS.add(HalfSizedDialogFragment.DIALOG_TAG);
+ AVAILABLE_DIALOG_TAGS.add(InteractiveAppDialogFragment.DIALOG_TAG);
}
private final MainActivity mMainActivity;
@@ -210,6 +213,7 @@ public class TvOverlayManager implements AccessibilityStateChangeListener {
private final SideFragmentManager mSideFragmentManager;
private final ProgramGuide mProgramGuide;
private final ChannelBannerView mChannelBannerView;
+ private final InputBannerViewBase mInputBannerView;
private final KeypadChannelSwitchView mKeypadChannelSwitchView;
private final SelectInputView mSelectInputView;
private final ProgramGuideSearchFragment mSearchFragment;
@@ -235,7 +239,7 @@ public class TvOverlayManager implements AccessibilityStateChangeListener {
TvOptionsManager optionsManager,
KeypadChannelSwitchView keypadChannelSwitchView,
ChannelBannerView channelBannerView,
- InputBannerView inputBannerView,
+ InputBannerViewBase inputBannerView,
SelectInputView selectInputView,
ViewGroup sceneContainer,
ProgramGuideSearchFragment searchFragment,
@@ -251,6 +255,7 @@ public class TvOverlayManager implements AccessibilityStateChangeListener {
mInputManager = tvInputManager;
mTvView = tvView;
mChannelBannerView = channelBannerView;
+ mInputBannerView = inputBannerView;
mKeypadChannelSwitchView = keypadChannelSwitchView;
mSelectInputView = selectInputView;
mSearchFragment = searchFragment;
@@ -873,6 +878,12 @@ public class TvOverlayManager implements AccessibilityStateChangeListener {
}
}
+ public void updateInputBannerIfNeeded(StreamInfo info) {
+ if (mTransitionManager.isInputBannerActive()) {
+ mInputBannerView.onStreamInfoUpdated(info);
+ }
+ }
+
@TvOverlayType
private int convertSceneToOverlayType(@SceneType int sceneType) {
switch (sceneType) {
diff --git a/src/com/android/tv/ui/TvTransitionManager.java b/src/com/android/tv/ui/TvTransitionManager.java
index f60337f1..579fcfce 100644
--- a/src/com/android/tv/ui/TvTransitionManager.java
+++ b/src/com/android/tv/ui/TvTransitionManager.java
@@ -56,7 +56,7 @@ public class TvTransitionManager extends TransitionManager {
private final MainActivity mMainActivity;
private final ViewGroup mSceneContainer;
private final ChannelBannerView mChannelBannerView;
- private final InputBannerView mInputBannerView;
+ private final InputBannerViewBase mInputBannerView;
private final KeypadChannelSwitchView mKeypadChannelSwitchView;
private final SelectInputView mSelectInputView;
private final FrameLayout mEmptyView;
@@ -78,7 +78,7 @@ public class TvTransitionManager extends TransitionManager {
MainActivity mainActivity,
ViewGroup sceneContainer,
ChannelBannerView channelBannerView,
- InputBannerView inputBannerView,
+ InputBannerViewBase inputBannerView,
KeypadChannelSwitchView keypadChannelSwitchView,
SelectInputView selectInputView) {
mMainActivity = mainActivity;
@@ -159,6 +159,10 @@ public class TvTransitionManager extends TransitionManager {
return mCurrentScene != null && mCurrentScene == mSelectInputScene;
}
+ public boolean isInputBannerActive() {
+ return mCurrentScene != null && mCurrentScene == mInputBannerScene;
+ }
+
public void setListener(Listener listener) {
mListener = listener;
}
diff --git a/src/com/android/tv/ui/sidepanel/InteractiveAppSettingsFragment.java b/src/com/android/tv/ui/sidepanel/InteractiveAppSettingsFragment.java
new file mode 100755
index 00000000..b56a1d66
--- /dev/null
+++ b/src/com/android/tv/ui/sidepanel/InteractiveAppSettingsFragment.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.ui.sidepanel;
+
+import com.android.tv.R;
+import com.android.tv.util.TvSettings;
+import java.util.ArrayList;
+import java.util.List;
+
+public class InteractiveAppSettingsFragment extends SideFragment {
+ private static final String TRACKER_LABEL = "Interactive Application Settings";
+ @Override
+ protected String getTitle() {
+ return getString(R.string.interactive_app_settings);
+ }
+ @Override
+ public String getTrackerLabel() {
+ return TRACKER_LABEL;
+ }
+ @Override
+ protected List<Item> getItemList() {
+ List<Item> items = new ArrayList<>();
+ items.add(
+ new SwitchItem(
+ getString(R.string.tv_iapp_on),
+ getString(R.string.tv_iapp_off)) {
+ @Override
+ protected void onUpdate() {
+ super.onUpdate();
+ setChecked(TvSettings.isTvIAppOn(getContext()));
+ }
+ @Override
+ protected void onSelected() {
+ super.onSelected();
+ boolean checked = isChecked();
+ TvSettings.setTvIAppOn(getContext(), checked);
+ }
+ });
+ return items;
+ }
+}
diff --git a/src/com/android/tv/ui/sidepanel/SettingsFragment.java b/src/com/android/tv/ui/sidepanel/SettingsFragment.java
index 1c03b6a9..762a190c 100644
--- a/src/com/android/tv/ui/sidepanel/SettingsFragment.java
+++ b/src/com/android/tv/ui/sidepanel/SettingsFragment.java
@@ -29,6 +29,7 @@ import com.android.tv.common.CommonPreferences;
import com.android.tv.common.customization.CustomizationManager;
import com.android.tv.common.util.PermissionUtils;
import com.android.tv.dialog.PinDialogFragment;
+import com.android.tv.features.TvFeatures;
import com.android.tv.license.LicenseSideFragment;
import com.android.tv.license.Licenses;
import com.android.tv.util.Utils;
@@ -190,6 +191,22 @@ public class SettingsFragment extends SideFragment {
}
});
}
+
+ //Interactive Application Settings
+ if (TvFeatures.HAS_TIAF.isEnabled(getContext()))
+ {
+ items.add(
+ new ActionItem(getString(R.string.interactive_app_settings)) {
+ @Override
+ protected void onSelected() {
+ getMainActivity()
+ .getOverlayManager()
+ .getSideFragmentManager()
+ .show(new InteractiveAppSettingsFragment(), false);
+ }
+ });
+ }
+
// Show version.
SimpleActionItem version =
new SimpleActionItem(
diff --git a/src/com/android/tv/util/SetupUtils.java b/src/com/android/tv/util/SetupUtils.java
index 52b3e3e8..858114d5 100644
--- a/src/com/android/tv/util/SetupUtils.java
+++ b/src/com/android/tv/util/SetupUtils.java
@@ -31,14 +31,18 @@ import android.support.annotation.UiThread;
import android.text.TextUtils;
import android.util.ArraySet;
import android.util.Log;
+import com.android.tv.R;
import com.android.tv.TvSingletons;
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.common.util.CommonUtils;
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.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
@@ -59,6 +63,8 @@ public class SetupUtils {
// Recognized inputs means that the user already knows the inputs are installed.
private static final String PREF_KEY_RECOGNIZED_INPUTS = "recognized_inputs";
private static final String PREF_KEY_IS_FIRST_TUNE = "is_first_tune";
+ // Whether to mark new channels to browsable.
+ private static final boolean MARK_NEW_CHANNELS_BROWSABLE = false;
private final Context mContext;
private final SharedPreferences mSharedPreferences;
@@ -128,11 +134,11 @@ public class SetupUtils {
boolean browsableChanged = false;
for (Channel channel : manager.getChannelList()) {
if (channel.getInputId().equals(inputId)) {
- if (!channel.isBrowsable()) {
+ if (!channel.isBrowsable() && MARK_NEW_CHANNELS_BROWSABLE) {
manager.updateBrowsable(channel.getId(), true, true);
browsableChanged = true;
}
- if (firstChannelForInput == null) {
+ if (firstChannelForInput == null && channel.isBrowsable()) {
firstChannelForInput = channel;
}
}
@@ -150,9 +156,15 @@ public class SetupUtils {
});
}
- /** Marks the channels in newly installed inputs browsable. */
+ /** Marks the channels in newly installed inputs browsable if enabled. */
@UiThread
- public void markNewChannelsBrowsable() {
+ public void markNewChannelsBrowsableIfEnabled() {
+ // TODO(b/288499376): Handle browsable field for channels added outside Live TV app in a
+ // better way.
+ if (!MARK_NEW_CHANNELS_BROWSABLE) {
+ return;
+ }
+
Set<String> newInputsWithChannels = new HashSet<>();
TvSingletons singletons = TvSingletons.getSingletons(mContext);
TvInputManagerHelper tvInputManagerHelper = singletons.getTvInputManagerHelper();
@@ -362,6 +374,52 @@ public class SetupUtils {
}
/**
+ * Create a Intent to launch setup activity for {@code inputId}. The setup activity defined
+ * in the overlayable resources precedes the one defined in the corresponding TV input service.
+ */
+ @Nullable
+ public Intent createSetupIntent(Context context, TvInputInfo input) {
+ String[] componentStrings = context.getResources()
+ .getStringArray(R.array.setup_ComponentNames);
+
+ if (componentStrings != null) {
+ for (String component : componentStrings) {
+ String[] split = component.split("#");
+ if (split.length != 2) {
+ Log.w(TAG, "Invalid component item: " + Arrays.toString(split));
+ continue;
+ }
+
+ final String inputId = split[0].trim();
+ if (inputId.equals(input.getId())) {
+ final String flattenedComponentName = split[1].trim();
+ final ComponentName componentName = ComponentName
+ .unflattenFromString(flattenedComponentName);
+ if (componentName == null) {
+ Log.w(TAG, "Failed to unflatten component: " + flattenedComponentName);
+ continue;
+ }
+
+ final Intent overlaySetupIntent = new Intent(Intent.ACTION_MAIN);
+ overlaySetupIntent.setComponent(componentName);
+ overlaySetupIntent.putExtra(TvInputInfo.EXTRA_INPUT_ID, inputId);
+
+ PackageManager pm = context.getPackageManager();
+ if (overlaySetupIntent.resolveActivityInfo(pm, 0) == null) {
+ Log.w(TAG, "unable to find component" + flattenedComponentName);
+ continue;
+ }
+
+ Log.i(TAG, "overlay input id: " + inputId
+ + " to setup activity: " + flattenedComponentName);
+ return CommonUtils.createSetupIntent(overlaySetupIntent, inputId);
+ }
+ }
+ }
+ return CommonUtils.createSetupIntent(input);
+ }
+
+ /**
* Called when an setup is done. Once it is called, {@link #isSetupDone} returns {@code true}
* for {@code inputId}.
*/
diff --git a/src/com/android/tv/util/TvSettings.java b/src/com/android/tv/util/TvSettings.java
index ae79e7e5..1a5434cb 100644
--- a/src/com/android/tv/util/TvSettings.java
+++ b/src/com/android/tv/util/TvSettings.java
@@ -53,6 +53,9 @@ public final class TvSettings {
private static final String PREF_CONTENT_RATING_LEVEL = "pref.content_rating_level";
private static final String PREF_DISABLE_PIN_UNTIL = "pref.disable_pin_until";
+ // tviapp settings
+ private static final String PREF_TV_IAPP_STATES = "pref.tviapp_on";
+
@Retention(RetentionPolicy.SOURCE)
@IntDef({
CONTENT_RATING_LEVEL_NONE,
@@ -242,4 +245,16 @@ public final class TvSettings {
.putLong(PREF_DISABLE_PIN_UNTIL, timeMillis)
.apply();
}
+
+ public static boolean isTvIAppOn(Context context) {
+ return PreferenceManager.getDefaultSharedPreferences(context)
+ .getBoolean(PREF_TV_IAPP_STATES, false);
+ }
+
+ public static void setTvIAppOn(Context context, boolean isOn) {
+ PreferenceManager.getDefaultSharedPreferences(context)
+ .edit()
+ .putBoolean(PREF_TV_IAPP_STATES, isOn)
+ .apply();
+ }
}
diff --git a/tests/Android.bp b/tests/Android.bp
new file mode 100644
index 00000000..f09cbfb7
--- /dev/null
+++ b/tests/Android.bp
@@ -0,0 +1,45 @@
+//
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+ // See: http://go/android-license-faq
+ default_applicable_licenses: [
+ "Android-Apache-2.0",
+ ],
+}
+
+android_test {
+ name: "TVUnitTests",
+ package_name: "com.android.tv.tests",
+ srcs: ["unit/src/**/*.java"],
+ static_libs: [
+ "androidx.test.runner",
+ "mockito-target",
+ "tv-test-common",
+ ],
+ libs: [
+ "android.test.runner.stubs",
+ "android.test.base.stubs",
+ "android.test.mock.stubs",
+ ],
+ manifest: "unit/AndroidManifest.xml",
+ resource_dirs: ["common/res"],
+ instrumentation_for: "LiveTv",
+ sdk_version: "system_current",
+ optimize: {
+ enabled: false,
+ },
+}
diff --git a/tests/Android.mk b/tests/Android.mk
deleted file mode 100644
index 5053e7d6..00000000
--- a/tests/Android.mk
+++ /dev/null
@@ -1 +0,0 @@
-include $(call all-subdir-makefiles)
diff --git a/tests/common/Android.bp b/tests/common/Android.bp
index 5a65538d..ae00673c 100644
--- a/tests/common/Android.bp
+++ b/tests/common/Android.bp
@@ -34,8 +34,8 @@ android_library {
"tv-guava-android-jar",
"mockito-robolectric-prebuilt",
"tv-lib-truth",
- "ub-uiautomator",
- "Robolectric_all-target",
+ "androidx.test.uiautomator_uiautomator",
+ "Robolectric_all-target_upstream",
],
// Link tv-common as shared library to avoid the problem of initialization of the constants
@@ -52,3 +52,27 @@ android_library {
},
}
+
+android_library {
+ name: "tv-test-common-robo",
+ srcs: [
+ "src/com/android/tv/testing/robo/**/*.java",
+ "src/com/android/tv/testing/shadows/**/*.java",
+ ],
+ static_libs: [
+ "Robolectric_all-target_upstream",
+ "mockito-robolectric-prebuilt",
+ "tv-test-common",
+ ],
+ // Disable dexpreopt and <uses-library> check for test.
+ enforce_uses_libs: false,
+ dex_preopt: {
+ enabled: false,
+ },
+ resource_dirs: [
+ "res",
+ ],
+ libs: [
+ "LiveTv",
+ ],
+}
diff --git a/tests/common/Android.mk b/tests/common/Android.mk
deleted file mode 100644
index 7a232ff7..00000000
--- a/tests/common/Android.mk
+++ /dev/null
@@ -1,28 +0,0 @@
-LOCAL_PATH:= $(call my-dir)
-include $(CLEAR_VARS)
-
-LOCAL_MODULE := tv-test-common-robo
-LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
-LOCAL_LICENSE_CONDITIONS := notice
-
-LOCAL_SRC_FILES := \
- $(call all-java-files-under, src/com/android/tv/testing/robo) \
- $(call all-java-files-under, src/com/android/tv/testing/shadows)
-
-LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
-
-LOCAL_STATIC_JAVA_LIBRARIES := \
- robolectric_android-all-stub \
- Robolectric_all-target \
- mockito-robolectric-prebuilt \
- tv-test-common \
-
-# Disable dexpreopt and <uses-library> check for test.
-LOCAL_ENFORCE_USES_LIBRARIES := false
-LOCAL_DEX_PREOPT := false
-
-LOCAL_INSTRUMENTATION_FOR := LiveTv
-
-LOCAL_MODULE_TAGS := optional
-
-include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/tests/common/src/com/android/tv/testing/uihelper/BaseUiDeviceHelper.java b/tests/common/src/com/android/tv/testing/uihelper/BaseUiDeviceHelper.java
index 21b05d67..6765266b 100644
--- a/tests/common/src/com/android/tv/testing/uihelper/BaseUiDeviceHelper.java
+++ b/tests/common/src/com/android/tv/testing/uihelper/BaseUiDeviceHelper.java
@@ -16,7 +16,7 @@
package com.android.tv.testing.uihelper;
import android.content.res.Resources;
-import android.support.test.uiautomator.UiDevice;
+import androidx.test.uiautomator.UiDevice;
/** Base class for building UiAutomator Helper classes. */
public abstract class BaseUiDeviceHelper {
diff --git a/tests/common/src/com/android/tv/testing/uihelper/ByResource.java b/tests/common/src/com/android/tv/testing/uihelper/ByResource.java
index 47b8d9f9..84527c2e 100644
--- a/tests/common/src/com/android/tv/testing/uihelper/ByResource.java
+++ b/tests/common/src/com/android/tv/testing/uihelper/ByResource.java
@@ -16,8 +16,8 @@
package com.android.tv.testing.uihelper;
import android.content.res.Resources;
-import android.support.test.uiautomator.By;
-import android.support.test.uiautomator.BySelector;
+import androidx.test.uiautomator.By;
+import androidx.test.uiautomator.BySelector;
/** Convenience methods for creating {@link BySelector}s using resource ids. */
public final class ByResource {
diff --git a/tests/common/src/com/android/tv/testing/uihelper/Constants.java b/tests/common/src/com/android/tv/testing/uihelper/Constants.java
index 4b522914..d057d5ff 100644
--- a/tests/common/src/com/android/tv/testing/uihelper/Constants.java
+++ b/tests/common/src/com/android/tv/testing/uihelper/Constants.java
@@ -15,8 +15,8 @@
*/
package com.android.tv.testing.uihelper;
-import android.support.test.uiautomator.By;
-import android.support.test.uiautomator.BySelector;
+import androidx.test.uiautomator.By;
+import androidx.test.uiautomator.BySelector;
import com.android.tv.common.CommonConstants;
public final class Constants {
diff --git a/tests/common/src/com/android/tv/testing/uihelper/DialogHelper.java b/tests/common/src/com/android/tv/testing/uihelper/DialogHelper.java
index 2ac4b648..e68d63a5 100644
--- a/tests/common/src/com/android/tv/testing/uihelper/DialogHelper.java
+++ b/tests/common/src/com/android/tv/testing/uihelper/DialogHelper.java
@@ -21,9 +21,9 @@ import static com.android.tv.testing.uihelper.UiDeviceAsserts.waitForCondition;
import android.app.DialogFragment;
import android.content.res.Resources;
-import android.support.test.uiautomator.BySelector;
-import android.support.test.uiautomator.UiDevice;
-import android.support.test.uiautomator.Until;
+import androidx.test.uiautomator.BySelector;
+import androidx.test.uiautomator.UiDevice;
+import androidx.test.uiautomator.Until;
import com.android.tv.R;
/** Helper for testing {@link DialogFragment}s. */
diff --git a/tests/common/src/com/android/tv/testing/uihelper/LiveChannelsUiDeviceHelper.java b/tests/common/src/com/android/tv/testing/uihelper/LiveChannelsUiDeviceHelper.java
index 30fbf371..ea0a6bf8 100644
--- a/tests/common/src/com/android/tv/testing/uihelper/LiveChannelsUiDeviceHelper.java
+++ b/tests/common/src/com/android/tv/testing/uihelper/LiveChannelsUiDeviceHelper.java
@@ -21,11 +21,11 @@ import static junit.framework.TestCase.assertTrue;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
-import android.support.test.uiautomator.By;
-import android.support.test.uiautomator.BySelector;
-import android.support.test.uiautomator.UiDevice;
-import android.support.test.uiautomator.Until;
import android.util.Log;
+import androidx.test.uiautomator.By;
+import androidx.test.uiautomator.BySelector;
+import androidx.test.uiautomator.UiDevice;
+import androidx.test.uiautomator.Until;
import com.android.tv.common.CommonConstants;
import com.android.tv.testing.utils.Utils;
import junit.framework.Assert;
diff --git a/tests/common/src/com/android/tv/testing/uihelper/MenuHelper.java b/tests/common/src/com/android/tv/testing/uihelper/MenuHelper.java
index c8ea85ac..c9ffbd61 100644
--- a/tests/common/src/com/android/tv/testing/uihelper/MenuHelper.java
+++ b/tests/common/src/com/android/tv/testing/uihelper/MenuHelper.java
@@ -19,12 +19,12 @@ package com.android.tv.testing.uihelper;
import static com.android.tv.testing.uihelper.Constants.MENU;
import android.content.res.Resources;
-import android.support.test.uiautomator.By;
-import android.support.test.uiautomator.BySelector;
-import android.support.test.uiautomator.Direction;
-import android.support.test.uiautomator.UiDevice;
-import android.support.test.uiautomator.UiObject2;
-import android.support.test.uiautomator.Until;
+import androidx.test.uiautomator.By;
+import androidx.test.uiautomator.BySelector;
+import androidx.test.uiautomator.Direction;
+import androidx.test.uiautomator.UiDevice;
+import androidx.test.uiautomator.UiObject2;
+import androidx.test.uiautomator.Until;
import com.android.tv.R;
import junit.framework.Assert;
diff --git a/tests/common/src/com/android/tv/testing/uihelper/SidePanelHelper.java b/tests/common/src/com/android/tv/testing/uihelper/SidePanelHelper.java
index ba015260..d1a0651b 100644
--- a/tests/common/src/com/android/tv/testing/uihelper/SidePanelHelper.java
+++ b/tests/common/src/com/android/tv/testing/uihelper/SidePanelHelper.java
@@ -17,11 +17,11 @@
package com.android.tv.testing.uihelper;
import android.content.res.Resources;
-import android.support.test.uiautomator.By;
-import android.support.test.uiautomator.BySelector;
-import android.support.test.uiautomator.Direction;
-import android.support.test.uiautomator.UiDevice;
-import android.support.test.uiautomator.UiObject2;
+import androidx.test.uiautomator.By;
+import androidx.test.uiautomator.BySelector;
+import androidx.test.uiautomator.Direction;
+import androidx.test.uiautomator.UiDevice;
+import androidx.test.uiautomator.UiObject2;
import com.android.tv.R;
import com.android.tv.ui.sidepanel.SideFragment;
import junit.framework.Assert;
diff --git a/tests/common/src/com/android/tv/testing/uihelper/UiDeviceAsserts.java b/tests/common/src/com/android/tv/testing/uihelper/UiDeviceAsserts.java
index 28ea163e..2b01addb 100644
--- a/tests/common/src/com/android/tv/testing/uihelper/UiDeviceAsserts.java
+++ b/tests/common/src/com/android/tv/testing/uihelper/UiDeviceAsserts.java
@@ -20,13 +20,13 @@ import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertNotNull;
import static junit.framework.Assert.assertTrue;
-import android.support.test.uiautomator.By;
-import android.support.test.uiautomator.BySelector;
-import android.support.test.uiautomator.Direction;
-import android.support.test.uiautomator.SearchCondition;
-import android.support.test.uiautomator.UiDevice;
-import android.support.test.uiautomator.UiObject2;
-import android.support.test.uiautomator.Until;
+import androidx.test.uiautomator.By;
+import androidx.test.uiautomator.BySelector;
+import androidx.test.uiautomator.Direction;
+import androidx.test.uiautomator.SearchCondition;
+import androidx.test.uiautomator.UiDevice;
+import androidx.test.uiautomator.UiObject2;
+import androidx.test.uiautomator.Until;
import junit.framework.Assert;
/** Asserts for {@link UiDevice}s. */
diff --git a/tests/common/src/com/android/tv/testing/uihelper/UiDeviceUtils.java b/tests/common/src/com/android/tv/testing/uihelper/UiDeviceUtils.java
index d5545023..26017d94 100644
--- a/tests/common/src/com/android/tv/testing/uihelper/UiDeviceUtils.java
+++ b/tests/common/src/com/android/tv/testing/uihelper/UiDeviceUtils.java
@@ -15,9 +15,9 @@
*/
package com.android.tv.testing.uihelper;
-import android.support.test.uiautomator.Direction;
-import android.support.test.uiautomator.UiDevice;
import android.view.KeyEvent;
+import androidx.test.uiautomator.Direction;
+import androidx.test.uiautomator.UiDevice;
/** Static utility methods for {@link UiDevice}. */
public final class UiDeviceUtils {
diff --git a/tests/common/src/com/android/tv/testing/uihelper/UiObject2Asserts.java b/tests/common/src/com/android/tv/testing/uihelper/UiObject2Asserts.java
index ee02d7f7..d53bcc89 100644
--- a/tests/common/src/com/android/tv/testing/uihelper/UiObject2Asserts.java
+++ b/tests/common/src/com/android/tv/testing/uihelper/UiObject2Asserts.java
@@ -17,8 +17,8 @@ package com.android.tv.testing.uihelper;
import static junit.framework.Assert.assertTrue;
-import android.support.test.uiautomator.SearchCondition;
-import android.support.test.uiautomator.UiObject2;
+import androidx.test.uiautomator.SearchCondition;
+import androidx.test.uiautomator.UiObject2;
/** Asserts for {@link UiObject2}s. */
public final class UiObject2Asserts {
diff --git a/tests/common/src/com/android/tv/testing/uihelper/UiObject2Utils.java b/tests/common/src/com/android/tv/testing/uihelper/UiObject2Utils.java
index 2f3779c5..eab70817 100644
--- a/tests/common/src/com/android/tv/testing/uihelper/UiObject2Utils.java
+++ b/tests/common/src/com/android/tv/testing/uihelper/UiObject2Utils.java
@@ -16,8 +16,8 @@
package com.android.tv.testing.uihelper;
import android.graphics.Point;
-import android.support.test.uiautomator.Direction;
-import android.support.test.uiautomator.UiObject2;
+import androidx.test.uiautomator.Direction;
+import androidx.test.uiautomator.UiObject2;
/** Static utility methods for {@link UiObject2}s. */
public class UiObject2Utils {
diff --git a/tests/func/Android.bp b/tests/func/Android.bp
index d4085394..befb96f8 100644
--- a/tests/func/Android.bp
+++ b/tests/func/Android.bp
@@ -10,7 +10,7 @@ android_test {
static_libs: [
"androidx.test.runner",
"tv-test-common",
- "ub-uiautomator",
+ "androidx.test.uiautomator_uiautomator",
],
libs: ["android.test.base.stubs"],
instrumentation_for: "LiveTv",
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 c06c859c..e7171533 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.uiautomator.Until;
import androidx.test.filters.MediumTest;
+import androidx.test.uiautomator.Until;
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 2467de21..ac40531d 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.uiautomator.BySelector;
-import android.support.test.uiautomator.Until;
import androidx.test.filters.MediumTest;
+import androidx.test.uiautomator.BySelector;
+import androidx.test.uiautomator.Until;
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 ac2aad43..92105089 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.uiautomator.BySelector;
-import android.support.test.uiautomator.Until;
import androidx.test.filters.MediumTest;
+import androidx.test.uiautomator.BySelector;
+import androidx.test.uiautomator.Until;
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 fa3335d9..c3f6e736 100644
--- a/tests/func/src/com/android/tv/tests/ui/LiveChannelsTestController.java
+++ b/tests/func/src/com/android/tv/tests/ui/LiveChannelsTestController.java
@@ -23,14 +23,14 @@ import android.content.Context;
import android.content.res.Resources;
import android.os.Build;
import android.os.SystemClock;
-import android.support.test.uiautomator.BySelector;
-import android.support.test.uiautomator.Configurator;
-import android.support.test.uiautomator.SearchCondition;
-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 androidx.test.uiautomator.BySelector;
+import androidx.test.uiautomator.Configurator;
+import androidx.test.uiautomator.SearchCondition;
+import androidx.test.uiautomator.UiDevice;
+import androidx.test.uiautomator.Until;
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 bff0e7d7..8c269801 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.uiautomator.BySelector;
-import android.support.test.uiautomator.UiObject2;
-import android.support.test.uiautomator.Until;
import androidx.test.filters.MediumTest;
+import androidx.test.uiautomator.BySelector;
+import androidx.test.uiautomator.UiObject2;
+import androidx.test.uiautomator.Until;
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 e24c72fa..e129ad9a 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.uiautomator.BySelector;
-import android.support.test.uiautomator.UiObject2;
-import android.support.test.uiautomator.Until;
import android.view.KeyEvent;
import androidx.test.filters.SmallTest;
+import androidx.test.uiautomator.BySelector;
+import androidx.test.uiautomator.UiObject2;
+import androidx.test.uiautomator.Until;
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 0a6a85d6..0a8fa62a 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.uiautomator.Until;
import androidx.test.filters.MediumTest;
+import androidx.test.uiautomator.Until;
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 73e869f1..7d371401 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.uiautomator.Until;
import androidx.test.filters.LargeTest;
+import androidx.test.uiautomator.Until;
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 8998b458..359fd1d9 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.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 androidx.test.uiautomator.By;
+import androidx.test.uiautomator.BySelector;
+import androidx.test.uiautomator.Until;
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 d035874a..3ee2ad0d 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.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 androidx.test.uiautomator.BySelector;
+import androidx.test.uiautomator.Direction;
+import androidx.test.uiautomator.UiObject2;
+import androidx.test.uiautomator.Until;
import com.android.tv.R;
import com.android.tv.testing.uihelper.Constants;
import com.android.tv.tests.ui.LiveChannelsTestController;
diff --git a/tests/jank/Android.bp b/tests/jank/Android.bp
index 1cea734b..a16e38ec 100644
--- a/tests/jank/Android.bp
+++ b/tests/jank/Android.bp
@@ -9,9 +9,9 @@ android_test {
srcs: ["src/**/*.java"],
static_libs: [
"androidx.test.runner",
+ "androidx.test.uiautomator_uiautomator",
"tv-test-common",
"ub-janktesthelper",
- "ub-uiautomator",
],
libs: ["android.test.base.stubs"],
instrumentation_for: "LiveTv",
diff --git a/tests/jank/src/com/android/tv/tests/jank/LiveChannelsTestCase.java b/tests/jank/src/com/android/tv/tests/jank/LiveChannelsTestCase.java
index 507e9dd5..602aa53d 100644
--- a/tests/jank/src/com/android/tv/tests/jank/LiveChannelsTestCase.java
+++ b/tests/jank/src/com/android/tv/tests/jank/LiveChannelsTestCase.java
@@ -17,7 +17,7 @@ package com.android.tv.tests.jank;
import android.content.res.Resources;
import android.support.test.jank.JankTestBase;
-import android.support.test.uiautomator.UiDevice;
+import androidx.test.uiautomator.UiDevice;
import com.android.tv.testing.uihelper.LiveChannelsUiDeviceHelper;
/** Base test case for LiveChannel jank tests. */
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 da2eb9cb..3baf4557 100644
--- a/tests/jank/src/com/android/tv/tests/jank/ProgramGuideJankTest.java
+++ b/tests/jank/src/com/android/tv/tests/jank/ProgramGuideJankTest.java
@@ -19,8 +19,8 @@ import static com.android.tv.testing.uihelper.UiDeviceAsserts.assertWaitForCondi
import android.support.test.jank.GfxMonitor;
import android.support.test.jank.JankTest;
-import android.support.test.uiautomator.Until;
import androidx.test.filters.MediumTest;
+import androidx.test.uiautomator.Until;
import com.android.tv.R;
import com.android.tv.testing.uihelper.ByResource;
import com.android.tv.testing.uihelper.Constants;
diff --git a/tests/jank/src/com/android/tv/tests/jank/Utils.java b/tests/jank/src/com/android/tv/tests/jank/Utils.java
index 57e5f100..c1828ecc 100644
--- a/tests/jank/src/com/android/tv/tests/jank/Utils.java
+++ b/tests/jank/src/com/android/tv/tests/jank/Utils.java
@@ -15,7 +15,7 @@
*/
package com.android.tv.tests.jank;
-import android.support.test.uiautomator.UiDevice;
+import androidx.test.uiautomator.UiDevice;
import com.android.tv.testing.uihelper.UiDeviceUtils;
public final class Utils {
diff --git a/tests/robotests/Android.bp b/tests/robotests/Android.bp
new file mode 100644
index 00000000..56a7e701
--- /dev/null
+++ b/tests/robotests/Android.bp
@@ -0,0 +1,53 @@
+//
+// Copyright 2023, The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+ // See: http://go/android-license-faq
+ default_applicable_licenses: [
+ "Android-Apache-2.0",
+ ],
+}
+
+android_robolectric_test {
+ name: "TvRoboTests",
+ srcs: [
+ "src/**/*.java",
+ ],
+ exclude_srcs: [
+ "src/com/android/tv/data/epg/EpgFetcherImplTest.java",
+ "src/com/android/tv/guide/ProgramItemViewTest.java",
+ ],
+ static_libs: [
+ "tv-lib-dagger",
+ "tv-lib-truth",
+ "android-support-annotations",
+ "androidx.leanback_leanback",
+ "androidx.test.core",
+ "androidx.test.ext.truth",
+ "tv-lib-dagger-android",
+ "tv-test-common",
+ "tv-test-common-robo",
+ ],
+ plugins: [
+ "tv-lib-dagger-android-processor",
+ "tv-lib-dagger-compiler",
+ ],
+ instrumentation_for: "LiveTv",
+ test_options: {
+ timeout: 36000,
+ },
+ upstream: true,
+}
diff --git a/tests/robotests/Android.mk b/tests/robotests/Android.mk
deleted file mode 100644
index c5341ab8..00000000
--- a/tests/robotests/Android.mk
+++ /dev/null
@@ -1,78 +0,0 @@
-#############################################################
-# Tv Robolectric test target. #
-#############################################################
-LOCAL_PATH := $(call my-dir)
-include $(CLEAR_VARS)
-
-LOCAL_MODULE := TvRoboTests
-LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
-LOCAL_LICENSE_CONDITIONS := notice
-LOCAL_MODULE_CLASS := JAVA_LIBRARIES
-
-BASE_DIR = src/com/android/tv
-EXCLUDE_FILES := \
- $(BASE_DIR)/data/epg/EpgFetcherImplTest.java \
- $(BASE_DIR)/guide/ProgramItemViewTest.java \
-
-LOCAL_SRC_FILES := $(call all-java-files-under, src)
-LOCAL_SRC_FILES := $(filter-out $(EXCLUDE_FILES),$(LOCAL_SRC_FILES))
-
-LOCAL_JAVA_LIBRARIES := \
- Robolectric_all-target \
- mockito-robolectric-prebuilt \
- robolectric_android-all-stub \
-
-LOCAL_STATIC_JAVA_LIBRARIES := \
- tv-lib-dagger \
- tv-lib-truth \
-
-LOCAL_STATIC_ANDROID_LIBRARIES := \
- androidx.leanback_leanback-nodeps \
- androidx.test.core \
- androidx.test.ext.truth \
- tv-lib-dagger-android \
- tv-test-common \
- tv-test-common-robo \
-
-LOCAL_ANNOTATION_PROCESSORS := \
- tv-lib-dagger-android-processor \
- tv-lib-dagger-compiler \
-
-LOCAL_ANNOTATION_PROCESSOR_CLASSES := \
- dagger.internal.codegen.ComponentProcessor,dagger.android.processor.AndroidProcessor
-
-LOCAL_INSTRUMENTATION_FOR := LiveTv
-
-LOCAL_MODULE_TAGS := optional
-
-include $(BUILD_STATIC_JAVA_LIBRARY)
-
-#############################################################
-# Tv runner target to run the previous target. #
-#############################################################
-include $(CLEAR_VARS)
-LOCAL_MODULE := RunTvRoboTests
-LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
-LOCAL_LICENSE_CONDITIONS := notice
-
-BASE_DIR = com/android/tv
-EXCLUDE_FILES := \
- $(BASE_DIR)/data/epg/EpgFetcherImplTest.java \
- $(BASE_DIR)/guide/ProgramItemViewTest.java \
-
-LOCAL_ROBOTEST_FILES := $(call find-files-in-subdirs,$(LOCAL_PATH)/src,*Test.java,.)
-LOCAL_ROBOTEST_FILES := $(filter-out $(EXCLUDE_FILES),$(LOCAL_ROBOTEST_FILES))
-
-LOCAL_JAVA_LIBRARIES := \
- Robolectric_all-target \
- TvRoboTests \
- mockito-robolectric-prebuilt \
- robolectric_android-all-stub \
- tv-test-common \
- tv-test-common-robo \
-
-LOCAL_TEST_PACKAGE := LiveTv
-
-LOCAL_ROBOTEST_TIMEOUT := 36000
-
-include external/robolectric-shadows/run_robotests.mk
diff --git a/tests/robotests/src/com/android/tv/ShadowTvView.java b/tests/robotests/src/com/android/tv/ShadowTvView.java
index 8aad9f00..274bbb2a 100644
--- a/tests/robotests/src/com/android/tv/ShadowTvView.java
+++ b/tests/robotests/src/com/android/tv/ShadowTvView.java
@@ -46,10 +46,6 @@ public class ShadowTvView extends ShadowView {
public void __constructor__(Context context, AttributeSet attrs) {
}
- @Override
- public void __constructor__(Context context, AttributeSet attrs, int defStyleAttr) {
- }
-
@Implementation
public List<TvTrackInfo> getTracks(int type) {
return mTracks.get(type);
diff --git a/tests/robotests/src/com/android/tv/testing/TvRobolectricTestRunner.java b/tests/robotests/src/com/android/tv/testing/TvRobolectricTestRunner.java
index 93813c08..d1c16a8d 100644
--- a/tests/robotests/src/com/android/tv/testing/TvRobolectricTestRunner.java
+++ b/tests/robotests/src/com/android/tv/testing/TvRobolectricTestRunner.java
@@ -44,7 +44,6 @@ public class TvRobolectricTestRunner extends RobolectricTestRunner {
* We are going to create our own custom manifest so that we can add multiple resource paths to
* it. This lets us access resources in both Settings and SettingsLib in our tests.
*/
- @Override
protected AndroidManifest getAppManifest(Config config) {
final String packageName = "com.android.tv";
diff --git a/tests/unit/Android.mk b/tests/unit/Android.mk
deleted file mode 100644
index 6123af7b..00000000
--- a/tests/unit/Android.mk
+++ /dev/null
@@ -1,32 +0,0 @@
-LOCAL_PATH:= $(call my-dir)
-include $(CLEAR_VARS)
-
-LOCAL_MODULE_TAGS := tests
-
-
-# Include all test java files.
-LOCAL_SRC_FILES := $(call all-java-files-under, src)
-
-LOCAL_STATIC_JAVA_LIBRARIES := \
- androidx.test.runner \
- mockito-robolectric-prebuilt \
- tv-test-common \
-
-LOCAL_JAVA_LIBRARIES := \
- android.test.runner.stubs \
- android.test.base.stubs \
- android.test.mock.stubs \
-
-
-LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/../common/res
-
-LOCAL_PACKAGE_NAME := TVUnitTests
-LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
-LOCAL_LICENSE_CONDITIONS := notice
-
-LOCAL_INSTRUMENTATION_FOR := LiveTv
-
-LOCAL_SDK_VERSION := system_current
-
-LOCAL_PROGUARD_ENABLED := disabled
-include $(BUILD_PACKAGE)
diff --git a/tuner/Android.bp b/tuner/Android.bp
index d094c459..81ae2eda 100644
--- a/tuner/Android.bp
+++ b/tuner/Android.bp
@@ -52,4 +52,7 @@ android_library {
"tv-lib-dagger-compiler",
],
min_sdk_version: "23",
+ lint: {
+ baseline_filename: "lint-baseline.xml",
+ },
}
diff --git a/tuner/lint-baseline.xml b/tuner/lint-baseline.xml
index a0db5e0b..f359c6b3 100644
--- a/tuner/lint-baseline.xml
+++ b/tuner/lint-baseline.xml
@@ -169,4 +169,20 @@
line="101"/>
</issue>
-</issues>
+ <issue
+ id="NewApi"
+ message="Call requires API level 26 (current min is 23): `new android.app.Notification.TvExtender`">
+ <location
+ file="packages/apps/TV/tuner/src/com/android/tv/tuner/setup/BaseTunerSetupActivity.java"
+ line="416"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Cast from `TvExtender` to `Extender` requires API level 26 (current min is 23)">
+ <location
+ file="packages/apps/TV/tuner/src/com/android/tv/tuner/setup/BaseTunerSetupActivity.java"
+ line="416"/>
+ </issue>
+
+</issues> \ No newline at end of file
diff --git a/tuner/sampletunertvinput/src/com/android/tv/samples/sampletunertvinput/SampleTunerTvInputSectionParser.java b/tuner/sampletunertvinput/src/com/android/tv/samples/sampletunertvinput/SampleTunerTvInputSectionParser.java
new file mode 100644
index 00000000..20c73de4
--- /dev/null
+++ b/tuner/sampletunertvinput/src/com/android/tv/samples/sampletunertvinput/SampleTunerTvInputSectionParser.java
@@ -0,0 +1,341 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.samples.sampletunertvinput;
+
+import android.util.Log;
+
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+
+/** Parser for ATSC PSIP sections */
+public class SampleTunerTvInputSectionParser {
+ private static final String TAG = "SampleTunerTvInput";
+ private static final boolean DEBUG = true;
+
+ public static final byte DESCRIPTOR_TAG_EXTENDED_CHANNEL_NAME = (byte) 0xa0;
+ public static final byte COMPRESSION_TYPE_NO_COMPRESSION = (byte) 0x00;
+ public static final byte MODE_UTF16 = (byte) 0x3f;
+
+ /**
+ * Parses a single TVCT section, as defined in A/65 6.4
+ * @param data, a ByteBuffer containing a single TVCT section which describes only one channel
+ * @return null if there is an error while parsing, the channel with parsed data otherwise
+ */
+ public static TvctChannelInfo parseTvctSection(byte[] data) {
+ if (!checkValidPsipSection(data)) {
+ return null;
+ }
+ int numChannels = data[9] & 0xff;
+ if(numChannels != 1) {
+ Log.e(TAG, "parseTVCTSection expected 1 channel, found " + numChannels);
+ return null;
+ }
+ // TVCT Sections are a minimum of 16 bytes, with a minimum of 32 bytes per channel
+ if(data.length < 48) {
+ Log.e(TAG, "parseTVCTSection found section under minimum length");
+ return null;
+ }
+
+ // shortName begins at data[10] and ends at either the first stuffing
+ // UTF-16 character of value 0x0000, or at a length of 14 Bytes
+ int shortNameLength = 14;
+ for(int i = 0; i < 14; i += 2) {
+ int charValue = ((data[10 + i] & 0xff) << 8) | (data[10 + (i + 1)] & 0xff);
+ if (charValue == 0x0000) {
+ shortNameLength = i;
+ break;
+ }
+ }
+ // Data field positions are as defined by A/65 Section 6.4 for one channel
+ String name = new String(Arrays.copyOfRange(data, 10, 10 + shortNameLength),
+ StandardCharsets.UTF_16);
+ int majorNumber = ((data[24] & 0x0f) << 6) | ((data[25] & 0xff) >> 2);
+ int minorNumber = ((data[25] & 0x03) << 8) | (data[26] & 0xff);
+ if (DEBUG) {
+ Log.d(TAG, "parseTVCTSection found shortName: " + name
+ + " channel number: " + majorNumber + "-" + minorNumber);
+ }
+ int descriptorsLength = ((data[40] & 0x03) << 8) | (data[41] & 0xff);
+ List<TsDescriptor> descriptors = parseDescriptors(data, 42, 42 + descriptorsLength);
+ for (TsDescriptor descriptor : descriptors) {
+ if (descriptor instanceof ExtendedChannelNameDescriptor) {
+ ExtendedChannelNameDescriptor longNameDescriptor =
+ (ExtendedChannelNameDescriptor)descriptor;
+ name = longNameDescriptor.getLongChannelName();
+ if (DEBUG) {
+ Log.d(TAG, "parseTVCTSection found longName: " + name);
+ }
+ }
+ }
+
+ return new TvctChannelInfo(name, majorNumber, minorNumber);
+ }
+
+ /**
+ * Parses a single EIT section, as defined in ATSC A/65 Section 6.5
+ * @param data, a byte array containing a single EIT section which describes only one event
+ * @return {@code null} if there is an error while parsing, the event with parsed data otherwise
+ */
+ public static EitEventInfo parseEitSection(byte[] data) {
+ if (!checkValidPsipSection(data)) {
+ return null;
+ }
+ int numEvents = data[9] & 0xff;
+ if(numEvents != 1) {
+ Log.e(TAG, "parseEitSection expected 1 event, found " + numEvents);
+ return null;
+ }
+ // EIT Sections are a minimum of 14 bytes, with a minimum of 12 bytes per event
+ if(data.length < 26) {
+ Log.e(TAG, "parseEitSection found section under minimum length");
+ return null;
+ }
+
+ // Data field positions are as defined by A/65 Section 6.5 for one event
+ int lengthInSeconds = ((data[16] & 0x0f) << 16) | ((data[17] & 0xff) << 8)
+ | (data[18] & 0xff);
+ int titleLength = data[19] & 0xff;
+ String titleText = parseMultipleStringStructure(data, 20, 20 + titleLength);
+
+ if (DEBUG) {
+ Log.d(TAG, "parseEitSection found titleText: " + titleText
+ + " lengthInSeconds: " + lengthInSeconds);
+ }
+ return new EitEventInfo(titleText, lengthInSeconds);
+ }
+
+
+ // Descriptor data structure defined in ISO/IEC 13818-1 Section 2.6
+ // Returns an empty list on parsing failures
+ private static List<TsDescriptor> parseDescriptors(byte[] data, int offset, int limit) {
+ List<TsDescriptor> descriptors = new ArrayList<>();
+ if (data.length < limit) {
+ Log.e(TAG, "parseDescriptors given limit larger than data");
+ return descriptors;
+ }
+ int pos = offset;
+ while (pos + 1 < limit) {
+ int tag = data[pos] & 0xff;
+ int length = data[pos + 1] & 0xff;
+ if (length <= 0) {
+ continue;
+ }
+ pos += 2;
+
+ if (limit < pos + length) {
+ Log.e(TAG, "parseDescriptors found descriptor with length longer than limit");
+ break;
+ }
+ if (DEBUG) {
+ Log.d(TAG, "parseDescriptors found descriptor with tag: " + tag);
+ }
+ TsDescriptor descriptor = null;
+ switch ((byte) tag) {
+ case DESCRIPTOR_TAG_EXTENDED_CHANNEL_NAME:
+ descriptor = parseExtendedChannelNameDescriptor(data, pos, pos + length);
+ break;
+ default:
+ break;
+ }
+ if (descriptor != null) {
+ descriptors.add(descriptor);
+ }
+ pos += length;
+ }
+ return descriptors;
+ }
+
+ // ExtendedChannelNameDescriptor is defined in ATSC A/65 Section 6.9.4 as containing only
+ // a single MultipleStringStructure after its tag and length.
+ // @return {@code null} if parsing MultipleStringStructure fails
+ private static ExtendedChannelNameDescriptor parseExtendedChannelNameDescriptor(byte[] data,
+ int offset, int limit) {
+ String channelName = parseMultipleStringStructure(data, offset, limit);
+ return channelName == null ? null : new ExtendedChannelNameDescriptor(channelName);
+ }
+
+ // MultipleStringStructure is defined in ATSC A/65 Section 6.10
+ // Returns first string segment with supported compression and mode
+ // @return {@code null} on invalid data or no supported string segments
+ private static String parseMultipleStringStructure(byte[] data, int offset, int limit) {
+ if (limit < offset + 8) {
+ Log.e(TAG, "parseMultipleStringStructure given too little data");
+ return null;
+ }
+
+ int numStrings = data[offset] & 0xff;
+ if (numStrings <= 0) {
+ Log.e(TAG, "parseMultipleStringStructure found no strings");
+ return null;
+ }
+ int pos = offset + 1;
+ for (int i = 0; i < numStrings; i++) {
+ if (limit < pos + 4) {
+ Log.e(TAG, "parseMultipleStringStructure ran out of data");
+ return null;
+ }
+ int numSegments = data[pos + 3] & 0xff;
+ pos += 4;
+ for (int j = 0; j < numSegments; j++) {
+ if (limit < pos + 3) {
+ Log.e(TAG, "parseMultipleStringStructure ran out of data");
+ return null;
+ }
+ int compressionType = data[pos] & 0xff;
+ int mode = data[pos + 1] & 0xff;
+ int numBytes = data[pos + 2] & 0xff;
+ pos += 3;
+ if (data.length < pos + numBytes) {
+ Log.e(TAG, "parseMultipleStringStructure ran out of data");
+ return null;
+ }
+ if (compressionType == COMPRESSION_TYPE_NO_COMPRESSION && mode == MODE_UTF16) {
+ return new String(data, pos, numBytes, StandardCharsets.UTF_16);
+ }
+ pos += numBytes;
+ }
+ }
+
+ Log.e(TAG, "parseMultipleStringStructure found no supported segments");
+ return null;
+ }
+
+ private static boolean checkValidPsipSection(byte[] data) {
+ if (data.length < 13) {
+ Log.e(TAG, "Section was too small");
+ return false;
+ }
+ if ((data[0] & 0xff) == 0xff) {
+ // Should clear stuffing bytes as detailed by H222.0 section 2.4.4.
+ Log.e(TAG, "Unexpected stuffing bytes while parsing section");
+ return false;
+ }
+ int sectionLength = (((data[1] & 0x0f) << 8) | (data[2] & 0xff)) + 3;
+ if (sectionLength != data.length) {
+ Log.e(TAG, "Length mismatch while parsing section");
+ return false;
+ }
+ int sectionNumber = data[6] & 0xff;
+ int lastSectionNumber = data[7] & 0xff;
+ if(sectionNumber > lastSectionNumber) {
+ Log.e(TAG, "Found sectionNumber > lastSectionNumber while parsing section");
+ return false;
+ }
+ // TODO: Check CRC 32/MPEG for validity
+ return true;
+ }
+
+ // Contains the portion of the data contained in the TVCT used by
+ // our SampleTunerTvInputSetupActivity
+ public static class TvctChannelInfo {
+ private final String mChannelName;
+ private final int mMajorChannelNumber;
+ private final int mMinorChannelNumber;
+
+ public TvctChannelInfo(
+ String channelName,
+ int majorChannelNumber,
+ int minorChannelNumber) {
+ mChannelName = channelName;
+ mMajorChannelNumber = majorChannelNumber;
+ mMinorChannelNumber = minorChannelNumber;
+ }
+
+ public String getChannelName() {
+ return mChannelName;
+ }
+
+ public int getMajorChannelNumber() {
+ return mMajorChannelNumber;
+ }
+
+ public int getMinorChannelNumber() {
+ return mMinorChannelNumber;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ Locale.US,
+ "ChannelName: %s ChannelNumber: %d-%d",
+ mChannelName,
+ mMajorChannelNumber,
+ mMinorChannelNumber);
+ }
+ }
+
+ /**
+ * Contains the portion of the data contained in the EIT used by
+ * our SampleTunerTvInputService
+ */
+ public static class EitEventInfo {
+ private final String mEventTitle;
+ private final int mLengthSeconds;
+
+ public EitEventInfo(
+ String eventTitle,
+ int lengthSeconds) {
+ mEventTitle = eventTitle;
+ mLengthSeconds = lengthSeconds;
+ }
+
+ public String getEventTitle() {
+ return mEventTitle;
+ }
+
+ public int getLengthSeconds() {
+ return mLengthSeconds;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ Locale.US,
+ "Event Title: %s Length in Seconds: %d",
+ mEventTitle,
+ mLengthSeconds);
+ }
+ }
+
+ /**
+ * A base class for TS descriptors
+ * For details of their structure, see ATSC A/65 Section 6.9
+ */
+ public abstract static class TsDescriptor {
+ public abstract int getTag();
+ }
+
+ public static class ExtendedChannelNameDescriptor extends TsDescriptor {
+ private final String mLongChannelName;
+
+ public ExtendedChannelNameDescriptor(String longChannelName) {
+ mLongChannelName = longChannelName;
+ }
+
+ @Override
+ public int getTag() {
+ return DESCRIPTOR_TAG_EXTENDED_CHANNEL_NAME;
+ }
+
+ public String getLongChannelName() {
+ return mLongChannelName;
+ }
+ }
+}
diff --git a/tuner/sampletunertvinput/src/com/android/tv/samples/sampletunertvinput/SampleTunerTvInputService.java b/tuner/sampletunertvinput/src/com/android/tv/samples/sampletunertvinput/SampleTunerTvInputService.java
index 03e79650..d59ccd9d 100644
--- a/tuner/sampletunertvinput/src/com/android/tv/samples/sampletunertvinput/SampleTunerTvInputService.java
+++ b/tuner/sampletunertvinput/src/com/android/tv/samples/sampletunertvinput/SampleTunerTvInputService.java
@@ -1,34 +1,31 @@
package com.android.tv.samples.sampletunertvinput;
+import static android.media.tv.TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING;
import static android.media.tv.TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN;
+import android.content.ContentUris;
+import android.content.ContentValues;
import android.content.Context;
import android.media.MediaCodec;
import android.media.MediaCodec.BufferInfo;
-import android.media.MediaCodec.LinearBlock;
import android.media.MediaFormat;
+import android.media.tv.TvContract;
import android.media.tv.tuner.dvr.DvrPlayback;
import android.media.tv.tuner.dvr.DvrSettings;
-import android.media.tv.tuner.filter.AvSettings;
import android.media.tv.tuner.filter.Filter;
import android.media.tv.tuner.filter.FilterCallback;
import android.media.tv.tuner.filter.FilterEvent;
import android.media.tv.tuner.filter.MediaEvent;
-import android.media.tv.tuner.filter.TsFilterConfiguration;
-import android.media.tv.tuner.frontend.AtscFrontendSettings;
-import android.media.tv.tuner.frontend.DvbtFrontendSettings;
-import android.media.tv.tuner.frontend.FrontendSettings;
-import android.media.tv.tuner.frontend.OnTuneEventListener;
import android.media.tv.tuner.Tuner;
import android.media.tv.TvInputService;
+import android.media.tv.tuner.filter.SectionEvent;
import android.net.Uri;
import android.os.Handler;
-import android.os.HandlerExecutor;
-import android.os.ParcelFileDescriptor;
import android.util.Log;
import android.view.Surface;
-import java.io.File;
-import java.io.FileNotFoundException;
+
+import com.android.tv.common.util.Clock;
+
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayDeque;
@@ -42,40 +39,31 @@ public class SampleTunerTvInputService extends TvInputService {
private static final String TAG = "SampleTunerTvInput";
private static final boolean DEBUG = true;
- private static final int AUDIO_TPID = 257;
- private static final int VIDEO_TPID = 256;
- private static final int STATUS_MASK = 0xf;
- private static final int LOW_THRESHOLD = 0x1000;
- private static final int HIGH_THRESHOLD = 0x07fff;
- private static final int FREQUENCY = 578000;
- private static final int FILTER_BUFFER_SIZE = 16000000;
- private static final int DVR_BUFFER_SIZE = 4000000;
- private static final int INPUT_FILE_MAX_SIZE = 700000;
- private static final int PACKET_SIZE = 188;
-
private static final int TIMEOUT_US = 100000;
private static final boolean SAVE_DATA = false;
- private static final String ES_FILE_NAME = "test.es";
+ private static final boolean USE_DVR = true;
+ private static final String MEDIA_INPUT_FILE_NAME = "media.ts";
private static final MediaFormat VIDEO_FORMAT;
static {
// format extracted for the specific input file
- VIDEO_FORMAT = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, 320, 240);
+ VIDEO_FORMAT = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, 480, 360);
VIDEO_FORMAT.setInteger(MediaFormat.KEY_TRACK_ID, 1);
- VIDEO_FORMAT.setLong(MediaFormat.KEY_DURATION, 9933333);
- VIDEO_FORMAT.setInteger(MediaFormat.KEY_LEVEL, 32);
+ VIDEO_FORMAT.setLong(MediaFormat.KEY_DURATION, 10000000);
+ VIDEO_FORMAT.setInteger(MediaFormat.KEY_LEVEL, 256);
VIDEO_FORMAT.setInteger(MediaFormat.KEY_PROFILE, 65536);
ByteBuffer csd = ByteBuffer.wrap(
- new byte[] {0, 0, 0, 1, 103, 66, -64, 20, -38, 5, 7, -24, 64, 0, 0, 3, 0, 64, 0,
- 0, 15, 35, -59, 10, -88});
+ new byte[] {0, 0, 0, 1, 103, 66, -64, 30, -39, 1, -32, -65, -27, -64, 68, 0, 0, 3,
+ 0, 4, 0, 0, 3, 0, -16, 60, 88, -71, 32});
VIDEO_FORMAT.setByteBuffer("csd-0", csd);
- csd = ByteBuffer.wrap(new byte[] {0, 0, 0, 1, 104, -50, 60, -128});
+ csd = ByteBuffer.wrap(new byte[] {0, 0, 0, 1, 104, -53, -125, -53, 32});
VIDEO_FORMAT.setByteBuffer("csd-1", csd);
}
public static final String INPUT_ID =
"com.android.tv.samples.sampletunertvinput/.SampleTunerTvInputService";
private String mSessionId;
+ private Uri mChannelUri;
@Override
public TvInputSessionImpl onCreateSession(String inputId, String sessionId) {
@@ -89,6 +77,9 @@ public class SampleTunerTvInputService extends TvInputService {
@Override
public TvInputSessionImpl onCreateSession(String inputId) {
+ if (DEBUG) {
+ Log.d(TAG, "onCreateSession(inputId=" + inputId + ")");
+ }
return new TvInputSessionImpl(this);
}
@@ -100,12 +91,16 @@ public class SampleTunerTvInputService extends TvInputService {
private Surface mSurface;
private Filter mAudioFilter;
private Filter mVideoFilter;
+ private Filter mSectionFilter;
private DvrPlayback mDvr;
private Tuner mTuner;
private MediaCodec mMediaCodec;
private Thread mDecoderThread;
- private Deque<MediaEvent> mDataQueue;
- private List<MediaEvent> mSavedData;
+ private Deque<MediaEventData> mDataQueue;
+ private List<MediaEventData> mSavedData;
+ private long mCurrentLoopStartTimeUs = 0;
+ private long mLastFramePtsUs = 0;
+ private boolean mVideoAvailable;
private boolean mDataReady = false;
@@ -133,6 +128,9 @@ public class SampleTunerTvInputService extends TvInputService {
if (mVideoFilter != null) {
mVideoFilter.close();
}
+ if (mSectionFilter != null) {
+ mSectionFilter.close();
+ }
if (mDvr != null) {
mDvr.close();
mDvr = null;
@@ -170,7 +168,11 @@ public class SampleTunerTvInputService extends TvInputService {
Log.e(TAG, "null codec!");
return false;
}
+ mChannelUri = uri;
mHandler = new Handler();
+ mVideoAvailable = false;
+ notifyVideoUnavailable(VIDEO_UNAVAILABLE_REASON_TUNING);
+
mDecoderThread =
new Thread(
this::decodeInternal,
@@ -186,139 +188,79 @@ public class SampleTunerTvInputService extends TvInputService {
}
}
- private Filter audioFilter() {
- Filter audioFilter = mTuner.openFilter(Filter.TYPE_TS, Filter.SUBTYPE_AUDIO,
- FILTER_BUFFER_SIZE, new HandlerExecutor(mHandler),
- new FilterCallback() {
- @Override
- public void onFilterEvent(Filter filter, FilterEvent[] events) {
- if (DEBUG) {
- Log.d(TAG, "onFilterEvent audio, size=" + events.length);
- }
- for (int i = 0; i < events.length; i++) {
- if (DEBUG) {
- Log.d(TAG, "events[" + i + "] is "
- + events[i].getClass().getSimpleName());
- }
- }
+ private FilterCallback videoFilterCallback() {
+ return new FilterCallback() {
+ @Override
+ public void onFilterEvent(Filter filter, FilterEvent[] events) {
+ if (DEBUG) {
+ Log.d(TAG, "onFilterEvent video, size=" + events.length);
+ }
+ for (int i = 0; i < events.length; i++) {
+ if (DEBUG) {
+ Log.d(TAG, "events[" + i + "] is "
+ + events[i].getClass().getSimpleName());
}
+ if (events[i] instanceof MediaEvent) {
+ MediaEvent me = (MediaEvent) events[i];
- @Override
- public void onFilterStatusChanged(Filter filter, int status) {
- if (DEBUG) {
- Log.d(TAG, "onFilterEvent audio, status=" + status);
+ MediaEventData storedEvent = MediaEventData.generateEventData(me);
+ if (storedEvent == null) {
+ continue;
+ }
+ mDataQueue.add(storedEvent);
+ if (SAVE_DATA) {
+ mSavedData.add(storedEvent);
}
}
- });
- AvSettings settings =
- AvSettings.builder(Filter.TYPE_TS, true).setPassthrough(false).build();
- audioFilter.configure(
- TsFilterConfiguration.builder().setTpid(AUDIO_TPID)
- .setSettings(settings).build());
- return audioFilter;
+ }
+ }
+
+ @Override
+ public void onFilterStatusChanged(Filter filter, int status) {
+ if (DEBUG) {
+ Log.d(TAG, "onFilterEvent video, status=" + status);
+ }
+ if (status == Filter.STATUS_DATA_READY) {
+ mDataReady = true;
+ }
+ }
+ };
}
- private Filter videoFilter() {
- Filter videoFilter = mTuner.openFilter(Filter.TYPE_TS, Filter.SUBTYPE_VIDEO,
- FILTER_BUFFER_SIZE, new HandlerExecutor(mHandler),
- new FilterCallback() {
- @Override
- public void onFilterEvent(Filter filter, FilterEvent[] events) {
- if (DEBUG) {
- Log.d(TAG, "onFilterEvent video, size=" + events.length);
- }
- for (int i = 0; i < events.length; i++) {
- if (DEBUG) {
- Log.d(TAG, "events[" + i + "] is "
- + events[i].getClass().getSimpleName());
- }
- if (events[i] instanceof MediaEvent) {
- MediaEvent me = (MediaEvent) events[i];
- mDataQueue.add(me);
- if (SAVE_DATA) {
- mSavedData.add(me);
- }
- }
- }
+ private FilterCallback sectionFilterCallback() {
+ return new FilterCallback() {
+ @Override
+ public void onFilterEvent(Filter filter, FilterEvent[] events) {
+ if (DEBUG) {
+ Log.d(TAG, "onFilterEvent section, size=" + events.length);
+ }
+ for (int i = 0; i < events.length; i++) {
+ if (DEBUG) {
+ Log.d(TAG, "events[" + i + "] is "
+ + events[i].getClass().getSimpleName());
}
-
- @Override
- public void onFilterStatusChanged(Filter filter, int status) {
+ if (events[i] instanceof SectionEvent) {
+ SectionEvent sectionEvent = (SectionEvent) events[i];
+ int dataSize = (int)sectionEvent.getDataLengthLong();
if (DEBUG) {
- Log.d(TAG, "onFilterEvent video, status=" + status);
- }
- if (status == Filter.STATUS_DATA_READY) {
- mDataReady = true;
+ Log.d(TAG, "section dataSize:" + dataSize);
}
- }
- });
- AvSettings settings =
- AvSettings.builder(Filter.TYPE_TS, false).setPassthrough(false).build();
- videoFilter.configure(
- TsFilterConfiguration.builder().setTpid(VIDEO_TPID)
- .setSettings(settings).build());
- return videoFilter;
- }
- private DvrPlayback dvrPlayback() {
- DvrPlayback dvr = mTuner.openDvrPlayback(DVR_BUFFER_SIZE, new HandlerExecutor(mHandler),
- status -> {
- if (DEBUG) {
- Log.d(TAG, "onPlaybackStatusChanged status=" + status);
+ byte[] data = new byte[dataSize];
+ filter.read(data, 0, dataSize);
+
+ handleSection(data);
}
- });
- int res = dvr.configure(
- DvrSettings.builder()
- .setStatusMask(STATUS_MASK)
- .setLowThreshold(LOW_THRESHOLD)
- .setHighThreshold(HIGH_THRESHOLD)
- .setDataFormat(DvrSettings.DATA_FORMAT_ES)
- .setPacketSize(PACKET_SIZE)
- .build());
- if (DEBUG) {
- Log.d(TAG, "config res=" + res);
- }
- String testFile = mContext.getFilesDir().getAbsolutePath() + "/" + ES_FILE_NAME;
- File file = new File(testFile);
- if (file.exists()) {
- try {
- dvr.setFileDescriptor(
- ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_WRITE));
- } catch (FileNotFoundException e) {
- Log.e(TAG, "Failed to create FD");
+ }
}
- } else {
- Log.w(TAG, "File not existing");
- }
- return dvr;
- }
- private void tune() {
- DvbtFrontendSettings feSettings = DvbtFrontendSettings.builder()
- .setFrequency(FREQUENCY)
- .setTransmissionMode(DvbtFrontendSettings.TRANSMISSION_MODE_AUTO)
- .setBandwidth(DvbtFrontendSettings.BANDWIDTH_8MHZ)
- .setConstellation(DvbtFrontendSettings.CONSTELLATION_AUTO)
- .setHierarchy(DvbtFrontendSettings.HIERARCHY_AUTO)
- .setHighPriorityCodeRate(DvbtFrontendSettings.CODERATE_AUTO)
- .setLowPriorityCodeRate(DvbtFrontendSettings.CODERATE_AUTO)
- .setGuardInterval(DvbtFrontendSettings.GUARD_INTERVAL_AUTO)
- .setHighPriority(true)
- .setStandard(DvbtFrontendSettings.STANDARD_T)
- .build();
- mTuner.setOnTuneEventListener(new HandlerExecutor(mHandler), new OnTuneEventListener() {
@Override
- public void onTuneEvent(int tuneEvent) {
- if (DEBUG) {
- Log.d(TAG, "onTuneEvent " + tuneEvent);
- }
- long read = mDvr.read(INPUT_FILE_MAX_SIZE);
+ public void onFilterStatusChanged(Filter filter, int status) {
if (DEBUG) {
- Log.d(TAG, "read=" + read);
+ Log.d(TAG, "onFilterStatusChanged section, status=" + status);
}
}
- });
- mTuner.tune(feSettings);
+ };
}
private boolean initCodec() {
@@ -335,6 +277,7 @@ public class SampleTunerTvInputService extends TvInputService {
if (mMediaCodec == null) {
Log.e(TAG, "null codec!");
+ mVideoAvailable = false;
notifyVideoUnavailable(VIDEO_UNAVAILABLE_REASON_UNKNOWN);
return false;
}
@@ -347,14 +290,26 @@ public class SampleTunerTvInputService extends TvInputService {
mTuner = new Tuner(mContext, mSessionId,
TvInputService.PRIORITY_HINT_USE_CASE_TYPE_LIVE);
- mAudioFilter = audioFilter();
- mVideoFilter = videoFilter();
+ mAudioFilter = SampleTunerTvInputUtils.createAvFilter(mTuner, mHandler,
+ SampleTunerTvInputUtils.createDefaultLoggingFilterCallback("audio"), true);
+ mVideoFilter = SampleTunerTvInputUtils.createAvFilter(mTuner, mHandler,
+ videoFilterCallback(), false);
+ mSectionFilter = SampleTunerTvInputUtils.createSectionFilter(mTuner, mHandler,
+ sectionFilterCallback());
mAudioFilter.start();
mVideoFilter.start();
- // use dvr playback to feed the data on platform without physical tuner
- mDvr = dvrPlayback();
- tune();
- mDvr.start();
+ mSectionFilter.start();
+
+ // Dvr Playback can be used to read a file instead of relying on physical tuner
+ if (USE_DVR) {
+ mDvr = SampleTunerTvInputUtils.configureDvrPlayback(mTuner, mHandler,
+ DvrSettings.DATA_FORMAT_TS);
+ SampleTunerTvInputUtils.readFilePlaybackInput(getApplicationContext(), mDvr,
+ MEDIA_INPUT_FILE_NAME);
+ mDvr.start();
+ } else {
+ SampleTunerTvInputUtils.tune(mTuner, mHandler);
+ }
mMediaCodec.start();
try {
@@ -369,7 +324,10 @@ public class SampleTunerTvInputService extends TvInputService {
mDataQueue.pollFirst();
}
}
- if (SAVE_DATA) {
+ else if (SAVE_DATA) {
+ if (DEBUG) {
+ Log.d(TAG, "Adding saved data to data queue");
+ }
mDataQueue.addAll(mSavedData);
}
}
@@ -378,24 +336,50 @@ public class SampleTunerTvInputService extends TvInputService {
}
}
- private boolean handleDataBuffer(MediaEvent mediaEvent) {
- if (mediaEvent.getLinearBlock() == null) {
- if (DEBUG) Log.d(TAG, "getLinearBlock() == null");
- return true;
+ private void handleSection(byte[] data) {
+ SampleTunerTvInputSectionParser.EitEventInfo eventInfo =
+ SampleTunerTvInputSectionParser.parseEitSection(data);
+ if (eventInfo == null) {
+ Log.e(TAG, "Did not receive event info from parser");
+ return;
+ }
+
+ // We assume that our program starts at the current time
+ long startTimeMs = Clock.SYSTEM.currentTimeMillis();
+ long endTimeMs = startTimeMs + ((long)eventInfo.getLengthSeconds() * 1000);
+
+ // Remove any other programs which conflict with our start and end time
+ Uri conflictsUri =
+ TvContract.buildProgramsUriForChannel(mChannelUri, startTimeMs, endTimeMs);
+ int programsDeleted = mContext.getContentResolver().delete(conflictsUri, null, null);
+ if (DEBUG) {
+ Log.d(TAG, "Deleted " + programsDeleted + " conflicting program(s)");
+ }
+
+ // Insert our new program into the newly opened time slot
+ ContentValues values = new ContentValues();
+ values.put(TvContract.Programs.COLUMN_CHANNEL_ID, ContentUris.parseId(mChannelUri));
+ values.put(TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS, startTimeMs);
+ values.put(TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS, endTimeMs);
+ values.put(TvContract.Programs.COLUMN_TITLE, eventInfo.getEventTitle());
+ values.put(TvContract.Programs.COLUMN_SHORT_DESCRIPTION, "");
+ if (DEBUG) {
+ Log.d(TAG, "Inserting program with values: " + values);
}
+ mContext.getContentResolver().insert(TvContract.Programs.CONTENT_URI, values);
+ }
+
+ private boolean handleDataBuffer(MediaEventData mediaEventData) {
boolean success = false;
- LinearBlock block = mediaEvent.getLinearBlock();
- if (queueCodecInputBuffer(block, mediaEvent.getDataLength(), mediaEvent.getOffset(),
- mediaEvent.getPts())) {
+ if (queueCodecInputBuffer(mediaEventData.getData(), mediaEventData.getDataSize(),
+ mediaEventData.getPts())) {
releaseCodecOutputBuffer();
success = true;
}
- mediaEvent.release();
return success;
}
- private boolean queueCodecInputBuffer(LinearBlock block, long sampleSize,
- long offset, long pts) {
+ private boolean queueCodecInputBuffer(byte[] data, int size, long pts) {
int res = mMediaCodec.dequeueInputBuffer(TIMEOUT_US);
if (res >= 0) {
ByteBuffer buffer = mMediaCodec.getInputBuffer(res);
@@ -403,41 +387,19 @@ public class SampleTunerTvInputService extends TvInputService {
throw new RuntimeException("Null decoder input buffer");
}
- ByteBuffer data = block.map();
- if (offset > 0 && offset < data.limit()) {
- data.position((int) offset);
- } else {
- data.position(0);
- }
-
if (DEBUG) {
Log.d(
TAG,
"Decoder: Send data to decoder."
- + " Sample size="
- + sampleSize
+ " pts="
+ pts
- + " limit="
- + data.limit()
- + " pos="
- + data.position()
+ " size="
- + (data.limit() - data.position()));
+ + size);
}
// fill codec input buffer
- int size = sampleSize > data.limit() ? data.limit() : (int) sampleSize;
- if (DEBUG) Log.d(TAG, "limit " + data.limit() + " sampleSize " + sampleSize);
- if (data.hasArray()) {
- Log.d(TAG, "hasArray");
- buffer.put(data.array(), 0, size);
- } else {
- byte[] array = new byte[size];
- data.get(array, 0, size);
- buffer.put(array, 0, size);
- }
+ buffer.put(data, 0, size);
- mMediaCodec.queueInputBuffer(res, 0, (int) sampleSize, pts, 0);
+ mMediaCodec.queueInputBuffer(res, 0, size, pts, 0);
} else {
if (DEBUG) Log.d(TAG, "queueCodecInputBuffer res=" + res);
return false;
@@ -450,10 +412,43 @@ public class SampleTunerTvInputService extends TvInputService {
BufferInfo bufferInfo = new BufferInfo();
int res = mMediaCodec.dequeueOutputBuffer(bufferInfo, TIMEOUT_US);
if (res >= 0) {
- mMediaCodec.releaseOutputBuffer(res, true);
- notifyVideoAvailable();
+ long currentFramePtsUs = bufferInfo.presentationTimeUs;
+
+ // We know we are starting a new loop if the loop time is not set or if
+ // the current frame is before the last frame
+ if (mCurrentLoopStartTimeUs == 0 || currentFramePtsUs < mLastFramePtsUs) {
+ mCurrentLoopStartTimeUs = System.nanoTime() / 1000;
+ }
+ mLastFramePtsUs = currentFramePtsUs;
+
+ long desiredUs = mCurrentLoopStartTimeUs + currentFramePtsUs;
+ long nowUs = System.nanoTime() / 1000;
+ long sleepTimeUs = desiredUs - nowUs;
+
if (DEBUG) {
- Log.d(TAG, "notifyVideoAvailable");
+ Log.d(TAG, "currentFramePts: " + currentFramePtsUs
+ + " sleeping for: " + sleepTimeUs);
+ }
+ if (sleepTimeUs > 0) {
+ try {
+ Thread.sleep(
+ /* millis */ sleepTimeUs / 1000,
+ /* nanos */ (int) (sleepTimeUs % 1000) * 1000);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ if (DEBUG) {
+ Log.d(TAG, "InterruptedException:\n" + Log.getStackTraceString(e));
+ }
+ return;
+ }
+ }
+ mMediaCodec.releaseOutputBuffer(res, true);
+ if (!mVideoAvailable) {
+ mVideoAvailable = true;
+ notifyVideoAvailable();
+ if (DEBUG) {
+ Log.d(TAG, "notifyVideoAvailable");
+ }
}
} else if (res == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
MediaFormat format = mMediaCodec.getOutputFormat();
@@ -472,4 +467,75 @@ public class SampleTunerTvInputService extends TvInputService {
}
}
+
+ /**
+ * MediaEventData is a helper class which is used to hold the data within MediaEvents
+ * locally in our Java code, instead of in the position allocated by our native code
+ */
+ public static class MediaEventData {
+ private final long mPts;
+ private final int mDataSize;
+ private final byte[] mData;
+
+ public MediaEventData(long pts, int dataSize, byte[] data) {
+ mPts = pts;
+ mDataSize = dataSize;
+ mData = data;
+ }
+
+ /**
+ * Parses a MediaEvent, including copying its data and freeing the underlying LinearBlock
+ * @return {@code null} if the event has no LinearBlock
+ */
+ public static MediaEventData generateEventData(MediaEvent event) {
+ if(event.getLinearBlock() == null) {
+ if (DEBUG) {
+ Log.d(TAG, "MediaEvent had null LinearBlock");
+ }
+ return null;
+ }
+
+ ByteBuffer memoryBlock = event.getLinearBlock().map();
+ int eventOffset = (int)event.getOffset();
+ int eventDataLength = (int)event.getDataLength();
+ if (DEBUG) {
+ Log.d(TAG, "MediaEvent has length=" + eventDataLength
+ + " offset=" + eventOffset
+ + " capacity=" + memoryBlock.capacity()
+ + " limit=" + memoryBlock.limit());
+ }
+ if (eventOffset < 0 || eventDataLength < 0 || eventOffset >= memoryBlock.limit()) {
+ if (DEBUG) {
+ Log.e(TAG, "MediaEvent length or offset was invalid");
+ }
+ event.getLinearBlock().recycle();
+ event.release();
+ return null;
+ }
+ // We allow the case of eventOffset + eventDataLength > memoryBlock.limit()
+ // When it occurs, we read until memoryBlock.limit
+ int dataSize = Math.min(eventDataLength, memoryBlock.limit() - eventOffset);
+ memoryBlock.position(eventOffset);
+
+ byte[] memoryData = new byte[dataSize];
+ memoryBlock.get(memoryData, 0, dataSize);
+ MediaEventData eventData = new MediaEventData(event.getPts(), dataSize, memoryData);
+
+ event.getLinearBlock().recycle();
+ event.release();
+ return eventData;
+ }
+
+ public long getPts() {
+ return mPts;
+ }
+
+ public int getDataSize() {
+ return mDataSize;
+ }
+
+ public byte[] getData() {
+ return mData;
+ }
+ }
}
diff --git a/tuner/sampletunertvinput/src/com/android/tv/samples/sampletunertvinput/SampleTunerTvInputSetupActivity.java b/tuner/sampletunertvinput/src/com/android/tv/samples/sampletunertvinput/SampleTunerTvInputSetupActivity.java
index b932b605..4774243e 100644
--- a/tuner/sampletunertvinput/src/com/android/tv/samples/sampletunertvinput/SampleTunerTvInputSetupActivity.java
+++ b/tuner/sampletunertvinput/src/com/android/tv/samples/sampletunertvinput/SampleTunerTvInputSetupActivity.java
@@ -3,48 +3,158 @@ package com.android.tv.samples.sampletunertvinput;
import android.app.Activity;
import android.content.Intent;
import android.media.tv.TvInputInfo;
+import android.media.tv.TvInputService;
+import android.media.tv.tuner.Tuner;
+import android.media.tv.tuner.dvr.DvrPlayback;
+import android.media.tv.tuner.dvr.DvrSettings;
+import android.media.tv.tuner.filter.Filter;
+import android.media.tv.tuner.filter.FilterCallback;
+import android.media.tv.tuner.filter.FilterEvent;
+import android.media.tv.tuner.filter.SectionEvent;
import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+
+import com.android.tv.common.util.Clock;
import com.android.tv.testing.data.ChannelInfo;
import com.android.tv.testing.data.ChannelUtils;
import com.android.tv.testing.data.ProgramInfo;
+import com.android.tv.testing.data.ProgramUtils;
+
import java.util.Collections;
+import java.util.Locale;
+import java.util.concurrent.TimeUnit;
/** Setup activity for SampleTunerTvInput */
public class SampleTunerTvInputSetupActivity extends Activity {
+ private static final String TAG = "SampleTunerTvInput";
+ private static final boolean DEBUG = true;
+
+ private static final boolean USE_DVR = true;
+ private static final String SETUP_INPUT_FILE_NAME = "setup.ts";
+
+ private Tuner mTuner;
+ private DvrPlayback mDvr;
+ private Filter mSectionFilter;
+
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+ initTuner();
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ if (mTuner != null) {
+ mTuner.close();
+ mTuner = null;
+ }
+ if (mDvr != null) {
+ mDvr.close();
+ mDvr = null;
+ }
+ if (mSectionFilter != null) {
+ mSectionFilter.close();
+ mSectionFilter = null;
+ }
+ }
+
+ private void setChannel(byte[] sectionData) {
+ SampleTunerTvInputSectionParser.TvctChannelInfo channelInfo =
+ SampleTunerTvInputSectionParser.parseTvctSection(sectionData);
+
+ String channelNumber = "";
+ String channelName = "";
+
+ if(channelInfo == null) {
+ Log.e(TAG, "Did not receive channel description from parser");
+ } else {
+ channelNumber = String.format(Locale.US, "%d-%d", channelInfo.getMajorChannelNumber(),
+ channelInfo.getMinorChannelNumber());
+ channelName = channelInfo.getChannelName();
+ }
+
ChannelInfo channel =
- new ChannelInfo.Builder()
- .setNumber("1-1")
- .setName("Sample Channel")
- .setLogoUrl(
- ChannelInfo.getUriStringForChannelLogo(this, 100))
- .setOriginalNetworkId(1)
- .setVideoWidth(640)
- .setVideoHeight(480)
- .setAudioChannel(2)
- .setAudioLanguageCount(1)
- .setHasClosedCaption(false)
- .setProgram(
- new ProgramInfo(
- "Sample Program",
- "",
- 0,
- 0,
- ProgramInfo.GEN_POSTER,
- "Sample description",
- ProgramInfo.GEN_DURATION,
- null,
- ProgramInfo.GEN_GENRE,
- null))
- .build();
+ new ChannelInfo.Builder()
+ .setNumber(channelNumber)
+ .setName(channelName)
+ .setLogoUrl(
+ ChannelInfo.getUriStringForChannelLogo(this, 100))
+ .setOriginalNetworkId(1)
+ .setVideoWidth(640)
+ .setVideoHeight(480)
+ .setAudioChannel(2)
+ .setAudioLanguageCount(1)
+ .setHasClosedCaption(false)
+ .build();
Intent intent = getIntent();
String inputId = intent.getStringExtra(TvInputInfo.EXTRA_INPUT_ID);
ChannelUtils.updateChannels(this, inputId, Collections.singletonList(channel));
+ ProgramUtils.updateProgramForAllChannelsOf(this, inputId, Clock.SYSTEM,
+ TimeUnit.DAYS.toMillis(1));
+
setResult(Activity.RESULT_OK);
finish();
}
+ private FilterCallback sectionFilterCallback() {
+ return new FilterCallback() {
+ @Override
+ public void onFilterEvent(Filter filter, FilterEvent[] events) {
+ if (DEBUG) {
+ Log.d(TAG, "onFilterEvent setup section, size=" + events.length);
+ }
+ for (int i = 0; i < events.length; i++) {
+ if (DEBUG) {
+ Log.d(TAG, "events[" + i + "] is "
+ + events[i].getClass().getSimpleName());
+ }
+ if (events[i] instanceof SectionEvent) {
+ SectionEvent sectionEvent = (SectionEvent) events[i];
+ int dataSize = (int)sectionEvent.getDataLengthLong();
+ if (DEBUG) {
+ Log.d(TAG, "section dataSize:" + dataSize);
+ }
+
+ byte[] data = new byte[dataSize];
+ filter.read(data, 0, dataSize);
+
+ setChannel(data);
+ }
+ }
+ }
+
+ @Override
+ public void onFilterStatusChanged(Filter filter, int status) {
+ if (DEBUG) {
+ Log.d(TAG, "onFilterStatusChanged setup section, status=" + status);
+ }
+ }
+ };
+ }
+
+ private void initTuner() {
+ mTuner = new Tuner(getApplicationContext(), null,
+ TvInputService.PRIORITY_HINT_USE_CASE_TYPE_LIVE);
+ Handler handler = new Handler(Looper.myLooper());
+
+ mSectionFilter = SampleTunerTvInputUtils.createSectionFilter(mTuner, handler,
+ sectionFilterCallback());
+ mSectionFilter.start();
+
+ // Dvr Playback can be used to read a file instead of relying on physical tuner
+ if (USE_DVR) {
+ mDvr = SampleTunerTvInputUtils.configureDvrPlayback(mTuner, handler,
+ DvrSettings.DATA_FORMAT_TS);
+ SampleTunerTvInputUtils.readFilePlaybackInput(getApplicationContext(), mDvr,
+ SETUP_INPUT_FILE_NAME);
+ mDvr.start();
+ } else {
+ SampleTunerTvInputUtils.tune(mTuner, handler);
+ }
+ }
+
}
diff --git a/tuner/sampletunertvinput/src/com/android/tv/samples/sampletunertvinput/SampleTunerTvInputUtils.java b/tuner/sampletunertvinput/src/com/android/tv/samples/sampletunertvinput/SampleTunerTvInputUtils.java
new file mode 100644
index 00000000..9638f33a
--- /dev/null
+++ b/tuner/sampletunertvinput/src/com/android/tv/samples/sampletunertvinput/SampleTunerTvInputUtils.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.samples.sampletunertvinput;
+
+import android.content.Context;
+import android.media.tv.tuner.Tuner;
+import android.media.tv.tuner.dvr.DvrPlayback;
+import android.media.tv.tuner.dvr.DvrSettings;
+import android.media.tv.tuner.filter.AvSettings;
+import android.media.tv.tuner.filter.Filter;
+import android.media.tv.tuner.filter.FilterCallback;
+import android.media.tv.tuner.filter.FilterEvent;
+import android.media.tv.tuner.filter.SectionSettingsWithSectionBits;
+import android.media.tv.tuner.filter.TsFilterConfiguration;
+import android.media.tv.tuner.frontend.DvbtFrontendSettings;
+import android.os.Handler;
+import android.os.HandlerExecutor;
+import android.os.ParcelFileDescriptor;
+import android.util.Log;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+
+public class SampleTunerTvInputUtils {
+ private static final String TAG = "SampleTunerTvInput";
+ private static final boolean DEBUG = true;
+
+ private static final int AUDIO_TPID = 257;
+ private static final int VIDEO_TPID = 256;
+ private static final int SECTION_TPID = 255;
+ private static final int FILTER_BUFFER_SIZE = 16000000;
+
+ private static final int STATUS_MASK = 0xf;
+ private static final int LOW_THRESHOLD = 0x1000;
+ private static final int HIGH_THRESHOLD = 0x07fff;
+ private static final int DVR_BUFFER_SIZE = 4000000;
+ private static final int PACKET_SIZE = 188;
+ private static final long FREQUENCY = 578000;
+ private static final int INPUT_FILE_MAX_SIZE = 1000000;
+
+ public static DvrPlayback configureDvrPlayback(Tuner tuner, Handler handler, int dataFormat) {
+ DvrPlayback dvr = tuner.openDvrPlayback(DVR_BUFFER_SIZE, new HandlerExecutor(handler),
+ status -> {
+ if (DEBUG) {
+ Log.d(TAG, "onPlaybackStatusChanged status=" + status);
+ }
+ });
+ int res = dvr.configure(
+ DvrSettings.builder()
+ .setStatusMask(STATUS_MASK)
+ .setLowThreshold(LOW_THRESHOLD)
+ .setHighThreshold(HIGH_THRESHOLD)
+ .setDataFormat(dataFormat)
+ .setPacketSize(PACKET_SIZE)
+ .build());
+ if (DEBUG) {
+ Log.d(TAG, "config res=" + res);
+ }
+ return dvr;
+ }
+
+ public static void readFilePlaybackInput(Context context, DvrPlayback dvr, String fileName) {
+ String testFile = context.getFilesDir().getAbsolutePath() + "/" + fileName;
+ File file = new File(testFile);
+ if (file.exists()) {
+ try {
+ dvr.setFileDescriptor(
+ ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_WRITE));
+ } catch (FileNotFoundException e) {
+ Log.e(TAG, "Failed to create FD");
+ }
+ } else {
+ Log.w(TAG, "File not existing");
+ }
+
+ long read = dvr.read(INPUT_FILE_MAX_SIZE);
+ if (DEBUG) {
+ Log.d(TAG, "read=" + read);
+ }
+ }
+
+ public static void tune(Tuner tuner, Handler handler) {
+ DvbtFrontendSettings feSettings = DvbtFrontendSettings.builder()
+ .setFrequencyLong(FREQUENCY)
+ .setTransmissionMode(DvbtFrontendSettings.TRANSMISSION_MODE_AUTO)
+ .setBandwidth(DvbtFrontendSettings.BANDWIDTH_8MHZ)
+ .setConstellation(DvbtFrontendSettings.CONSTELLATION_AUTO)
+ .setHierarchy(DvbtFrontendSettings.HIERARCHY_AUTO)
+ .setHighPriorityCodeRate(DvbtFrontendSettings.CODERATE_AUTO)
+ .setLowPriorityCodeRate(DvbtFrontendSettings.CODERATE_AUTO)
+ .setGuardInterval(DvbtFrontendSettings.GUARD_INTERVAL_AUTO)
+ .setHighPriority(true)
+ .setStandard(DvbtFrontendSettings.STANDARD_T)
+ .build();
+
+ tuner.setOnTuneEventListener(new HandlerExecutor(handler), tuneEvent -> {
+ if (DEBUG) {
+ Log.d(TAG, "onTuneEvent " + tuneEvent);
+ }
+ });
+
+ tuner.tune(feSettings);
+ }
+
+ public static Filter createSectionFilter(Tuner tuner, Handler handler,
+ FilterCallback callback) {
+ Filter sectionFilter = tuner.openFilter(Filter.TYPE_TS, Filter.SUBTYPE_SECTION,
+ FILTER_BUFFER_SIZE, new HandlerExecutor(handler), callback);
+
+ SectionSettingsWithSectionBits settings = SectionSettingsWithSectionBits
+ .builder(Filter.TYPE_TS).build();
+
+ sectionFilter.configure(
+ TsFilterConfiguration.builder().setTpid(SECTION_TPID)
+ .setSettings(settings).build());
+
+ return sectionFilter;
+ }
+
+ public static Filter createAvFilter(Tuner tuner, Handler handler,
+ FilterCallback callback, boolean isAudio) {
+ Filter avFilter = tuner.openFilter(Filter.TYPE_TS,
+ isAudio ? Filter.SUBTYPE_AUDIO : Filter.SUBTYPE_VIDEO,
+ FILTER_BUFFER_SIZE,
+ new HandlerExecutor(handler),
+ callback);
+
+ AvSettings settings =
+ AvSettings.builder(Filter.TYPE_TS, isAudio).setPassthrough(false).build();
+ avFilter.configure(
+ TsFilterConfiguration.builder().
+ setTpid(isAudio ? AUDIO_TPID : VIDEO_TPID)
+ .setSettings(settings).build());
+ return avFilter;
+ }
+
+ public static FilterCallback createDefaultLoggingFilterCallback(String filterType) {
+ return new FilterCallback() {
+ @Override
+ public void onFilterEvent(Filter filter, FilterEvent[] events) {
+ if (DEBUG) {
+ Log.d(TAG, "onFilterEvent " + filterType + ", size=" + events.length);
+ }
+ for (int i = 0; i < events.length; i++) {
+ if (DEBUG) {
+ Log.d(TAG, "events[" + i + "] is "
+ + events[i].getClass().getSimpleName());
+ }
+ }
+ }
+
+ @Override
+ public void onFilterStatusChanged(Filter filter, int status) {
+ if (DEBUG) {
+ Log.d(TAG, "onFilterStatusChanged " + filterType + ", status=" + status);
+ }
+ }
+ };
+ }
+}
diff --git a/tuner/tests/robotests/Android.bp b/tuner/tests/robotests/Android.bp
new file mode 100644
index 00000000..a1baf651
--- /dev/null
+++ b/tuner/tests/robotests/Android.bp
@@ -0,0 +1,47 @@
+//
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+ // See: http://go/android-license-faq
+ default_applicable_licenses: [
+ "Android-Apache-2.0",
+ ],
+}
+
+android_robolectric_test {
+ name: "TvTunerRoboTests",
+ srcs: [
+ "javatests/**/*.java",
+ ],
+ java_resource_dirs: ["config"],
+ static_libs: [
+ "tv-lib-dagger",
+ "androidx.test.core",
+ "tv-lib-dagger-android",
+ "tv-test-common",
+ "tv-test-common-robo",
+ "tv-tuner-testing",
+ ],
+ plugins: [
+ "tv-lib-dagger-android-processor",
+ "tv-lib-dagger-compiler",
+ ],
+ instrumentation_for: "LiveTv",
+ test_options: {
+ timeout: 36000,
+ },
+ upstream: true,
+}
diff --git a/tuner/tests/robotests/Android.mk b/tuner/tests/robotests/Android.mk
deleted file mode 100644
index bf2f24c8..00000000
--- a/tuner/tests/robotests/Android.mk
+++ /dev/null
@@ -1,72 +0,0 @@
-#############################################################
-# Tv Robolectric test target. #
-#############################################################
-LOCAL_PATH := $(call my-dir)
-include $(CLEAR_VARS)
-
-LOCAL_MODULE := TvTunerRoboTests
-LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
-LOCAL_LICENSE_CONDITIONS := notice
-LOCAL_MODULE_CLASS := JAVA_LIBRARIES
-
-LOCAL_SRC_FILES := $(call all-java-files-under, javatests)
-
-LOCAL_JAVA_LIBRARIES := \
- Robolectric_all-target \
- mockito-robolectric-prebuilt \
- robolectric_android-all-stub \
-
-LOCAL_STATIC_JAVA_LIBRARIES := \
- tv-lib-dagger
-
-LOCAL_STATIC_ANDROID_LIBRARIES := \
- androidx.test.core \
- tv-lib-dagger-android \
- tv-test-common \
- tv-test-common-robo \
- tv-tuner-testing \
-
-LOCAL_ANNOTATION_PROCESSORS := \
- tv-lib-dagger-android-processor \
- tv-lib-dagger-compiler \
-
-LOCAL_ANNOTATION_PROCESSOR_CLASSES := \
- dagger.internal.codegen.ComponentProcessor,dagger.android.processor.AndroidProcessor
-
-LOCAL_INSTRUMENTATION_FOR := LiveTv
-
-LOCAL_MODULE_TAGS := optional
-
-include $(BUILD_STATIC_JAVA_LIBRARY)
-
-#############################################################
-# Tv runner target to run the previous target. #
-#############################################################
-include $(CLEAR_VARS)
-LOCAL_MODULE := RunTvTunerRoboTests
-LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
-LOCAL_LICENSE_CONDITIONS := notice
-
-BASE_DIR = com/android/tv/tuner
-EXCLUDE_FILES := \
- $(BASE_DIR)/dvb/DvbTunerHalTest.java \
- $(BASE_DIR)/exoplayer/tests/SampleSourceExtractorTest.java \
-
-LOCAL_ROBOTEST_FILES := $(call find-files-in-subdirs,$(LOCAL_PATH)/javatests,*Test.java,.)
-LOCAL_ROBOTEST_FILES := $(filter-out $(EXCLUDE_FILES),$(LOCAL_ROBOTEST_FILES))
-
-LOCAL_JAVA_LIBRARIES := \
- Robolectric_all-target \
- TvTunerRoboTests \
- mockito-robolectric-prebuilt \
- robolectric_android-all-stub \
- tv-lib-truth \
- tv-test-common \
- tv-test-common-robo \
- tv-tuner-testing \
-
-LOCAL_TEST_PACKAGE := LiveTv
-
-LOCAL_ROBOTEST_TIMEOUT := 36000
-
-include external/robolectric-shadows/run_robotests.mk
diff --git a/tuner/tests/robotests/javatests/com/android/tv/tuner/testing/TvTunerRobolectricTestRunner.java b/tuner/tests/robotests/javatests/com/android/tv/tuner/testing/TvTunerRobolectricTestRunner.java
index 89b7510d..ab0955e5 100644
--- a/tuner/tests/robotests/javatests/com/android/tv/tuner/testing/TvTunerRobolectricTestRunner.java
+++ b/tuner/tests/robotests/javatests/com/android/tv/tuner/testing/TvTunerRobolectricTestRunner.java
@@ -44,7 +44,6 @@ public class TvTunerRobolectricTestRunner extends RobolectricTestRunner {
* We are going to create our own custom manifest so that we can add multiple resource paths to
* it.
*/
- @Override
protected AndroidManifest getAppManifest(Config config) {
final String packageName = "com.android.tv.tuner";
diff --git a/tuner/tests/testing/Android.bp b/tuner/tests/testing/Android.bp
new file mode 100644
index 00000000..ca482263
--- /dev/null
+++ b/tuner/tests/testing/Android.bp
@@ -0,0 +1,41 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+ // See: http://go/android-license-faq
+ default_applicable_licenses: [
+ "Android-Apache-2.0",
+ ],
+}
+
+android_library {
+ name: "tv-tuner-testing",
+ srcs: [
+ "src/**/*.java",
+ "src/**/I*.aidl",
+ ],
+ static_libs: [
+ "android-support-annotations",
+ "androidx.test.runner",
+ "tv-guava-android-jar",
+ "mockito-robolectric-prebuilt",
+ "tv-lib-truth",
+ "androidx.test.uiautomator_uiautomator",
+ ],
+ libs: ["tv-common", "live-tv-tuner"],
+ sdk_version: "system_current",
+ aidl: {
+ local_include_dirs: ["src"],
+ },
+}
diff --git a/tuner/tests/testing/Android.mk b/tuner/tests/testing/Android.mk
deleted file mode 100644
index 38f7342a..00000000
--- a/tuner/tests/testing/Android.mk
+++ /dev/null
@@ -1,30 +0,0 @@
-LOCAL_PATH:= $(call my-dir)
-include $(CLEAR_VARS)
-
-# Include all test java files.
-LOCAL_SRC_FILES := \
- $(call all-java-files-under, src) \
- $(call all-Iaidl-files-under, src)
-
-LOCAL_STATIC_JAVA_LIBRARIES := \
- android-support-annotations \
- androidx.test.runner \
- tv-guava-android-jar \
- mockito-robolectric-prebuilt \
- tv-lib-truth \
- ub-uiautomator \
-
-# Link tv-common as shared library to avoid the problem of initialization of the constants
-LOCAL_JAVA_LIBRARIES := tv-common
-
-LOCAL_INSTRUMENTATION_FOR := LiveTv
-LOCAL_MODULE := tv-tuner-testing
-LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
-LOCAL_LICENSE_CONDITIONS := notice
-LOCAL_MODULE_TAGS := optional
-LOCAL_SDK_VERSION := system_current
-
-LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
-LOCAL_AIDL_INCLUDES += $(LOCAL_PATH)/src
-
-include $(BUILD_STATIC_JAVA_LIBRARY)