aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2023-07-07 01:09:06 +0000
committerAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2023-07-07 01:09:06 +0000
commit36ae4fb5e302c8cb8cca3dce04e5f9bc4d886ec8 (patch)
tree08aaccf8fde3dc86be6785b36e8729cb56e9b47e
parent75f3637b6008ad84e26df589a7b39cb7d6e8da45 (diff)
parent489f952460ffe2ef1eb1348b2083bba4de88567f (diff)
downloadTV-android14-mainline-resolv-release.tar.gz
Snap for 10447354 from 489f952460ffe2ef1eb1348b2083bba4de88567f to mainline-resolv-releaseaml_res_341510000aml_res_341410010aml_res_341311030aml_res_341110000aml_res_340912000android14-mainline-resolv-release
Change-Id: I3f664153b67b1683e8c0649c6e21521efe9f729f
-rw-r--r--AndroidManifest.xml14
-rw-r--r--OWNERS1
-rw-r--r--common/src/com/android/tv/common/feature/Sdk.java2
-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
-rwxr-xr-xres/drawable/tv_iapp_dialog_background.xml21
-rw-r--r--res/layout/activity_tv.xml6
-rwxr-xr-xres/layout/tv_app_dialog.xml71
-rw-r--r--res/values/arrays-custom.xml13
-rw-r--r--res/values/colors.xml4
-rw-r--r--res/values/strings.xml8
-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.java82
-rw-r--r--src/com/android/tv/SetupPassthroughActivity.java2
-rw-r--r--src/com/android/tv/audiotvservice/AudioOnlyTvService.java7
-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/dvr/recorder/RecordingScheduler.java3
-rw-r--r--src/com/android/tv/features/TvFeatures.java3
-rw-r--r--src/com/android/tv/interactive/IAppManager.java428
-rw-r--r--src/com/android/tv/onboarding/OnboardingActivity.java2
-rw-r--r--src/com/android/tv/receiver/AudioCapabilitiesReceiver.java3
-rw-r--r--src/com/android/tv/receiver/BootCompletedReceiver.java5
-rw-r--r--src/com/android/tv/setup/SystemSetupActivity.java3
-rw-r--r--src/com/android/tv/ui/SelectInputView.java12
-rw-r--r--src/com/android/tv/ui/TunableTvView.java51
-rw-r--r--src/com/android/tv/ui/TvOverlayManager.java2
-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.java50
-rw-r--r--src/com/android/tv/util/TvSettings.java15
-rw-r--r--tests/robotests/src/com/android/tv/MediaSessionWrapperTest.java5
-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/src/com/android/tv/tuner/setup/BaseTunerSetupActivity.java4
54 files changed, 3148 insertions, 553 deletions
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 75e2c4d5..6a2d435f 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -20,9 +20,6 @@
xmlns:tools="http://schemas.android.com/tools"
package="com.android.tv">
- <uses-sdk android:minSdkVersion="23"
- android:targetSdkVersion="29"/>
-
<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"/>
@@ -30,6 +27,7 @@
<uses-permission android:name="android.permission.HDMI_CEC"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.MODIFY_PARENTAL_CONTROLS"/>
+ <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>
<uses-permission android:name="android.permission.READ_CONTENT_RATING_SYSTEMS"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_TV_LISTINGS"/>
@@ -153,6 +151,7 @@
android:resource="@xml/searchable"/>
</activity>
<activity android:name="com.android.tv.LauncherActivity"
+ android:exported="false"
android:configChanges="keyboard|keyboardHidden"
android:theme="@android:style/Theme.Translucent.NoTitleBar"/>
<activity android:name="com.android.tv.SetupPassthroughActivity"
@@ -166,6 +165,7 @@
</intent-filter>
</activity>
<activity android:name="com.android.tv.SelectInputActivity"
+ android:exported="true"
android:configChanges="keyboard|keyboardHidden"
android:launchMode="singleTask"
android:theme="@style/Theme.SelectInputActivity">
@@ -175,6 +175,7 @@
</intent-filter>
</activity>
<activity android:name="com.android.tv.onboarding.OnboardingActivity"
+ android:exported="false"
android:configChanges="keyboard|keyboardHidden"
android:launchMode="singleTop"
android:theme="@style/Theme.Setup.GuidedStep"/>
@@ -219,14 +220,18 @@
android:theme="@style/Theme.TV.Dvr.Series.Settings.GuidedStep"/>
<activity android:name="com.android.tv.dvr.ui.DvrSeriesSettingsActivity"
android:configChanges="keyboard|keyboardHidden"
+ android:exported="false"
android:theme="@style/Theme.TV.Dvr.Series.Settings.GuidedStep"/>
<activity android:name="com.android.tv.dvr.ui.DvrSeriesDeletionActivity"
android:configChanges="keyboard|keyboardHidden"
+ android:exported="false"
android:theme="@style/Theme.TV.Dvr.Series.Deletion.GuidedStep"/>
<activity android:name="com.android.tv.dvr.ui.DvrSeriesScheduledDialogActivity"
+ android:exported="false"
android:theme="@style/Theme.TV.dialog.HalfSizedDialog"/>
<activity android:name="com.android.tv.dvr.ui.list.DvrSchedulesActivity"
android:configChanges="keyboard|keyboardHidden"
+ android:exported="false"
android:theme="@style/Theme.Leanback.Details"/>
<activity android:name="com.android.tv.dvr.ui.list.DvrHistoryActivity"
android:configChanges="keyboard|keyboardHidden"
@@ -236,6 +241,7 @@
<service android:name="com.android.tv.recommendation.NotificationService"
android:exported="false"/>
<service android:name="com.android.tv.recommendation.ChannelPreviewUpdater$ChannelPreviewUpdateService"
+ android:exported="false"
android:permission="android.permission.BIND_JOB_SERVICE"/>
<receiver android:name="com.android.tv.receiver.BootCompletedReceiver"
@@ -272,12 +278,14 @@
</intent-filter>
</activity> <!-- DVR -->
<service android:name="com.android.tv.dvr.recorder.DvrRecordingService"
+ android:exported="false"
android:label="@string/dvr_service_name"/>
<receiver android:name="com.android.tv.dvr.recorder.DvrStartRecordingReceiver"
android:exported="false"/>
<service android:name="com.android.tv.data.epg.EpgFetchService"
+ android:exported="false"
android:permission="android.permission.BIND_JOB_SERVICE"/>
</application>
diff --git a/OWNERS b/OWNERS
index e904f5c6..a386cdaf 100644
--- a/OWNERS
+++ b/OWNERS
@@ -1,3 +1,2 @@
-nchalko@google.com
shubang@google.com
quxiangfang@google.com
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/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/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/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..f46d7b9b 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -160,4 +160,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/strings.xml b/res/values/strings.xml
index e272244d..b36827d1 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -1044,4 +1044,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 8dbafe47..cea293de 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;
@@ -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
}
}
@@ -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
@@ -717,8 +739,8 @@ public class MainActivity extends Activity
mAudioCapabilitiesReceiver = new AudioCapabilitiesReceiver(this, null);
mAudioCapabilitiesReceiver.register();
Intent nowPlayingIntent = new Intent(this, MainActivity.class);
- PendingIntent pendingIntent =
- PendingIntent.getActivity(this, REQUEST_CODE_NOW_PLAYING, nowPlayingIntent, 0);
+ PendingIntent pendingIntent = PendingIntent.getActivity(this, REQUEST_CODE_NOW_PLAYING,
+ nowPlayingIntent, PendingIntent.FLAG_IMMUTABLE);
mMediaSessionWrapper = new MediaSessionWrapper(this, pendingIntent);
mTvViewUiManager.restoreDisplayMode(false);
@@ -732,9 +754,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 +867,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);
@@ -1081,7 +1115,7 @@ public class MainActivity extends Activity
}
mTvView.start();
- mAudioManagerHelper.setVolumeByAudioFocusStatus();
+ mAudioManagerHelper.requestAudioFocus();
tune(true);
}
@@ -1126,6 +1160,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 +1175,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 +1462,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 +1671,7 @@ public class MainActivity extends Activity
}
}
- private void stopTv() {
+ public void stopTv() {
stopTv(null, false);
}
@@ -1932,12 +1972,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 +2508,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,7 +2956,7 @@ public class MainActivity extends Activity
}
applyDisplayRefreshRate(info.getVideoFrameRate());
mTvViewUiManager.updateTvAspectRatio();
- applyMultiAudio(
+ applyMultiAudio(allowAutoSelectionOfTrack,
allowAutoSelectionOfTrack ? null : getSelectedTrack(TvTrackInfo.TYPE_AUDIO));
applyClosedCaption();
mOverlayManager.getMenu().onStreamInfoChanged();
@@ -2989,6 +3038,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 +3104,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/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/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/dvr/recorder/RecordingScheduler.java b/src/com/android/tv/dvr/recorder/RecordingScheduler.java
index f309537d..475c17f8 100644
--- a/src/com/android/tv/dvr/recorder/RecordingScheduler.java
+++ b/src/com/android/tv/dvr/recorder/RecordingScheduler.java
@@ -322,7 +322,8 @@ public class RecordingScheduler extends TvInputCallback implements ScheduledReco
long wakeAt = nextStartTime - MS_TO_WAKE_BEFORE_START;
if (DEBUG) Log.d(TAG, "Set alarm to record at " + wakeAt);
Intent intent = new Intent(mContext, DvrStartRecordingReceiver.class);
- PendingIntent alarmIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0);
+ PendingIntent alarmIntent =
+ PendingIntent.getBroadcast(mContext, 0, intent, PendingIntent.FLAG_IMMUTABLE);
// This will cancel the previous alarm.
mAlarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, wakeAt, alarmIntent);
} else {
diff --git a/src/com/android/tv/features/TvFeatures.java b/src/com/android/tv/features/TvFeatures.java
index 5282c28c..ebd7cb9a 100644
--- a/src/com/android/tv/features/TvFeatures.java
+++ b/src/com/android/tv/features/TvFeatures.java
@@ -101,5 +101,8 @@ 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;
+
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..0ce5d931 100644
--- a/src/com/android/tv/onboarding/OnboardingActivity.java
+++ b/src/com/android/tv/onboarding/OnboardingActivity.java
@@ -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/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/receiver/BootCompletedReceiver.java b/src/com/android/tv/receiver/BootCompletedReceiver.java
index 0eb03bec..0bf6ecf3 100644
--- a/src/com/android/tv/receiver/BootCompletedReceiver.java
+++ b/src/com/android/tv/receiver/BootCompletedReceiver.java
@@ -56,6 +56,11 @@ public class BootCompletedReceiver extends BroadcastReceiver {
Log.wtf(TAG, "Stopping because device does not have a TvInputManager");
return;
}
+ String action = intent.getAction();
+ if (!Intent.ACTION_BOOT_COMPLETED.equals(action)) {
+ Log.w(TAG, "invalid action " + action);
+ return;
+ }
if (DEBUG) Log.d(TAG, "boot completed " + intent);
Starter.start(context);
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/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..3ac841c2 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,9 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV
return;
}
mBlockScreenView.setVisibility(VISIBLE);
+ if (mTvIAppView != null) {
+ mTvIAppView.setVisibility(INVISIBLE);
+ }
mBlockScreenView.setBackgroundImage(null);
if (blockReason == VIDEO_UNAVAILABLE_REASON_SCREEN_BLOCKED) {
mBlockScreenView.setIconVisibility(true);
@@ -1007,6 +1051,9 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV
if (mBlockScreenView.getVisibility() == VISIBLE) {
mBlockScreenView.fadeOut();
}
+ if (mTvIAppView != null) {
+ mTvIAppView.setVisibility(VISIBLE);
+ }
}
}
diff --git a/src/com/android/tv/ui/TvOverlayManager.java b/src/com/android/tv/ui/TvOverlayManager.java
index cf1a9113..19af23b9 100644
--- a/src/com/android/tv/ui/TvOverlayManager.java
+++ b/src/com/android/tv/ui/TvOverlayManager.java
@@ -55,6 +55,7 @@ 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 +199,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;
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..aaee1047 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;
@@ -362,6 +366,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/robotests/src/com/android/tv/MediaSessionWrapperTest.java b/tests/robotests/src/com/android/tv/MediaSessionWrapperTest.java
index 5be62acb..c0263fa7 100644
--- a/tests/robotests/src/com/android/tv/MediaSessionWrapperTest.java
+++ b/tests/robotests/src/com/android/tv/MediaSessionWrapperTest.java
@@ -56,9 +56,8 @@ public class MediaSessionWrapperTest {
@Before
public void setUp() {
- pendingIntent =
- PendingIntent.getActivity(
- RuntimeEnvironment.application, TEST_REQUEST_CODE, new Intent(), 0);
+ pendingIntent = PendingIntent.getActivity(RuntimeEnvironment.application, TEST_REQUEST_CODE,
+ new Intent(), PendingIntent.FLAG_IMMUTABLE);
mediaSessionWrapper =
new MediaSessionWrapper(RuntimeEnvironment.application, pendingIntent) {
@Override
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/src/com/android/tv/tuner/setup/BaseTunerSetupActivity.java b/tuner/src/com/android/tv/tuner/setup/BaseTunerSetupActivity.java
index 05026907..99f3e6dc 100644
--- a/tuner/src/com/android/tv/tuner/setup/BaseTunerSetupActivity.java
+++ b/tuner/src/com/android/tv/tuner/setup/BaseTunerSetupActivity.java
@@ -463,8 +463,8 @@ public abstract class BaseTunerSetupActivity extends SetupActivity {
*/
private static PendingIntent createPendingIntentForSetupActivity(
Context context, Intent tunerSetupIntent) {
- return PendingIntent.getActivity(
- context, 0, tunerSetupIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+ return PendingIntent.getActivity(context, 0, tunerSetupIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT|PendingIntent.FLAG_IMMUTABLE);
}
/** Creates {@link Tuner} instances in a worker thread * */