Snap for 10453563 from 489f952460ffe2ef1eb1348b2083bba4de88567f to mainline-conscrypt-release

Change-Id: I6f2efef3d7d08072595880b8af77cbd0a9818ed2
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 75e2c4d..6a2d435 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 e904f5c..a386cda 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 e59bcd6..bf76c9c 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 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 0000000..eada4de
--- /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 0000000..72cd22f
--- /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 0000000..a51bc56
--- /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 0000000..915c352
--- /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 0000000..c209e78
--- /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 0000000..4f0f1d6
--- /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 0000000..948a307
--- /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 0000000..28d4b77
--- /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 0000000..aa7d642
--- /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 0000000..d2a0a25
--- /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 0000000..d0c33d7
--- /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 0000000..d207c99
--- /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 0000000..87020f2
--- /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 0000000..c53748e
--- /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 0000000..d85ab77
--- /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 d91a189..29aff21 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="                                                        ~~~~~~~~~~~~~~~">
+        message="Call requires API level 24 (current min is 23): `android.media.tv.TvInputInfo#getTunerCount`">
         <location
-            file="packages/apps/TV/src/com/android/tv/util/TvInputManagerHelper.java"
-            line="472"
-            column="57"/>
+            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="510"
-            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.TvInputInfo#loadCustomLabel`"
-        errorLine1="            String customLabel = canonicalizeLabel(input.loadCustomLabel(mContext));"
-        errorLine2="                                                         ~~~~~~~~~~~~~~~">
+        message="Method reference requires API level 24 (current min is 23): `MainActivity.super::enterPictureInPictureMode`">
         <location
-            file="packages/apps/TV/src/com/android/tv/search/TvProviderSearch.java"
-            line="535"
-            column="58"/>
+            file="packages/apps/TV/src/com/android/tv/MainActivity.java"
+            line="2435"/>
     </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="                                                            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/TV/src/com/android/tv/util/Utils.java"
-            line="276"
-            column="61"/>
-    </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 0000000..3f6f8e6
--- /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 b6a0a3a..6347f89 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 0000000..e12e0bf
--- /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 252d6f4..10f4402 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 b68feb1..f46d7b9 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 e272244..b36827d 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 fe13898..351f001 100644
--- a/src/com/android/tv/ChannelTuner.java
+++ b/src/com/android/tv/ChannelTuner.java
@@ -36,7 +36,7 @@
 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 ea17751..57fc883 100644
--- a/src/com/android/tv/InputSessionManager.java
+++ b/src/com/android/tv/InputSessionManager.java
@@ -18,6 +18,7 @@
 
 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 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 8dbafe4..cea293d 100644
--- a/src/com/android/tv/MainActivity.java
+++ b/src/com/android/tv/MainActivity.java
@@ -18,6 +18,7 @@
 
 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.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.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.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.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 @@
                 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 @@
         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 @@
 
     private String mLastInputIdFromIntent;
 
+    private IAppManager mIAppManager;
+
     private final Handler mHandler = new MainActivityHandler(this);
     private final Set<OnActionClickListener> mOnActionClickListeners = new ArraySet<>();
 
@@ -406,6 +419,13 @@
                                 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 @@
             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 @@
         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 @@
             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 @@
         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 @@
         }
 
         mTvView.start();
-        mAudioManagerHelper.setVolumeByAudioFocusStatus();
+        mAudioManagerHelper.requestAudioFocus();
         tune(true);
     }
 
@@ -1126,6 +1160,9 @@
     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 @@
      * @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 @@
         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 @@
         }
     }
 
-    private void stopTv() {
+    public void stopTv() {
         stopTv(null, false);
     }
 
@@ -1932,12 +1972,21 @@
 
     @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 @@
         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 @@
             }
             applyDisplayRefreshRate(info.getVideoFrameRate());
             mTvViewUiManager.updateTvAspectRatio();
-            applyMultiAudio(
+            applyMultiAudio(allowAutoSelectionOfTrack,
                     allowAutoSelectionOfTrack ? null : getSelectedTrack(TvTrackInfo.TYPE_AUDIO));
             applyClosedCaption();
             mOverlayManager.getMenu().onStreamInfoChanged();
@@ -2989,6 +3038,14 @@
                         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 @@
 
         @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 e7f8910..2a4a556 100644
--- a/src/com/android/tv/SetupPassthroughActivity.java
+++ b/src/com/android/tv/SetupPassthroughActivity.java
@@ -109,7 +109,6 @@
                 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 @@
                     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 5d0e9c8..59e2406 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 @@
 
     @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 f31290d..5be1179 100644
--- a/src/com/android/tv/data/ChannelImpl.java
+++ b/src/com/android/tv/data/ChannelImpl.java
@@ -18,6 +18,7 @@
 
 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 @@
         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 e4237bf..f323423 100644
--- a/src/com/android/tv/data/StreamInfo.java
+++ b/src/com/android/tv/data/StreamInfo.java
@@ -44,6 +44,8 @@
 
     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 0000000..c5ffbaa
--- /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 f309537..475c17f 100644
--- a/src/com/android/tv/dvr/recorder/RecordingScheduler.java
+++ b/src/com/android/tv/dvr/recorder/RecordingScheduler.java
@@ -322,7 +322,8 @@
             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 5282c28..ebd7cb9 100644
--- a/src/com/android/tv/features/TvFeatures.java
+++ b/src/com/android/tv/features/TvFeatures.java
@@ -101,5 +101,8 @@
     /** 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 0000000..682b35c
--- /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 dd386d8..0ce5d93 100644
--- a/src/com/android/tv/onboarding/OnboardingActivity.java
+++ b/src/com/android/tv/onboarding/OnboardingActivity.java
@@ -193,7 +193,7 @@
                                     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 5fa7606..9578e24 100644
--- a/src/com/android/tv/receiver/AudioCapabilitiesReceiver.java
+++ b/src/com/android/tv/receiver/AudioCapabilitiesReceiver.java
@@ -67,7 +67,8 @@
     }
 
     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 0eb03be..0bf6ecf 100644
--- a/src/com/android/tv/receiver/BootCompletedReceiver.java
+++ b/src/com/android/tv/receiver/BootCompletedReceiver.java
@@ -56,6 +56,11 @@
             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 7bf0469..b39ac4e 100644
--- a/src/com/android/tv/setup/SystemSetupActivity.java
+++ b/src/com/android/tv/setup/SystemSetupActivity.java
@@ -53,6 +53,7 @@
     private static final int REQUEST_CODE_START_SETUP_ACTIVITY = 1;
 
     @Inject TvInputManagerHelper mInputManager;
+    @Inject SetupUtils mSetupUtils;
     @Inject UiFlags mUiFlags;
 
     @Override
@@ -97,7 +98,7 @@
                                     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 a0cfad3..8265d17 100644
--- a/src/com/android/tv/ui/SelectInputView.java
+++ b/src/com/android/tv/ui/SelectInputView.java
@@ -287,10 +287,18 @@
         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 a736e79..3ac841c 100644
--- a/src/com/android/tv/ui/TunableTvView.java
+++ b/src/com/android/tv/ui/TunableTvView.java
@@ -19,6 +19,7 @@
 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.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 @@
     private final InputSessionManager mInputSessionManager;
 
     private int mChannelSignalStrength;
+    private TvInteractiveAppView mTvIAppView;
 
     private final TvInputCallbackCompat mCallback =
             new TvInputCallbackCompat() {
@@ -413,6 +417,25 @@
                         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 @@
                         });
         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 @@
         }
     }
 
+    @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 @@
         void onContentAllowed();
 
         void onChannelSignalStrength();
+
+        @TargetApi(Build.VERSION_CODES.TIRAMISU)
+        void onAitInfoUpdated(String inputId, AitInfo aitInfo);
     }
 
     public void unblockContent(TvContentRating rating) {
@@ -976,6 +1017,9 @@
                 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 @@
             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 cf1a911..19af23b 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.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 @@
         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 0000000..b56a1d6
--- /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 1c03b6a..762a190 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.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 @@
                         }
                     });
         }
+
+        //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 52b3e3e..aaee104 100644
--- a/src/com/android/tv/util/SetupUtils.java
+++ b/src/com/android/tv/util/SetupUtils.java
@@ -31,14 +31,18 @@
 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 @@
     }
 
     /**
+     * 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 ae79e7e..1a5434c 100644
--- a/src/com/android/tv/util/TvSettings.java
+++ b/src/com/android/tv/util/TvSettings.java
@@ -53,6 +53,9 @@
     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 @@
                 .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 5be62ac..c0263fa 100644
--- a/tests/robotests/src/com/android/tv/MediaSessionWrapperTest.java
+++ b/tests/robotests/src/com/android/tv/MediaSessionWrapperTest.java
@@ -56,9 +56,8 @@
 
     @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 a0db5e0..f359c6b 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 0000000..20c73de
--- /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 03e7965..d59ccd9 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 @@
     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 @@
 
     @Override
     public TvInputSessionImpl onCreateSession(String inputId) {
+        if (DEBUG) {
+            Log.d(TAG, "onCreateSession(inputId=" + inputId + ")");
+        }
         return new TvInputSessionImpl(this);
     }
 
@@ -100,12 +91,16 @@
         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 @@
             if (mVideoFilter != null) {
                 mVideoFilter.close();
             }
+            if (mSectionFilter != null) {
+                mSectionFilter.close();
+            }
             if (mDvr != null) {
                 mDvr.close();
                 mDvr = null;
@@ -170,7 +168,11 @@
                 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 @@
             }
         }
 
-        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());
-                                }
-                            }
-                        }
-
-                        @Override
-                        public void onFilterStatusChanged(Filter filter, int status) {
-                            if (DEBUG) {
-                                Log.d(TAG, "onFilterEvent audio, status=" + status);
-                            }
-                        }
-                    });
-            AvSettings settings =
-                    AvSettings.builder(Filter.TYPE_TS, true).setPassthrough(false).build();
-            audioFilter.configure(
-                    TsFilterConfiguration.builder().setTpid(AUDIO_TPID)
-                            .setSettings(settings).build());
-            return audioFilter;
-        }
-
-        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);
-                                    }
-                                }
-                            }
-                        }
-
-                        @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;
-                            }
-                        }
-                    });
-            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);
-                        }
-                    });
-            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() {
+        private FilterCallback videoFilterCallback() {
+            return new FilterCallback() {
                 @Override
-                public void onTuneEvent(int tuneEvent) {
+                public void onFilterEvent(Filter filter, FilterEvent[] events) {
                     if (DEBUG) {
-                        Log.d(TAG, "onTuneEvent " + tuneEvent);
+                        Log.d(TAG, "onFilterEvent video, size=" + events.length);
                     }
-                    long read = mDvr.read(INPUT_FILE_MAX_SIZE);
-                    if (DEBUG) {
-                        Log.d(TAG, "read=" + read);
+                    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];
+
+                            MediaEventData storedEvent = MediaEventData.generateEventData(me);
+                            if (storedEvent == null) {
+                                continue;
+                            }
+                            mDataQueue.add(storedEvent);
+                            if (SAVE_DATA) {
+                                mSavedData.add(storedEvent);
+                            }
+                        }
                     }
                 }
-            });
-            mTuner.tune(feSettings);
+
+                @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 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());
+                        }
+                        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);
+
+                            handleSection(data);
+                        }
+                    }
+                }
+
+                @Override
+                public void onFilterStatusChanged(Filter filter, int status) {
+                    if (DEBUG) {
+                        Log.d(TAG, "onFilterStatusChanged section, status=" + status);
+                    }
+                }
+            };
         }
 
         private boolean initCodec() {
@@ -335,6 +277,7 @@
 
             if (mMediaCodec == null) {
                 Log.e(TAG, "null codec!");
+                mVideoAvailable = false;
                 notifyVideoUnavailable(VIDEO_UNAVAILABLE_REASON_UNKNOWN);
                 return false;
             }
@@ -347,14 +290,26 @@
             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 @@
                             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 @@
             }
         }
 
-        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 @@
                     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 @@
             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 @@
         }
 
     }
+
+    /**
+     * 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 b932b60..4774243 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 @@
 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 0000000..9638f33
--- /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 0502690..99f3e6d 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 @@
      */
     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 * */